Design pattern with Python 4: factory pattern

Posted by Schneider707 on Fri, 28 Jan 2022 20:52:29 +0100

Design pattern with Python 4: factory pattern

Factory pattern essentially includes two different design patterns: factory method and abstract factory. But they essentially encapsulate the created objects, so as to realize the decoupling design pattern to some extent, so they are introduced together.

Different from the Head First design pattern, the example of hamburger store is used here for illustration. Because I haven't eaten pizza several times and know little about the types of pizza, I don't use pizza store as an example, but in essence, there is no fundamental difference between the two except the name.

Hamburger

Suppose we want to open a hamburger restaurant and provide a variety of hamburgers for customers to order. The initial design may be as follows:

HamburgStore is our hamburger store. You can order through orderHamburg. The specific hamburgers include:

  • Zinger burger: spicy chicken leg Burger
  • MiniBurger: Rural drumstick Castle
  • New Orleans Roasted Burger
  • I refer to one found on the Internet KFC takeout menu , those interested can view the full menu by themselves.
  • The original name of the rural drumstick castle is Mini
  • My favorite is New Orleans drumstick castle 🙂

No matter what type of hamburger, it supports three basic operations: ready, cook and box, which correspond to three processes: preparation of ingredients, cooking and packaging.

So naturally, we can think of establishing an abstract class Hamburg (or interface) as the base class to carry out type constraints and define some general operations. The corresponding three processes can be defined as abstract methods.

In addition, in order to illustrate the dependency relationship, the dotted arrow is used here. At present, we do not do any additional abstraction. All specific operations to create the corresponding specific hamburger according to the given type are implemented in the orderHamburg method of HamburgStore, so this method depends on three specific hamburger classes, They are connected by dashed arrows indicating dependency.

The key code is shown below, and the complete code can be found in the Github project hamburg_store_v1:

#######################################################
# 
# HamburgStore.py
# Python implementation of the Class HamburgStore
# Generated by Enterprise Architect
# Created on:      19-6��-2021 15:37:37
# Original author: 70748
# 
#######################################################

from .Hamburg import Hamburg
from .ZingerBurger import ZingerBurger
from .MiniBurger import MiniBurger
from .NewOrleansRoastedBurger import NewOrleansRoastedBurger
class HamburgStore:
    def orderHamburg(self, type:str)->Hamburg:
        """Order a single hamburger
        type: Hamburger type
        """
        hamburg: Hamburg = None
        if type == "zinger":
            hamburg = ZingerBurger()
        elif type == "new_orliean":
            hamburg = NewOrleansRoastedBurger()
        elif type == "mini":
            hamburg = MiniBurger()
        else:
            pass
        if hamburg != None:
            hamburg.ready()
            hamburg.cook()
            hamburg.box()
        return hamburg

#######################################################
# 
# Hamburg.py
# Python implementation of the Class Hamburg
# Generated by Enterprise Architect
# Created on:      19-6��-2021 15:37:37
# Original author: 70748
# 
#######################################################

from abc import ABC, abstractmethod
class Hamburg(ABC):
    def __init__(self, name:str) -> None:
        """
        name: Hamburg name
        """
        super().__init__()
        self.name = name

    @abstractmethod
    def box():
        pass

    @abstractmethod
    def ready():
        pass

    @abstractmethod
    def cook():
        pass

    def __str__(self) -> str:
        return self.name
#######################################################
#
# MiniBurger.py
# Python implementation of the Class MiniBurger
# Generated by Enterprise Architect
# Created on:      19-6��-2021 15:37:37
# Original author: 70748
#
#######################################################
from .Hamburg import Hamburg


class MiniBurger(Hamburg):
    def __init__(self) -> None:
        super().__init__("Mini Burger")

    def box(self):
        print("box {}".format(self.name))

    def ready(self):
        print("Prepare ingredients")

    def cook(self):
        print("cook {}".format(self.name))

The test code is as follows:

