from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
# ===== Products (can be totally different types) =====
@dataclass
class Product1:
name: str = "Product1"
features: list[str] = field(default_factory=list)
def __str__(self) -> str:
return f"{self.name}({', '.join(self.features) or 'no features'})"
@dataclass
class Product2:
name: str = "Product2"
parts: list[str] = field(default_factory=list)
def __str__(self) -> str:
return f"{self.name}({', '.join(self.parts) or 'no parts'})"
# ===== Builder interface (per the UML) =====
class Builder(ABC):
@abstractmethod
def reset(self) -> None: ...
@abstractmethod
def buildStepA(self) -> None: ...
@abstractmethod
def buildStepB(self) -> None: ...
@abstractmethod
def buildStepZ(self) -> None: ...
@abstractmethod
def getResult(self):
"""Return the product and (optionally) prepare for a new build."""
# ===== Concrete builders (each yields a different product) =====
class ConcreteBuilder1(Builder):
def __init__(self) -> None:
self._product: Product1 | None = None
self.reset()
def reset(self) -> None:
self._product = Product1()
def buildStepA(self) -> None:
self._product.features.append("FeatureA1")
def buildStepB(self) -> None:
self._product.features.append("FeatureB1")
def buildStepZ(self) -> None:
self._product.features.append("FeatureZ1")
def getResult(self) -> Product1:
# Typical builder behavior: hand off the current product and reset
product = self._product
self.reset()
return product
class ConcreteBuilder2(Builder):
def __init__(self) -> None:
self._product: Product2 | None = None
self.reset()
def reset(self) -> None:
self._product = Product2()
def buildStepA(self) -> None:
self._product.parts.append("PartA2")
def buildStepB(self) -> None:
self._product.parts.append("PartB2")
def buildStepZ(self) -> None:
self._product.parts.append("PartZ2")
def getResult(self) -> Product2:
product = self._product
self.reset()
return product
# ===== Director (defines build order / reusable recipes) =====
class Director:
def __init__(self, builder: Builder) -> None:
self.builder = builder
def changeBuilder(self, builder: Builder) -> None:
self.builder = builder
def make(self, type: str = "simple") -> None:
"""
Reusable 'recipes' that decide which steps to call and in what order.
The Director does NOT return the product; the client asks the builder.
"""
self.builder.reset()
if type == "simple":
self.builder.buildStepA()
elif type == "standard":
self.builder.buildStepA()
self.builder.buildStepB()
elif type == "deluxe":
self.builder.buildStepB()
self.builder.buildStepZ()
elif type == "ultimate":
self.builder.buildStepA()
self.builder.buildStepB()
self.builder.buildStepZ()
else:
raise ValueError(f"Unknown build type: {type}")
# ===== Client code (matches the call sequence in the diagram) =====
if __name__ == "__main__":
# b = new ConcreteBuilder1()
b = ConcreteBuilder1()
# d = new Director(b)
d = Director(b)
# d.make()
d.make("ultimate")
# Product1 p = b.getResult()
p1 = b.getResult()
print(p1) # -> Product1(FeatureA1, FeatureB1, FeatureZ1)
# Switch builder at runtime and reuse the same Director "recipes"
d.changeBuilder(ConcreteBuilder2())
d.make("simple")
p2 = d.builder.getResult() # or keep a reference to the builder instance
print(p2) # -> Product2(PartA2)