设计模式 with Python 4:工厂模式
工厂模式实质上包含两种不同的设计模式:工厂方法和抽象工厂。但他们本质上都是对创建对象进行封装,从而实现某种程度上的解耦的设计模式,所以放在一起进行介绍。
和《Head First 设计模式》不同,这里采用汉堡店的例子进行说明,因为我没吃过几次披萨,对披萨的种类知之甚少,所以就不用披萨店作为例子,但本质上两者除了名称没有根本上的区别。
汉堡店
假设我们要开一家汉堡店,并提供多种汉堡可供顾客点餐,最初的设计可能是这样的:
HamburgStore
是我们的汉堡店,通过orderHamburg
方法可以点餐,具体的汉堡类有:
- ZingerBurger:香辣鸡腿堡
- MiniBurger:田园鸡腿堡
- NewOrleansRoastedBurger:新奥尔良鸡腿堡
- 具体的汉堡英文我参考了网上找到的一个肯德基外卖菜单,感兴趣的可以自行查看完整菜单。
- 原来田园鸡腿堡叫Mini…
- 我最喜欢的是新奥尔良鸡腿堡🙂
不管是何种类型的汉堡,都支持三个基本操作:ready、cook、box
,分别对应准备食材、烹饪、包装三道工序。
所以很自然的,我们可以想到建立一个抽象类Hamburg
(也可以是接口)来作为基类,进行类型约束和定义一些通用的操作。相应的三道工序我们可以定义为抽象方法。
此外,为了说明依赖关系,这里用虚线箭头进行描述,目前我们没有做更多的额外抽象,所有的具体的根据给定类型type
来创建对应的具体汉堡的操作都是在HamburgStore
的orderHamburg
方法中实现的,所以这个方法依赖于三个具体的汉堡类,它们之间用表示依赖的虚线箭头进行连接。
下面展示关键代码,完整代码见Github项目中的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:
"""点单一个汉堡
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: 汉堡名称
"""
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))
测试代码如下:
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
就像UML关系图中展示的那样,HamburgStore
的orderHamburg
方法对于三个具体的汉堡类是直接依赖,是紧耦合的。所以任意具体汉堡类的修改,或者增加或者删除一种汉堡,都需要修改orderHamburg
中的代码,这显然是违反我们在上一篇设计模式中提到的“开放闭合原则”。
如果你不清楚什么是开放闭合原则,请阅读设计模式 with Python3:装饰器模式。
我们可以使用一种极为简单的方式进行改进。
简单工厂
简单工厂真的很简单,它都不是一种设计模式,而更像是一种编写代码的习惯。
我们直接看使用简单工厂之后的UML:
我们创建了一个新的类SimpleFactory
,将根据给定类型type
构建具体汉堡的工作都交给SimpleFactory
的静态方法getHamburg()
,这样做了之后HamburgStore
的orderHamburg()
方法就不会再依赖于具体的汉堡类,它只依赖于SimpleFactory
和Hamburg
,这样做会让HamburgStore
中的代码简洁一些,但其实质并没有改变,只不过我们后续对汉堡的调整造成的代码修改都变成了对SimpleFactory
的getHamburg()
方法而已。这基本上就是将这部分代码封装到一个其他的方法中了事的方案,所以称之为“简单”工厂一点也不为过。
同样的,这里只展示相对于v1版本修改的部分关键代码,完整代码见Github仓库的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:
"""点单一个汉堡
type: 汉堡类型
"""
hamburg: Hamburg = SimpleFactory.getHamburg(type)
if hamburg != None:
hamburg.ready()
hamburg.cook()
hamburg.box()
return hamburg
再次强调:简单工厂是一种编程习惯,而非设计模式。
如果我们的需求一直不变,或者维持很简单的改变,比如说添加或者删除一种汉堡之类的,我们上边的设计已经做的很不错了,也无需再使用更复杂的设计模式进行改动。
要知道所有的设计模式都是为了应对更复杂的、频繁改变的需求,我们不能为了使用设计模式而使用设计模式,那样做反而是舍本逐末。如果你目前的解决方案可以应对需求的改变,而且运行的不错,那为什么要改变呢。
据说编程这个行当有这么一句俗语:**如果目前的程序没有问题,那就不要试图改变它。**当然,我个人觉得这更多是一种调侃,适当的重构是相当有必要的,否则项目会变成“屎山”。
所以为了引入我们后面要介绍的两种设计模式,我们这里假设我们现在赚了大钱,一家汉堡店已经不能满足我们了,我们需要扩张,需要开更多的汉堡店。
但相应的,这也伴随着更多原来我们不会考虑的问题,比如说四川的汉堡店不管什么菜品,辣椒必须翻倍。湖南的汉堡店辣椒直接三倍。而江苏的汉堡店则辣椒减半。
我们先来看一下如果不使用设计模式,我们的代码会是什么样的。
No 设计模式
从UML图可以看到,我们创建了一个汉堡店抽象基类HamburgStore
,并将原来的汉堡店升级为总店BeijingHamburgStore
,并且新建了两个汉堡店SichuanHamburgStore
和JiangsuHamburgStore
,并根据地域特点微调了菜品,江苏的店辣椒减半,对应的具体汉堡类就是NotHotXXXBurger
,而四川的汉堡店辣椒加倍,对应的汉堡类就是VeryHotXXXBurger
。
可以看到每个具体的汉堡店也依赖于具体的某些地方特色汉堡。当然,我们可以使上边说过的简单工厂进行解耦,添加几个简单工厂,或者在一个简单工厂中添加若干静态方法,将具体的汉堡店与具体的汉堡进行解耦。但我们之前说过了,这基本上是一种治标不治本的方式,如果后续接着开店,需要创建更多的地方特色菜品,我们就要频繁修改相应的简单工厂中的代码。
这个设计的代码可以实质上和v1版本的汉堡店代码是相似的,只不过包含的类数量庞大了很多,所以这里就不展示代码了,想查看完整代码可以访问Github代码仓库hamburg_store_v3。
不同地区的汉堡店制作同一种汉堡的输出几乎一样,我是故意的,是为了不让顾客察觉他们吃的汉堡是地区特供版,但是我们是可以通过调用
print(repr(hamburg))
查看真实的汉堡类型。
我们已经知道了不使用设计模式的情况下整个系统是什么样的,现在我们看下工厂模式如何改进设计。
工厂方法
我们先看工厂方法,工厂方法和模版模式很类似,就是那种在基类预留一个“接口方法”,将其定义为抽象方法,然后由基类去进行具体实现,这也是抽象方法和继承最常见的用途:
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
在上面这个例子中,doSomething
这个抽象方法就扮演着模版方法的角色,__before
和__after
方法都由基类定义,而子类只要覆盖doSomething
方法即可。这种方式通常会应用在web框架等情况,在web框架中,Page
之类的基类会处理http请求的参数,和返回的报文头等,而子类可以重写相应的模版方法添加具体的处理逻辑。
我们现在来看工厂方法,这和模版方法的结构极为类似,只不过工厂方法不是预留给子类实线某些逻辑用的,而是期望子类能创建一个对象:
这里的核心概念在于基类Creator
需要获取到一个抽象的Product
对象来进行业务处理,但为了避免因为直接引入某个具体的Product
对象而导致Creator
和具体的Product
子类型紧耦合,所以这里采用创建一个抽象方法factoryMethod()
,将获取具体Product
对象的工作“甩锅”给子类,让子类来完成,这就是所谓的“工厂方法”。
通过这样的方式,Creator
和具体的Product
子类是解耦的,它只需要关注抽象类型Product
即可。
依赖倒置原则
这个设计模式体现的是这样一条设计原则:
- 依赖倒置原则:即正常情况下高层组件依赖于底层组件,这里的高层和底层是指在系统架构层面用途上的高低,比如上面的
Creator
,需要持有一个Product
类型的对象来进行业务处理,那Creator
相对Product
就是一个高层组件。在正常情况下Creator
需要直接和具体的Product1
/Product2
等建立联系,但是如果我们建立一个抽象层Product
,让Creator
只依赖于Product
,Product1
/Product2
也继承自Product
,这样就让高层组件和底层组件解耦,他们不直接建立联系,而是都依赖于一个抽象。
应用这种原则有一个窍门,即在客户端程序(在上面的UML图中指Creator
)中应当使用抽象类的引用(指Product
),而非具体的类型。这有点像我们之前说的一条设计原则:针对接口编程,而非实现。但两者的侧重点不同,前者是针对引用对象而言,后者则是对方法调用而言,但其本质相似,都是通过“接口”的方式用更高层次的抽象概念来代替具体类型,从而实现一种弹性设计。
工厂方法实践
我们来看如何在汉堡店这个系统中使用工厂方法:
看起来似乎改变不大,但事实上原本每个HamburgStore
子类的orderHamburger()
方法都要实现这样的步骤:
- 获取一个合适的
Hamburg
对象。 - 调用
ready()
- 调用
cook()
- 调用
box()
而现在子类的orderHamburger()
的逻辑完全继承自抽象基类,而子类只需要实现工厂方法creatHamburg
即可。
这里只展示相对于v3版本改动的部分核心代码,完整代码见Github仓库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
现在再开新店需要做的修改就少了很多,不需要再重复那些准备材料、烹饪、打包的标准程序了,我们的新店只需要专注于搭配菜单(实现creatHamburg()
)即可。🐶
抽象工厂
虽然我们的汉堡店计划进行的很顺利,但是假设我们因为分店的原材料品控出了问题,我们需要将原材料配送也纳入整个系统,而且每个分店的原材料还因为地球有差别,比如说北京分店的鸡肉用的是白羽鸡,四川分店用的是三黄鸡,湖南分店用的是乌鸡(不要问我汉堡能不能用乌鸡做 😈)。
现在应该这么处理?
这里就要引入我们的抽象工厂了,这个设计模式是通过创建一系列工厂类来实现对某一类产品的差异性创建。
我们看一下将抽象工厂引入汉堡店以后的UML图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cho0FXSe-1624160401304)(http://image.icexmoon.xyz/image-20210619212759280.png)]
这里只展示新增的部分,因为类图实在太过庞杂,无法完整显示,如果想看完整类图,可以查看Github仓库的汉堡店.eapx,这是EA的工程文件,需要用EA打开。
同样的,这里只展示关键代码,完整代码见Github仓库的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
从测试结果可以看出,各个区域店铺“自动”地使用了区域工厂配送的特殊食材,比如四川店铺就使用的三黄鸡(ThreeYellowChicken
)和四川辣椒(SichuanPepper
)。
我这里偷懒了,只修改了
XXXMiniBurger
相关的类,其它类型的汉堡没做修改。事实上给Hamburger
子类做的修改几乎一模一样,完全可以对在基类中添加,让子类继承,但是那并不是抽象工厂模式的关键所在,对说明抽象工厂的关键要点也没有帮助,所以我没有那样改进,感兴趣的朋友可以自行尝试。
好了,关于抽象工厂和工厂方法的要点我们已经介绍完了,最后总结一下这两者的优缺点和区别。
工厂方法 or 抽象工厂
这两种设计模式都是对对象创建进行封装,以实现弹性设计,但在细节上有所区别:
-
目的:工厂方法的目的是通过在基类预留工厂方法的方式,让对象创建推迟到子类,基类只需要管理一个抽象概念即可,而抽象工厂则是通过继承一个抽象工厂,创建多个子类工厂来分别创建具有差异性的一类对象,比如本地化的蔬菜、水果、肉类等等。它们创建的是一类对象,而非单一对象。
-
使用方式:工厂方法通过继承并重写抽象方法使用,而抽象工厂则通过组合一个抽象方法的引用进行使用。
这里的“使用”,主体指客户端程序。
-
解耦:工厂方法是将基类与具体的单个类型创建进行了解耦,而抽象工厂则是将客户端程序与一组具体的类型创建进行了解耦。
-
缺点:使用工厂方法,在最终实现中必然需要通过创建确切的子类来确定会创建何种具体类型,这是无法避免的。而抽象工厂则面临着如果要添加一种新的产品,则需要修改基类工厂和所有的子类,这同样无法避免。
好了,关于工厂模式的介绍就到这里,谢谢阅读。
本系列文章的所有代码和工程文件都保存在Github项目design-pattern-with-python。