from src.HamburgStore import HamburgStore
from src.Hamburg import Hamburg
store = HamburgStore()
hamburg: Hamburg = store.orderHamburg("mini")
print(hamburg)
hamburg = store.orderHamburg("zinger")
print(hamburg)
hamburg = store.orderHamburg("new_orliean")
print(hamburg)
# Prepare ingredients
# cook Mini Burger
# box Mini Burger
# Mini Burger
# Prepare ingredients
# cook Zinger Burger
# box Zinger Burger
# Zinger Burger
# Prepare ingredients
# cook New Orleans Roasted Burger
# box New Orleans Roasted Burger
# New Orleans Roasted Burger

As shown in the UML diagram, the orderHamburg method of HamburgStore is directly dependent on three specific hamburger classes and is tightly coupled. Therefore, the modification of any specific hamburger class, or the addition or deletion of a hamburger, requires the modification of the code in orderHamburg, which obviously violates the "open and closed principle" mentioned in the previous design pattern.

If you don't know what the open close principle is, please read Design mode with Python 3: decorator mode.

We can use a very simple way to improve.

Simple factory

Simple factory is really simple. It is not a design pattern, but more like a habit of writing code.

Let's look directly at the UML after using a simple factory:

We have created a new class SimpleFactory and entrusted the task of building a specific hamburger according to the given type to the static method getHamburg() of SimpleFactory. After this, the orderHamburg() method of HamburgStore will no longer depend on the specific hamburger class. It only depends on SimpleFactory and Hamburg. This will make the code in HamburgStore simpler, But its essence has not changed, except that the code changes caused by our subsequent adjustments to Hamburg have become the gethamburg () method of SimpleFactory. This is basically the solution to encapsulate this part of the code into another method, so it's not too much to call it a "simple" factory.

Similarly, only part of the key code modified relative to v1 version is shown here. For the complete code, see Github warehouse hamburg_store_v2:

from .Hamburg import Hamburg
from .ZingerBurger import ZingerBurger
from .NewOrleansRoastedBurger import NewOrleansRoastedBurger
from .MiniBurger import MiniBurger
class SimpleFactory:
    @classmethod
    def getHamburg(cls, type:str)->Hamburg:
        hamburg: Hamburg = None
        if type == "zinger":
            hamburg = ZingerBurger()
        elif type == "new_orliean":
            hamburg = NewOrleansRoastedBurger()
        elif type == "mini":
            hamburg = MiniBurger()
        else:
            pass
        return hamburg
#######################################################
# 
# HamburgStore.py
# Python implementation of the Class HamburgStore
# Generated by Enterprise Architect
# Created on:      19-6��-2021 15:37:37
# Original author: 70748
# 
#######################################################

from .Hamburg import Hamburg
from .SimpleFactory import SimpleFactory
class HamburgStore:
    def orderHamburg(self, type:str)->Hamburg:
        """Order a single hamburger
        type: Hamburger type
        """
        hamburg: Hamburg = SimpleFactory.getHamburg(type)
        if hamburg != None:
            hamburg.ready()
            hamburg.cook()
            hamburg.box()
        return hamburg

Again: simple factory is a programming habit, not a design pattern.

If our requirements remain unchanged, or simple changes are maintained, such as adding or deleting a hamburger, the design above has been done very well, and there is no need to use more complex design patterns to make changes.

We should know that all design patterns are to cope with more complex and frequently changing needs. We can't use design patterns in order to use design patterns. Instead, we will abandon the basics. If your current solution can cope with changes in requirements and works well, why change.

It is said that there is a saying in the field of programming: * * if there is no problem with the current program, don't try to change it** Of course, I personally think this is more of a joke. Appropriate reconstruction is quite necessary, otherwise the project will become a "shit mountain".

Therefore, in order to introduce the two design modes we will introduce later, let's assume that we have made a lot of money now. One hamburger store can't meet us anymore. We need to expand and open more hamburgers.

But correspondingly, this is also accompanied by more problems that we would not consider originally. For example, no matter what dishes are served in hamburgers in Sichuan, pepper must be doubled. Hunan's hamburger restaurant has three times the pepper directly. Hamburgers in Jiangsu cut chili peppers by half.

Let's take a look at what our code would look like if we didn't use design patterns.

