文章目录
- 建造者模式中的简单版本
- 逐渐复杂的问题
- 建造者模式的实现
- 建造者模式中的经典版本
建造者(builder)模式属于创建型模式,建造者模式一般有两种类型的应用
建造者模式中的简单版本
逐渐复杂的问题
假设现在需要创建一个用户对象,那么你会这样实现代码
class User:
def __init__(self, name):
self.name = name
user = User("jack")
后来你需要为这个用户对象引入其他的属性:生日、住址、身高、体重、兴趣,不同的用户在创建时不愿意提供其中一些信息,于是你实现了下面的版本:
class User:
def __init__(self, name, age=None,birth_date=None,address=None,heigh=None,weight=None,hobby=None):
self.name = name
self.age = age
self.birth_date = birth_date
self.address = address
self.heigh = heigh
self.weight = weight
self.hobby = hobby
user = User("John", heigh=165,weight=58,hobby="reading")
可以看到随着对象的属性越来越多,我们的构造函数的参数列表越来越长。这里由于使用了关键字参数的关系,所以问题并不明显,假如使用位置参数,那么使用者在使用时就必须记住参数的位置,甚至对于没有的属性需要放置None
值:
def __init__(self, name, age,birthday,address,heigh,weight,hobby):
...
User("Lucy",17,None,None,68,None,None)
当然这个问题也不是不能接受,让我们继续增加问题的复杂度,假如现在我们还需要对一些属性进行校验或者转化处理,那么你可能会这么修改代码:
class User:
def __init__(self, name, age=None,birth_date=None,address=None,heigh=None,weight=None,hobby=None):
self.name = name
self.age = age if isinstance(age,int) and age > 0 else None
self.birth_date = birth_date
self.address = address
self.heigh = heigh if isinstance(heigh, int) else None
self.weight = weight if isinstance(weight, int) else None
self.hobby = self.set_hobby(hobby)
def set_hobby(self, hobby):
if isinstance(hobby, list):
hobby = ",".join(hobby)
return hobby
user = User("John", heigh=165,weight=58,hobby=["reading","running"])
在这个版本中,构造参数依然很多,但set_hobby
的出现,让我们开始思考属性的添加为什么不采用调用实例函数的方式设置?于是我们修改了整个 User
的实现
class User:
def __init__(self, name):
self.name = name
self.label = None
def get_user_info(self):
return self.__dict__
def add_age(self,age:int):
if age < 0:
raise ValueError("Age should be > 0!!!")
self.age = age
def add_birthday(self,birthday):
self.birthday = birthday
def add_address(self,address:str):
self.address = address
def add_height(self,height:int):
self.height = height
def add_weight(self,weight:int):
self.weight = weight
def add_hobby(self, hobby):
if isinstance(hobby, list):
hobby = ",".join(hobby)
self.hobby = hobby
def main():
user = User("John")
user.add_height(165)
user.add_weight(58)
user.add_hobby(["reading","running"])
print(user.get_user_info())
main()
如果没有再继续添加需求和限制的话,这个版本已经解决了问题。为了让你明白我们为啥要用建造者模式并引入一个 simple builder version
,我们增加多几个限制:
- 用户实例需要是一次生成的,它是不可变对象,比如下面这种场景:
- 如果我们根据这个用户所有的属性来生成一个值,定义它的
__hash__
方法,由于属性值的持续变化就会 hash 造成前后不一致
- 如果我们根据这个用户所有的属性来生成一个值,定义它的
- 我们不希望用户实例的所有属性在还没完全确定下来之前就被客户使用,这会造成错误。比如下面这种场景:
- 如果这是一个汽车实例,我们不能在还没有完全制造好之前就交付给客户使用
- 回到这个案例,假设客户端代码这样写:
def main():
user = User("John")
user.add_height(165)
user.add_weight(58)
if user.hobby is None:
print(f"{user.name} is a man without any hobby!")
user.label = "boring man!"
user.add_hobby(["reading","running"])
print(user.get_user_info())
main()
显然我们会得到
AttributeError: 'User' object has no attribute 'hobby'
在 user
还没完全定义完全的时候,我们对 user
做其他操作时发现并没有个 hobby
这个属性(即使我们可以用属性 getattr
判断来规避这个异常,我们也错误的给 user 贴了一个 boring 的标签,而实际上他并不是)
在上面这个问题中,我们发现了3个限制和需求:
- 繁琐复杂的构造参数,存在不同的组合情况
- 传入参数时需要校验或者转化
- 实例在完全生成之前不允许交付给第三方使用
建造者模式的实现
聊到这里,我们就要引入建造者模式来解决问题了。先直接看建造者模式的实现:
class User:
def __init__(self,name,age,birthday,address,weight,height,hobby):
self.name = name
self.age = age
self.birthday = birthday
self.address = address
self.weight = weight
self.height = height
self.hobby = hobby
self.label = None
def get_user_info(self):
return self.__dict__
class Builder:
def __init__(self):
self.name = None
self.age = None
self.birthday = None
self.weight = None
self.height = None
self.address = None
self.hobby = None
def get_user(self):
return User(
name=self.name,
age=self.age,
birthday=self.birthday,
address=self.address,
hobby=self.hobby,
weight=self.weight,
height=self.height
)
def set_name(self, name):
self.name = name
return self
def set_age(self,age:int):
if age < 0:
raise ValueError("Age should be > 0!!!")
self.age = age
return self
def set_birthday(self,birthday):
self.birthday = birthday
return self
def set_address(self,address:str):
self.address = address
return self
def set_height(self,height:int):
self.height = height
return self
def set_weight(self,weight:int):
self.weight = weight
return self
def set_hobby(self, hobby):
if isinstance(hobby, list):
hobby = ",".join(hobby)
self.hobby = hobby
return self
def main():
builder = Builder()
builder.set_name("John").set_height(165).set_weight(58)
builder.set_hobby(["reading","running"])
user = builder.get_user()
print(user.get_user_info())
main()
在这个实现中,我引入了 builder
来完成对象 User 的创建,它隐藏了 User
对象构造函数中复杂的参数。你可能会觉得这个实现看起来更复杂了,确实是这样,这是这种设计模式在简单对象上的缺点,但如果现在这个对象的属性足够复杂,并且需要根据不同的需求生成不同风格的 user
及对应的响应步骤呢?
总结下这个实现的调用逻辑:
到此为止,你已经接触了建造者模式的初级版本!
建造者模式中的经典版本
前面我花了很大的篇幅去引入建造者模式的简单版本,因为我想让你明白为什么不用更常规且直观的方式。
建造者模式通常由下面的组件构成:
- 产品(Product):要构建的复杂对象。产品类通常包含多个部分或属性。
- 抽象建造者(Builder):定义了构建产品的抽象接口,包括构建产品的各个部分的方法。
- 具体建造者(Concrete Builder):实现抽象建造者接口,具体确定如何构建产品的各个部分,并负责返回最终构建的产品。
- 指导者(Director):负责调用建造者的方法来构建产品,指导者并不了解具体的构建过程,只关心产品的构建顺序和方式。
现在我们来设计一个案例以便介绍经典模式下的建造者模式实现
有一个公司的业务是帮别人开公司,而开公司的流程一般是:
1. 注册公司
2. 注册商标
3. 制定公司规范和文化
4. 招人
业务进行的过程中有一些不同的地方:
1. 成立海外公司和中国公司的注册地不一样
2. 公司文化也不一样
4. 用户会根据他的需求添加一些部门(法务部、人事部、行政部、研发部、销售部)
而建造者模式可以做到将 Company
对象的构造过程划分为一组步骤, 比如先注册公司再申请 logo等等。 每次创建对象时,你都需要通过生成器对象执行一些步骤。 重点在于你无需调用所有步骤, 而只需调用创建特定对象配置所需的那些步骤即可,比如给公司起名字这个步骤无法省略。
当你需要创建不同形式的产品时, 其中的一些构造步骤可能需要不同的实现。 例如, 国内公司的注册流程和海外公司的注册流程差距很大,logo 的设计风格也不一样。
在这种情况下, 你可以创建多个不同的 builder
, 用不同方式实现一组相同的创建步骤。 然后你就可以在创建过程中使用这些 builder
(例如按顺序调用多个构造步骤) 来生成不同类型的对象。
在这个指导原则下,应用这个模式的类关系图:
代码实现
from abc import ABC, abstractmethod
class Company:
def __init__(self, logo, company_name,company_register_address,document, department_list):
self.logo = logo
self.company_name = company_name
self.company_register_address = company_register_address
self.document = document
self.department_list = department_list
def __str__(self):
s = ""
for k, v in self.__dict__.items():
if k == "department_list":
s += "department_list:\n"
for i in v:
s += f"{i}\n"
else:
s += f"{k}:{v}\n"
return s
class Builder(ABC):
def __init__(self):
self.company_name = None
self.logo = None
self.department_list = []
def register_company(self):
raise NotImplementedError
def register_logo(self):
raise NotImplementedError
def define_document(self):
raise NotImplementedError
def set_department(self, department_name,people_counts):
self.department_list.append((department_name, people_counts))
return self
def set_company_name(self,name):
self.company_name = name
return self
def get_company(self):
self.register_company()
self.register_logo()
self.define_document()
return Company(
logo=self.logo,
company_name=self.company_name,
company_register_address=self.company_register_address,
document=self.document,
department_list=self.department_list
)
class AboardCompanyBuilder(Builder):
def register_logo(self):
self.logo = "aboard logo"
print("Do a aboard style logo")
print("-"*10)
def register_company(self):
# do some specify step
print("-"*10)
print("注册流程:")
print("前往海外申请")
print("校验是否由海外法人")
print("公示一个月")
print("-"*10)
self.company_register_address = "aboard"
def define_document(self):
self.document = "aboard culture company rules"
class LocalCompanyBuilder(Builder):
def register_logo(self):
self.logo = "local logo"
print("Do a local style logo")
print("-"*10)
def register_company(self):
# do some specify step
print("-"*10)
print("注册流程:")
print("在国内申请")
print("需要公示一周")
print("-"*10)
self.company_register_address = "local"
def define_document(self):
self.document = "aboard culture company rules"
def set_department(self, department_name, people_counts):
if people_counts > 5:
# 将人数 +2
self.department_list.append((department_name, people_counts+2))
else:
self.department_list.append((department_name, people_counts))
return self
def main():
builder = LocalCompanyBuilder()
builder.set_company_name("Big Company")
builder.set_department("人事部",6).set_department("研发部",2)
company = builder.get_company()
print(company)
main()
现在我们对比下两种 Builder
的输出:
----------
注册流程:
在国内申请
需要公示一周
----------
Do a local style logo
----------
logo:local logo
company_name:Big Company
company_register_address:local
document:aboard culture company rules
department_list:
('人事部', 8)
('研发部', 2)
----------
注册流程:
前往海外申请
校验是否由海外法人
公示一个月
----------
Do a aboard style logo
----------
logo:aboard logo
company_name:Big Company
company_register_address:aboard
document:aboard culture company rules
department_list:
('人事部', 6)
('研发部', 2)
通过这种方式,我们向 client
隐藏了具体 Company
的生成步骤(注册公司、logo、文化文档),而 client
只需关注他需要创建的公司是哪种类型的(海外公司还是国内公司,不同类型的公司它的行为和属性是不同的),并且他可以任意的添加他想要的部分(部门,当然这里将这个实现简略为往列表中添加元组,而在其他例子中,可能会用菜品或者零件来替代)。