Skip to content
Code Kata
Go back

Kata: Factory Pattern

Suggest edit

Table of contents

Open Table of contents

Problem Statement

Build a shape system using the factory pattern:

  1. Define a Shape interface with area() and perimeter() methods
  2. Implement Circle, Rectangle, and Triangle
  3. Create a ShapeFactory that produces shapes from a configuration object or string identifier
  4. The factory should validate inputs and throw descriptive errors

Bonus: Implement an abstract factory that creates themed UI components (Button, Input, Card) for different design systems.

Concepts

The factory pattern encapsulates object creation, centralizing the logic for deciding which class to instantiate. It’s one of the most commonly used creational patterns.

When to use: When the creation logic is complex, when you want to decouple consumers from concrete classes, or when the type to create is determined at runtime.

TypeScript: Discriminated unions offer a type-safe alternative. See TypeScript Core Concepts.

PHP: Enums + match expressions create elegant factories. See PHP 8.x Updates and PHP 8.x Updates.

Python: Pattern matching provides a clean dispatch mechanism. See Python 3.10+ Updates.

Boilerplate

TypeScript

// factory.ts
interface Shape {
  area(): number;
  perimeter(): number;
  describe(): string;
}

type ShapeConfig =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; a: number; b: number; c: number };

class ShapeFactory {
  static create(config: ShapeConfig): Shape {
    // TODO
    throw new Error("Not implemented");
  }
}

Hints

Hint 1: Factory dispatch

Use the kind field from the discriminated union to decide which class to instantiate. TypeScript narrows the type automatically in each case.

Hint 2: Triangle validation

A valid triangle must satisfy the triangle inequality: the sum of any two sides must be greater than the third side. Use Heron’s formula for the area.

Solution

TypeScript

interface Shape {
  area(): number;
  perimeter(): number;
  describe(): string;
}

type ShapeConfig =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; a: number; b: number; c: number };

class Circle implements Shape {
  constructor(private radius: number) {
    if (radius <= 0) throw new Error("Radius must be positive");
  }

  area(): number {
    return Math.PI * this.radius ** 2;
  }

  perimeter(): number {
    return 2 * Math.PI * this.radius;
  }

  describe(): string {
    return `Circle (r=${this.radius})`;
  }
}

class Rectangle implements Shape {
  constructor(
    private width: number,
    private height: number
  ) {
    if (width <= 0 || height <= 0) throw new Error("Dimensions must be positive");
  }

  area(): number {
    return this.width * this.height;
  }

  perimeter(): number {
    return 2 * (this.width + this.height);
  }

  describe(): string {
    return `Rectangle (${this.width}x${this.height})`;
  }
}

class Triangle implements Shape {
  constructor(
    private a: number,
    private b: number,
    private c: number
  ) {
    if (a <= 0 || b <= 0 || c <= 0) throw new Error("Sides must be positive");
    if (a + b <= c || a + c <= b || b + c <= a) {
      throw new Error("Invalid triangle: violates triangle inequality");
    }
  }

  area(): number {
    const s = (this.a + this.b + this.c) / 2;
    return Math.sqrt(s * (s - this.a) * (s - this.b) * (s - this.c));
  }

  perimeter(): number {
    return this.a + this.b + this.c;
  }

  describe(): string {
    return `Triangle (${this.a}, ${this.b}, ${this.c})`;
  }
}

class ShapeFactory {
  static create(config: ShapeConfig): Shape {
    switch (config.kind) {
      case "circle":
        return new Circle(config.radius);
      case "rectangle":
        return new Rectangle(config.width, config.height);
      case "triangle":
        return new Triangle(config.a, config.b, config.c);
    }
  }
}

// Usage
const shapes = [
  ShapeFactory.create({ kind: "circle", radius: 5 }),
  ShapeFactory.create({ kind: "rectangle", width: 4, height: 6 }),
  ShapeFactory.create({ kind: "triangle", a: 3, b: 4, c: 5 }),
];

for (const shape of shapes) {
  console.log(`${shape.describe()}: area=${shape.area().toFixed(2)}`);
}

Python

from __future__ import annotations
from dataclasses import dataclass
from math import pi, sqrt
from typing import Protocol

class Shape(Protocol):
    def area(self) -> float: ...
    def perimeter(self) -> float: ...

@dataclass(frozen=True)
class Circle:
    radius: float

    def area(self) -> float:
        return pi * self.radius ** 2

    def perimeter(self) -> float:
        return 2 * pi * self.radius

@dataclass(frozen=True)
class Rectangle:
    width: float
    height: float

    def area(self) -> float:
        return self.width * self.height

    def perimeter(self) -> float:
        return 2 * (self.width + self.height)

@dataclass(frozen=True)
class Triangle:
    a: float
    b: float
    c: float

    def __post_init__(self) -> None:
        if self.a + self.b <= self.c or self.a + self.c <= self.b or self.b + self.c <= self.a:
            raise ValueError("Invalid triangle")

    def area(self) -> float:
        s = (self.a + self.b + self.c) / 2
        return sqrt(s * (s - self.a) * (s - self.b) * (s - self.c))

    def perimeter(self) -> float:
        return self.a + self.b + self.c

def create_shape(config: dict) -> Shape:
    match config:
        case {"kind": "circle", "radius": r}:
            return Circle(r)
        case {"kind": "rectangle", "width": w, "height": h}:
            return Rectangle(w, h)
        case {"kind": "triangle", "a": a, "b": b, "c": c}:
            return Triangle(a, b, c)
        case _:
            raise ValueError(f"Unknown shape: {config}")

PHP

enum ShapeType: string {
    case Circle = 'circle';
    case Rectangle = 'rectangle';
    case Triangle = 'triangle';
}

class ShapeFactory {
    public static function create(ShapeType $type, array $params): Shape {
        return match ($type) {
            ShapeType::Circle => new Circle($params['radius']),
            ShapeType::Rectangle => new Rectangle($params['width'], $params['height']),
            ShapeType::Triangle => new Triangle($params['a'], $params['b'], $params['c']),
        };
    }
}

$shape = ShapeFactory::create(ShapeType::Circle, ['radius' => 5]);

Complexity Analysis

Factory creation is O(1) — the factory itself adds no algorithmic overhead. The pattern is about code organization, not performance.

Cross-Language Notes

TypeScript: Discriminated unions (ShapeConfig) combined with switch/exhaustiveness checking make the factory pattern particularly type-safe. The compiler ensures all shape kinds are handled.

PHP: The combination of enums (8.1) and match expressions (8.0) makes PHP factories very readable. Enums prevent invalid type strings, and match ensures exhaustive handling.

Python: Structural pattern matching (3.10+) provides the cleanest factory dispatch. The match/case syntax with dict patterns reads almost like a specification.

Go: Go doesn’t have classes, so factories are just functions. The lack of generics (until Go 1.18) historically made Go factories more verbose.


Suggest edit
Share this post on:

Previous Post
Kata: Singleton Pattern
Next Post
Kata: Decorator Pattern