No design mode

As can be seen from the UML diagram, we have created an abstract base class HamburgStore for hamburgers, upgraded the original hamburger store to the head office Beijing HamburgStore, built two new hamburgers, Sichuan HamburgStore and JiangsuHamburgStore, and fine tuned the dishes according to the regional characteristics. The chili in Jiangsu is halved, and the corresponding specific hamburger class is NotHotXXXBurger, The chili peppers in hamburgers in Sichuan are doubled, and the corresponding hamburger is veryhot xxxburger.

It can be seen that each specific hamburger restaurant also depends on specific local characteristics. Of course, we can decouple the simple factories mentioned above, add several simple factories, or add several static methods to a simple factory to decouple specific hamburgers from specific hamburgers. But as we said before, this is basically a way to treat the symptoms rather than the root causes. If we need to create more local specialties after opening a store, we need to frequently modify the code in the corresponding simple factory.

The code of this design can be essentially similar to the v1 version of hamburger code, except that it contains a large number of classes, so the code is not shown here. If you want to view the complete code, you can visit Github code warehouse hamburg_store_v3.

Hamburgers in different regions produce almost the same kind of hamburger. I'm deliberately trying to prevent customers from noticing that the hamburger they eat is a regional special edition, but we can check the real hamburger type by calling print(repr(hamburg)).

We already know what the whole system looks like without using design pattern. Now let's see how factory pattern can improve the design.

Factory method

Let's first look at the factory method. The factory method is very similar to the template mode, that is, reserve an "interface method" in the base class, define it as an abstract method, and then implement it by the base class. This is also the most common use of abstract methods and inheritance:

import abc


class BaseClass(abc.ABC):
    def mainFunction(self):
        self.__before()
        self.doSomething()
        self.__after()

    def __before(self):
        pass

    def __after(self):
        pass

    @abc.abstractmethod
    def doSomething(self):
        pass

In the above example, the abstract method doSomething plays the role of template method__ before and__ after methods are defined by the base class, and subclasses only need to override the doSomething method. This method is usually applied to the web framework. In the web framework, the base class such as Page will process the parameters of http request and the returned message header, and the subclass can rewrite the corresponding template method to add specific processing logic.

Now let's look at the factory method, which is very similar to the structure of the template method, except that the factory method is not reserved for some logic of the subclass solid line, but expects the subclass to create an object:

The core concept here is that the base class Creator needs to obtain an abstract Product object for business processing. However, in order to avoid tight coupling between the Creator and the specific Product subtype due to the direct introduction of a specific Product object, an abstract method factoryMethod() is created to "throw the pot" to the subclass, Let subclasses do it, which is the so-called "factory method".

In this way, the Creator is decoupled from the specific Product subclass. It only needs to focus on the abstract type Product.

Dependency Inversion Principle

This design pattern embodies such a design principle:

  • Dependency Inversion Principle: that is, under normal circumstances, high-level components depend on low-level components. Here, high-level and low-level refer to the use level at the system architecture level. For example, the Creator above needs to hold an object of Product type for business processing, and the Creator is a high-level component relative to Product. Under normal circumstances, Creator needs to directly establish contact with specific Product1/Product2, etc., but if we establish an abstract layer Product, let Creator only rely on Product, and Product1/Product2 also inherit from Product, so as to decouple high-level components from low-level components. They do not directly establish contact, but all rely on an abstraction.

There is a trick to applying this principle, that is, in the client program (referring to Creator in the above UML diagram), the reference of abstract class (referring to Product) should be used instead of specific type. This is a bit like a design principle we said before: programming for interfaces, not implementation. However, the emphasis of the two is different. The former is for reference objects, while the latter is for method calls, but their essence is similar. They both use higher-level abstract concepts to replace specific types through the way of "interface", so as to realize an elastic design.

Factory method practice

Let's look at how to use the factory method in the hamburger system:

It seems that it has not changed much, but in fact, the orderHamburger() method of each HamburgStore subclass should implement the following steps:

  1. Get a suitable Hamburg object.
  2. Call ready()
  3. Call cook()
  4. Call box()

