Search code examples
pythondesign-patternsumlaggregationfactory-pattern

Can this be considered a Factory Method pattern (or an equivalent)?


In a course I'm taking, a PizzaStore that uses a SimplePizzaFactory class which handles concrete pizza instantiation, described as follows: enter image description here

In the course, an intro to the factory method pattern is described by introducing the need to provide the PizzaStore with extra levels of specificity and the ability to provide the same types of Pizzas (Viggie, Cheese, etc..) but in a NY-Style and in a Chicago-Style, so we have a new set of subclasses (NYStyleViggiePizza, NYStyleCheesePizza, .. ChicagoStyleViggiePizza, ChicagoStyleCheesePizza, .. )

The solution introduced by the instructor was to use the factory method pattern as follows:

(UML)

enter image description here

Code re-written in python:

# Pizzas Subclasses are defined elsewhere
from abc import ABC, abstractmethod

class PizzaStore(ABC):
    @abstractmethod
    def create_pizza(self):
        pass

    def order_pizza(self,type_of_pizza):
        type_of_pizza = type_of_pizza.lower()
        pizza = self.create_pizza(type_of_pizza)
        pizza.prepare()
        pizza.bake()
        pizza.box()
        return pizza


class NYPizzaStore(PizzaStore):
    def create_pizza(self, type_of_pizza):
        if type_of_pizza == "cheese":
            pizza = NYStyleCheesePizza()
        
        elif type_of_pizza == "pepperoni":
            pizza = NYStylePepperoniPizza()

        elif type_of_pizza == "clam":
            pizza = NYStyleClamPizza()

        elif type_of_pizza == "viggie":
            pizza = NYStyleViggiePizza()
        else:
            raise Exception("You need to specify a type of NY pizza.")   
        return pizza


class ChicagoPizzaStore(PizzaStore):
    def create_pizza(self,type_of_pizza):
        if type_of_pizza == "cheese":
            pizza = ChicagoStyleCheesePizza()
        elif type_of_pizza == "pepperoni":
            pizza = ChicagoStylePepperoniPizza()

        elif type_of_pizza == "clam":
            pizza = ChicagoStyleClamPizza()

        elif type_of_pizza == "viggie":
            pizza = ChicagoStyleViggiePizza()
        else:
            raise Exception("You need to specify a type of NY pizza.")
        
        return pizza


# ===== Driver Code =====
# NY store
ny_pizza_store = NYPizzaStore()
ny_pizza_store.order_pizza("Cheese")
ny_pizza_store.order_pizza("Pepperoni")


print()


# Chicago store
chicago_pizza_store = ChicagoPizzaStore()
chicago_pizza_store.order_pizza("Cheese")
chicago_pizza_store.order_pizza("Pepperoni")

I tried the following design before jumping in to the factory method, where I kept the PizzaStore as it is and replaced the SimpleFactoryPizza with two new classes: NYPizzaFactory and ChicagoPizzaFactory

enter image description here

Code re-written in python:

class NYPizzaFactory():
    def create_pizza(self,type_of_pizza):
        if type_of_pizza == "cheese":
            pizza = NYStyleCheesePizza()
        elif type_of_pizza == "pepperoni":
            pizza = NYStylePepperoniPizza()

        elif type_of_pizza == "clam":
            pizza = NYStyleClamPizza()

        elif type_of_pizza == "viggie":
            pizza = NYStyleViggiePizza()
        else:
            raise Exception("You need to specify a type of NY pizza.")
        
        return pizza

class ChicagoPizzaFactory():
    def create_pizza(self,type_of_pizza):
        if type_of_pizza == "cheese":
            pizza = ChicagoStyleCheesePizza()
        elif type_of_pizza == "pepperoni":
            pizza = ChicagoStylePepperoniPizza()

        elif type_of_pizza == "clam":
            pizza = ChicagoStyleClamPizza()

        elif type_of_pizza == "viggie":
            pizza = ChicagoStyleViggiePizza()
        else:
            raise Exception("You need to specify a type of NY pizza.")
        
        return pizza

# PizzaStore is the same as before

class PizzaStore:
    def __init__(self, pizza_factory_obj):
        self.pizza_factory_obj = pizza_factory_obj

    def order_pizza(self,type_of_pizza):
        type_of_pizza = type_of_pizza.lower() 
        pizza = self.pizza_factory_obj.create_pizza(type_of_pizza)
        pizza.prepare()
        pizza.bake()
        pizza.box()
        return pizza

