Table of contents
Open Table of contents
Problem Statement
Build a shape system using the factory pattern:
- Define a
Shapeinterface witharea()andperimeter()methods - Implement
Circle,Rectangle, andTriangle - Create a
ShapeFactorythat produces shapes from a configuration object or string identifier - 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 withswitch/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/casesyntax 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.