Now, the logic of the subclass orderHamburger() is completely inherited from the abstract base class, and the subclass only needs to implement the factory method createhamburg.

Only part of the core code changed relative to v3 version is shown here. See Github warehouse for the complete code hamburg_store_v4:

#######################################################
#
# HamburgStore.py
# Python implementation of the Class HamburgStore
# Generated by Enterprise Architect
# Created on:      19-6��-2021 18:39:45
# Original author: 70748
#
#######################################################
from .Hamburg import Hamburg
import abc


class HamburgStore(abc.ABC):
    def orderHamburg(self, type: str) -> Hamburg:
        hamburg: Hamburg = self.creatHamburg(type)
        if hamburg is not None:
            hamburg.ready()
            hamburg.cook()
            hamburg.box()
        return hamburg

    @abc.abstractmethod
    def creatHamburg(self, type: str) -> Hamburg:
        pass

######################################################### JiangSuHamburgStore.py# Python implementation of the Class JiangSuHamburgStore# Generated by Enterprise Architect# Created on:      19-6��-2021 18:39:45# Original author: 70748########################################################from .Hamburg import Hamburgfrom .HamburgStore import HamburgStorefrom .NotHotMiniBurger import NotHotMiniBurgerfrom .NotHotNewOrleansRoastedBurger import NotHotNewOrleansRoastedBurgerfrom .NotHotZingerBurger import NotHotZingerBurgerclass JiangSuHamburgStore(HamburgStore):    def creatHamburg(self, type: str) -> Hamburg:        hamburg: Hamburg = None        if type == "zinger":            hamburg = NotHotZingerBurger()        elif type == "new_orliean":            hamburg = NotHotNewOrleansRoastedBurger()        elif type == "mini":            hamburg = NotHotMiniBurger()        else:            pass        return hamburg

Now there are fewer modifications to open a new store. There is no need to repeat the standard procedures of preparing materials, cooking and packaging. Our new store only needs to focus on the matching menu (realizing createhamburg()). 🐶

Abstract factory