# ===== Driver Code ======
# NY Store
ny_pizza_factory = NYPizzaFactory()
ny_pizza_store = PizzaStore(ny_pizza_factory)
ny_pizza_store.order_pizza("Cheese")
print()
ny_pizza_store.order_pizza("Pepperoni")
print()

# Chicago Store
chicago_pizza_factory = ChicagoPizzaFactory()
chicago_pizza_store = PizzaStore(chicago_pizza_factory)
chicago_pizza_store.order_pizza("Cheese")
print()
chicago_pizza_store.order_pizza("Pepperoni")

I understand that a Factory Method lets a class defer instantiation to its subclasses, where these subclasses will include the implementation of that "factory method".

Question 1:

  • By definition, Is my solution not considered a factory pattern? What are the differences and downsides to the approach I tried compared to the factory method represented?

Question 2:

The factory method structure is generalized by the following UML: (from the course material)

enter image description here

In the "Design Patterns: Elements of Reusable Object-Oriented Software" book, the Factory method pattern's structure is described via this UML:

enter image description here

  • The arrow between the factory and the product represents "aggregation" from the course material, while the UML diagram in the book represents "Dependency" (I think), Which one represents the correct relationship and why?

Solution

  • Q1: Is your implementation a factory?

    The factory method pattern intents to

    define an interface for creating an object, but let the subclass decide which class to instantiate - (GoF, page 107).

    Your design and re-implementation do exactly that and are factories.

    More detailed arguments

    In your re-written solution, according to your diagram, PizzaStore is some kind of context, which may use either a NYPizzaFactory or a ChicagoPizzaFactory or both. Your code is much clearer thant the UML, since you inject the factory into the store at construction.

    Your factories all appear to be concrete creators that produce instances of a product Pizza. Each concrete creator creates a different set of concrete pizzas. Taken individually, each of your XxxPizzaFactory appears to correspond to a concrete factory, the FactoryMethod() being called create_pizza().

    The only thing that is missing in the diagram and in the code, is a guarantee that the factories are interchangeable, by letting them inherit from a more general PizzaFactory. Fortunately for you, Python's dynamic typing can cope with the absence of same base class. But for maintenance purpose, better structure the classes in UML and in Python with the explicit subclassing.

    Factory or Abstract factory?

    The fact that each of your concrete factories can create different kind of Pizza is a variant of the pattern that is called "Parameterized factory method" GoF, page 110). So it is definitively the factory pattern, just that the create_pizza() takes an argument which specifies which concrete pizza to instantiate.

    This is not an abstract factory, because the abstract factory aims at creating families of related or dependent products. Each kind of product in the family has its specific factory method. This would be the case here, if you would have several create methods, such as create_pizza(), create_dessert(), create_wine(). This is not the case, here, since only one kind of product is created by each factory.

    Q2: Aggregation or dependency?

    First of all, GoF does not use UML (see GoF, page 363). UML was not yet officially published when the book was written:

    • GoF uses OMT and not UML for class diagrams
    • GoF uses a mix of Booch and Objectory for interaction diagrams.

    Interestingly, OMT, Booch and Objectory were the three leading OO notations that were merged to create UML.

    From an UML perspective,

    • the relation between ConcreteCreator and ConcreteProduct is a «create» dependency. In fact, there should also be a «create» dependency between Creator and Product.

    • There should be no aggregation nor no association between the Factory and the Product (unless, either product would keep track of the factory that created it or the factory would keep a list of all the products it created).

    • There is a problem about the side of the agregation: you could use an aggregation between Client and Factory, but with the diamond on the client side. Nevertheless, while it is not fundamentally wrong a simple association would better represent the relation between the two classes.

    Additional information:

    • Why a simple association would be more suitable than the aggregation in GoF: it's the second part of an answer to an unrelated question (in its comments, I listed the incont uses in GoF which alternates between the two notations)
    • This answer and this answer to unrelated questions explain why the fact of creating, returning or temporarily using a class in a method is not sufficient for making an association (and since aggregation is stronger than a simple association, it's even less sufficient for an aggregation).

    PS: I used GoF to refer to "Design Patterns: Elements of Reusable Object-Oriented Software"