2

Consider the following python3 code:

from abc import ABC, abstractmethod

class Food(ABC):
    _food_factory_map = {}
    _recipes = {}

    @classmethod
    def getFood(cls, foodName):
        return cls._food_factory_map[foodName]()

    @classmethod
    def registerFood(cls, foodName, recipe):
        def callable(foodClass):
            cls._food_factory_map[foodName] = foodClass
            cls._recipes[foodName] = recipe
            return foodClass
        return callable

    def __init__(self):
        print("Starting Food prep")

    @abstractmethod
    def prepareFood(self):
        ...

    def serveFood(self):
        food = self.prepareFood()
        print(food)

@Food.registerFood('Burger', 'Burger Recipe')
class Burger(Food):
    def prepareFood(self):
        return 'Burger is ready!'

@Food.registerFood('Pizza', 'Pizza Recipe')
class Pizza(Food):
    def prepareFood(self):
        return 'Pizza is ready!'

if __name__ == '__main__':
    fs = ('Burger', 'Pizza')
    for f in fs:
        print(f"Food Recipe: {Food._recipes[f]}")
        food = Food.getFood(f)
        food.serveFood()

Here the abstract base class Food has class methods that implement the Factory design pattern. A client can register their own concrete classes with registerFood and retrieve them with getFood. The abstract base class also implements other abstract and concrete methods to define the interface.

One can also separate out the two class methods (getFood and registerFood) and the two class members (_food_factory_map and _recipes) into a class of it's own (FoodFactory)

class FoodFactory(ABC):
    _food_factory_map = {}
    _recipes = {}

    @classmethod
    def getFood(cls, foodName):
        return cls._food_factory_map[foodName]()

    @classmethod
    def registerFood(cls, foodName, recipe):
        def callable(foodClass):
            cls._food_factory_map[foodName] = foodClass
            cls._recipes[foodName] = recipe
            return foodClass
        return callable

All the client code will then change to using FoodFactory instead of Food.

@FoodFactory.registerFood('Pizza', 'Pizza Recipe')
class Pizza(Food):
    def prepareFood(self):
        return 'Pizza is ready!'

if __name__ == '__main__':
    fs = ('Burger', 'Pizza')
    for f in fs:
        print(f"Food Recipe: {FoodFactory._recipes[f]}")
        food = FoodFactory.getFood(f)
        food.serveFood()

Which of these implementations is better and why? Or is it perfectly reasonable to use either? Is it language dependent?

One advantage I can think off is that with the second method, one can override the __new__ of FoodFactory to directly return an instance of the appropriate food rather than calling a separate getFood method. But that difference seems largely cosmetic.

2 Answers 2

4

Your blanket question

OOP Best practices: Is there any reason to separate out Factory functionality from an abstract base class?

A factory is warranted when a constructor is no longer the appropriate solution to the problem at hand. So you could argue that a constructor is a really simple factory, and that you only need to consider a factory (i.e. the separate class) when the construction logic gets complicated enough to warrant separation from the factory's product (i.e. the original class).

So, to answer your blanket question if there is any reason to every separate it out, the answer is yes. Specifically, in cases where the construction logic is sufficiently complex to warrant it, or e.g. when you need an injectable dependency which is able to generate products on the fly.


Your concrete example

It doesn't make semantical sense for Food to know a collection of recipes. That's meta-knowledge. Analogously, your car does not contain its own construction blueprint, let alone that of many other cars was well.

My intuition is telling me that this is a composition-over-inheritance violation. It makes more sense for the collection of recipes to live in a dedicated class (let's call it RecipeBook).

Inheritance is the wrong construct to use here. You're not making any use of the polymorphism that inheritance provides. A Burger is not a collection of recipes, and that's what your inheritance is currently claiming.

I'm no Python dev, so I don't feel comfortable writing an improved version of your snippet.

8
  • I think you've missed the question. A constructor isn't a factory. a factory class creates objects without the caller knowing the exact class. Are you thinking of a 'builder'?
    – Ewan
    Commented Apr 3, 2023 at 13:14
  • @Ewan: I think you've missed what I was pointing out. your comment uses "factory" as the strict definition of a particular pattern. I use the word factory more broadly as "something that generates a [product]", which is a spectrum where you can find a constructor on the simple end and "the factory pattern" (in its many shapes) on the other end. The factory pattern supplants the constructor, i.e. it exists because the usual constructor approach is not the right fit, and my answer points out that these are different approach to a similar goal and that context defines what it appropriate.
    – Flater
    Commented Apr 3, 2023 at 23:07
  • @Ewan: I also disagree with your definition of a factory. There are different kinds of factories. Some factories return a base type and have a consumer unaware of the derived concrete type that is being used; you are correct about that. However, there are also other factories where the returning type is known to the consumer, and the factory's expertise is not in choosing between available subtypes, but rather its ability to perform the difficult construction of the (known) type. Both are factories, but only one would apply to your (IMO overly restrictive) definition.
    – Flater
    Commented Apr 3, 2023 at 23:09
  • which of those two definitions is the OP using in their question?
    – Ewan
    Commented Apr 4, 2023 at 15:38
  • your "broader definition" is normally called a builder
    – Ewan
    Commented Apr 4, 2023 at 15:40
0

I'd say the main reason here is that you have applied a singleton pattern to your factory.

You can never have a different factory.

You can never mock the factory.

2
  • Hi, Can you please clarify a bit? Factory being a singleton is the main reason for what? And what do you mean by mock the factory?
    – raghavj
    Commented Apr 3, 2023 at 3:59
  • reason to separate the factory class. and mock for tests
    – Ewan
    Commented Apr 3, 2023 at 13:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.