Although the plan of our hamburger restaurant is going well, suppose we have problems with the raw material control of our branch, we need to integrate the raw material distribution into the whole system, and the raw materials of each branch are different because of the earth. For example, the chicken in Beijing Branch is white feather chicken, and the chicken in Sichuan branch is Sanhuang chicken, Hunan branch uses black chicken (don't ask me if I can make hamburger with black chicken) 😈).

Should we do this now?

Here we will introduce our abstract factory. This design pattern is to create a series of factory classes to realize the difference creation of a certain kind of products.

Let's take a look at the UML diagram after introducing the abstract factory into the hamburger store:

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-cho0fxse-1624160401304)( http://image.icexmoon.xyz/image-20210619212759280.png )]

Only the newly added parts are shown here, because the class diagram is too complex to be displayed completely. If you want to see the complete class diagram, you can check the class diagram of Github warehouse Hamburger eapx , this is the project file of EA, which needs to be opened with EA.

Similarly, only the key code is shown here, and the complete code is shown in the Github warehouse hamburg_store_v5:

######################################################### IngredientsFactory.py# Python implementation of the Class IngredientsFactory# Generated by Enterprise Architect# Created on:      19-6��-2021 21:34:31# Original author: 70748########################################################import abcfrom .Chicken import Chickenfrom .Pepper import Pepperclass IngredientsFactory(abc.ABC):    @abc.abstractmethod    def getChicken(self) -> Chicken:        pass    @abc.abstractmethod    def getPepper(self) -> Pepper:        pass
######################################################### BeijingIngredientFactory.py# Python implementation of the Class BeijingIngredientFactory# Generated by Enterprise Architect# Created on:      19-6��-2021 21:34:31# Original author: 70748########################################################from .IngredientsFactory import IngredientsFactoryfrom .BeijingPepper import BeijingPepperfrom .WhiteFeatherChicken import WhiteFeatherChickenfrom .Chicken import Chickenfrom .Pepper import Pepperclass BeijingIngredientFactory(IngredientsFactory):    def getChicken(self) -> Chicken:        return WhiteFeatherChicken()    def getPepper(self) -> Pepper:        return BeijingPepper()
######################################################### MiniBurger.py# Python implementation of the Class MiniBurger# Generated by Enterprise Architect# Created on:      19-6��-2021 18:39:45# Original author: 70748########################################################from .Chicken import Chickenfrom .Hamburg import Hamburgfrom .IngredientsFactory import IngredientsFactoryfrom .Pepper import Pepperclass MiniBurger(Hamburg):    def __init__(self, ingredientFactory: IngredientsFactory) -> None:        super().__init__("Mini Hamburger", ingredientFactory)    def box(self):        print("box {}".format(self.name))    def cook(self):        print("cook {}".format(self.name))        print("use {}".format(self.chicken.__class__.__name__))    def ready(self):        print("Prepare ingredients")        chicken: Chicken = self.ingredientFactory.getChicken()        self.chicken: Chicken = chicken        self.pepper: Pepper = self.ingredientFactory.getPepper()        print("prepare {}".format(chicken.__class__.__name__))        print("prepare {}".format(self.pepper.__class__.__name__))
from src.SichuanHamburgStore import SichuanHamburgStorefrom src.BeijingHamburgStore import BeijingHamburgStorefrom src.JiangSuHamburgStore import JiangSuHamburgStorebeijingStore = BeijingHamburgStore()sichuanStore = SichuanHamburgStore()jiangsuStore = JiangSuHamburgStore()berger1 = beijingStore.orderHamburg("mini")print(repr(berger1))berger2 = sichuanStore.orderHamburg("mini")print(repr(berger2))berger3 = jiangsuStore.orderHamburg("mini")print(repr(berger3))# Prepare ingredients# prepare WhiteFeatherChicken# prepare BeijingPepper# cook Mini Hamburger# use WhiteFeatherChicken# box Mini Hamburger# MiniBurger# Prepare ingredients# prepare ThreeYellowChicken# prepare SichuanPepper# cook Mini Hambruger# use ThreeYellowChicken# use SichuanPepper# box Mini Hambruger# VeryHotMiniBurger# Prepare ingredients# prepare BlackChicken# prepare JiangsuPepper# cook Mini Hamburger# use BlackChicken# use JiangsuPepper# box Mini Hamburger# NotHotMiniBurger

It can be seen from the test results that the stores in each region "automatically" use the special ingredients distributed by the regional factories, such as three yellow chicken and Sichuan pepper used by the stores in Sichuan.

I'm lazy here. I only modified the classes related to XXXMiniBurger, but I didn't modify other types of burgers. In fact, as like as two peas for Hamburger subclasses, the subclass can be added to the base class, but that is not the key to the abstract factory pattern. It is also not helpful to explain the key points of the abstract factory, so I have not improved it, and interested friends can try it on their own.

Well, we have introduced the main points of abstract factory and factory method. Finally, we summarize the advantages, disadvantages and differences between them.

Factory method or abstract factory

Both design patterns encapsulate the creation of objects to achieve flexible design, but there are differences in details:

  • Purpose: the purpose of factory method is to postpone the creation of objects to subclasses by reserving factory methods in the base class. The base class only needs to manage an abstract concept, while the abstract factory is to create a class of objects with differences by inheriting an abstract factory and creating multiple subclass factories, such as localized vegetables, fruits, meat, etc. They create a class of objects, not a single object.

  • Usage: factory methods are used by inheriting and overriding abstract methods, while Abstract factories are used by combining references of an abstract method.

    The "use" here refers to the client program.

  • Decoupling: the factory method is to understand and couple the base class with the creation of a specific single type, while the abstract factory is to understand and couple the client program with the creation of a group of specific types.

  • Disadvantages: using the factory method, it is inevitable to determine what specific type will be created by creating an exact subclass in the final implementation. The abstract factory faces the problem that if you want to add a new product, you need to modify the base class factory and all subclasses, which is also unavoidable.

Well, that's all for the introduction of factory mode. Thank you for reading.

All the code and engineering files for this series of articles are saved in the Github project design-pattern-with-python.

Topics: Python Design Pattern