在IT这个行业,技术日新月异,可能你今年刚弄懂一个编程框架,明年它就不流行了。 然而即使在易变的IT世界也有很多几乎不变的知识,他们晦涩而重要,默默的将程序员划分为卓越与平庸两类。比如说,基础算法,比如说,设计模式。
设计模式适用于前端、客户端、后台等等
设计模式的定义:
在软件工程领域,设计模式是一套通用的可复用的解决方案,用来解决在软件设计过程中产生的通用问题。它不是一个可以直接转换成源代码的设计,只是一套在软件系统设计过程中程序员应该遵循的最佳实践准则。
In software engineering, a software design pattern is a general, reusable solution to a commonly occurring problem within a given context in software design. It is not a finished design that can be transformed directly into source or machine code. It is a description or template for how to solve a problem that can be used in many different situations. Design patterns are formalized best practices that the programmer can use to solve common problems when designing an application or system.
概要
设计模式就是面向对象设计中常见问题的经典解决方案,同样的解决方案在各种项目中得到了反复使用, 最终有人给它们起了名字, 并对其进行了详细描述,这就是设计模式的起源。
具体
模式(Pattern)的概念是由建筑领域的书籍《建筑模式语言》中首次提出的,书中介绍了城市设计的模式。随后,软件工程领域的四位作者埃里希·伽玛、 约翰·弗利赛德斯、 拉尔夫·约翰逊和理查德· 赫尔姆
接受了模式的概念。
1994 年, 他们出版了《设计模式:可复用面向对象软件的基础》一书,将设计模式的概念应用到程序开发的领域中。 该书提出了 23 个模式来解决面向对象程序设计中的各种问题, 很快便成为了畅销书。 由于书名太长,人们将其简称为“四人组(Gang of Four,GoF)的书”,并且很快进一步简化为“GoF 的书”。
书里以设计一个文档编辑器为例子来介绍设计模式的实际应用,
其中,每个字符、空格、图片等元素都是一个对象,文档编辑器需要支持一系列功能,比如
优点是不仅能学习到设计模式,而且还能了解文档编辑器大致的设计思路和实现方式。
其实,很多工程师已从事程序开发工作多年, 却完全不知道设计模式是什么。即便如此,他们可能也在不自知的情况下已经使用过一些设计模式来提升代码质量了,所以为什么不花些时间来更系统性地学习它们呢?
学习设计模式的好处主要可以分为两点:
我学习设计模式的一些所想所得
关于设计模式有一句评语,“没有写过10万行代码,不要谈设计模式”,可能有点夸张,但道理是正确的。
刚开始的时候,很多年轻的工程师往往不能够领会到设计模式的存在意义。在他们看来,“有变化,改代码就行了呀。怎么改都是改,为什么就一定要像你(设计模式)说的那样改呢?”。
后来,直到他们深入到那些足够复杂足够混乱的代码,身心饱受摧残,才会对设计模式的认识有质的提高。因为设计模式的一个重要应用场景,是应付那些复杂的业务逻辑和快速的需求变化,它的价值在这些情况下,才能够清晰的体现出来。
团队合作:工程师单兵作战,代码是自己一个人从头写到脚。哪里有问题我就可以改哪里,完全没有心理负担。
需求变更:代码足够简单,有需求变更,直接加上一个if…else分支就完事了。
总的来说,越复杂的业务场景和代码逻辑,设计模式就越能彰显出自己的存在意义。
设计模式一直饱受争议,有很多人对设计模式推崇备至,但也有很多人认为设计模式把简单问题复杂化,没有必要。之所以会有这样的争议,主要是因为存在有不当使用设计模式的问题。
一些设计模式的初学者经常会有这样的问题: 在学习了某个模式后, 他们就将其认为是某种教条, 然后 “全心全意” 地实施这样的模式, 而不会根据项目的实际情况对其进行调整, 即便是在较为简单的代码也能胜任的地方也是如此。
工具定律:如果你有的只是一把铁锤, 那么所有东西看上去都像是一个钉子。(含义为对一个熟悉的工具过度的依赖)
实际上,设计模式也讲究使用场景,
假如你在网上让别人推荐一本设计模式的书,技术大佬大概率会推荐《设计模式:可复用面向对象软件的基础》。这本书固然很经典,但缺点就是太枯燥乏味,因为这本书也属于是计算机黑皮系列!
而且不知道是不是翻译的问题,经典的教科书味道,看的非常痛苦,总共261页,我看了70多页,看不下去了,太耗费时间了。
那些上千页的计算机书,用什么姿势看?
不少小伙伴在想要学习计算机基础类知识的时候,就买了这类黑皮书,书到货后,我们满怀信心,举着厚重的黑皮书,下决心要把这些黑皮书一页一页地攻读下来,结果不过几天就被劝退了,然后就只有前几页是有翻阅的痕迹,剩下的几百页都完全是新的,最终这些厚厚的黑皮书就成了垫显示器的神器。
其实在自学过程中,最容易踩坑的地方就是不看自己当前水平,盲目跟风买那些豆瓣高分的大而全的计算机黑皮系列的书,然后学几天,就放弃了。这些大而全的计算机黑皮书当然很经典,但是它们并不适合新人入门学习,因为这类书籍的内容都充满大量的专业术语,我们人在看到陌生又难以理解的词汇时,就会感觉很吃力,脑子看着就会很累。
学习一门学科的时候,应该先从最基础的书开始学起,快速入门,然后再渐渐步入到这些大而全的计算机黑皮书。
比如说有一个讲解设计模式的网站 Refactoring.Guru,优点如下:
可以通过这种简易的教程快速上手设计模式,后面有需要深入了解某一种设计模式的时候再查阅经典黑皮书。
这个网站的缺点是,列举的例子跟伪代码不一致,有一些割裂感。有的模式里举的例子也不好。应该结合网上的博客文章一起学习。
然而,学习设计模式,只是看书、教程和博客是不够的,你以为自己已经掌握了,实际上过一阵子就会遗忘,想要真正牢固的掌握设计模式,推荐
为什么设计模式在C++社区没有Java社区流行?
初学者如果在网上搜设计模式的博客和教程,一定会发现大多数的文章都是使用Java语言来描述设计模式的,这不禁令人疑惑:难道只有Java语言需要使用设计模式吗?
首先需要明确的是,理论上来说,只要是支持面向对象的编程语言,都可以使用设计模式,GoF的书中就是用C++和Smalltalk语言来描述设计模式的(书1994年出版,Java语言1995年发行),而且设计模式在比较大型的C++开源项目中用的还是比较多的,比如网络库ACE,界面库QT,游戏引擎Orge, Irrlicht等。
但是总的来说,设计模式确实在Java社区很流行,但是在C++社区却没有那么被关注,为什么呢?
历史原因
C++社区的很多程序员是从C语言转过来的,所以这些人更习惯面向过程的设计方式,即使用C++也只是拿来封装一些简单的类,面向对象的特性用的都不多,更不要说设计模式了。
性能原因
设计模式一般要新增间接层,会造成性能的损失,而很多C++应用的场合会比较强调性能。对于这个问题,
语言优势
SOLID原则是设计模式背后的本质思想,绝大部分设计模式都是基于这些软件设计原则来进行设计的。先了解SOLID原则,能够更好的理解每一种设计模式为什么要这么设计。所以在开始学习具体的模式之前,不妨先看下SOLID原则。
SOLID原则最初是在2000年,由Robert C·Martin在他的论文中提出,经过20多年时间的考验, SOLID 原则仍然是用于创建高质量软件的最佳实践。
SOLID原则有五条:
当然,有原则是件好事, 但是也要时刻从实用的角度来考量, 不要把这几个原则当作放之四海皆准的教条, 盲目遵守这些原则会弊大于利。
注意下文所说的客户端,是泛指服务的调用方,而不是安卓/苹果客户端。
修改一个类的原因只能有一个
尽量让每个类只负责软件中的一个功能, 并将该功能完全封装在该类中。
假如一个类没有遵循单一职责原则,会出现什么问题呢?
所以如果你开始感觉到,在定位程序的某个特定方面的内容时有些困难的话, 就应该想到单一职责原则,并考虑现在是否有必要将类的某些职责分割成几个新的类。
但是与此同时,单一职责原则也讲究聚合和拆分的一个平衡,太过聚合会导致牵一发动全身,拆分过细又会提升代码复杂性。如果实在无法判断/预测是否需要拆分,那就等变化发生时再拆分,避免过度设计。
对于扩展,类应该是“开放”的;
对于修改,类应该是“封闭”的。
一个软件系统应该具备良好的可扩展性,新增功能应当通过扩展的方式实现,而不是在已有的代码基础上修改。
如果一个类已经完成开发、 测试和审核工作, 而且属于某个框架或者可被其他类的代码直接使用的话, 对其代码进行修改就是有风险的。
你可以创建一个子类并重写部分内容以完成不同的行为, 而不是直接对原始类的代码进行修改。 这样你既可以达成自己的目标, 同时又无需修改已有的客户端代码。
当然,如果你发现类中存在缺陷, 直接对其进行修复即可。
子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。
可以理解为,在程序的某个地方,如果客户端使用的是一个父类,那么一定适用于其子类,而且它察觉不出父类对象和子类对象的区别。
里氏替换原则会对子类(特别是其方法)提出许多要求:
1、子类必须完全实现父类的抽象方法,但不能覆盖父类的非抽象方法
2、子类可以实现自己特有的方法
3、当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
例子:假设某个类有个方法用于给猫咪喂食: feed(Cat c) 。 客户端代码调用该方法时总是会传递“猫(cat)” 对象。
- 符合要求的子类:把喂食方法重写成feed(Animal c)。那么当客户端把父类对象切换成子类对象后,由于该方法可用于给任何动物喂食, 因此它仍然可以用于给客户端所传递的“猫”喂食。
- 不符合要求的子类:把喂食方法重写成feed(BengalCat c) ,即只能给孟加拉猫这一特定种类的猫喂食。那么当客户端把父类对象切换成子类对象后,它就不能给客户端所传递的“猫”喂食了。
4、当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
5、子类的实例可以替代任何父类的实例,但反之不成立
里氏替换原则LSP
客户端不应被强迫依赖于其不使用的接口
首先什么是接口?接口就是我们将一个功能抽象定义成一个方法,某一个类实现这个接口就必须要重写这个接口定义的方法,然后这个类就具有了这个功能。
接口隔离原则要求我们尽量提供小而美的接口,而不是一个庞大臃肿的接口,从而满足所有的调用者使用,它是对接口的的一种规范和约束。
举一个例子,一个安全门最基本的功能就是防盗了,好一点的防盗门还会有额外的功能。现在我们开发了一个高端的安全门,包括了防盗、指纹开锁、视频监控三个功能,自然而然的,我们直接在接口里把这三个方法都塞进去了。
由于很多设计者根本没有接口的概念,他们往往只从类的角度上去思考问题,而在类设计完毕后,而为了使用接口象征性的增加一个接口,然后把类的方法签名搬到接口里而已
伪代码如下
//安全门接口
public interface SafetyDoor {
//防盗功能
void antiTheft();
//指纹识别功能
void touchID();
//视频监控功能
void monitoring();
}
后来为了攻占下沉市场,需要开发成本更低的,只有防盗功能和指纹解锁功能的安全门,就尬住了,因为新的安全门无法实现视频监控方法,自然就无法实现上面定义的这个接口。
这个时候就要搬出接口隔离原则,由于这三种功能实际上是相互独立的,所以应该把这三个功能拆分成三个相互隔离的接口,一个安全门需要什么功能,就实现对应的一个或多个接口就行了。
// 防盗接口
public interface AntiTheft {
//防盗功能
void antiTheft();
}
// 指纹识别接口
public interface TouchID {
//指纹识别功能
void touchID();
}
// 视频监控接口
public interface Monitoring {
//视频监控功能
void monitoring();
}
运用接口隔离原则的时候,一定要适度,如果过度地细化和拆分接口,也会导致系统的接口数量的上涨,从而产生更大的维护成本。接口的粒度应该根据具体的业务场景来定。
接口隔离原则
高层模块的类不应该依赖于低层模块的类。两者都应该依赖于抽象接口。
抽象接口不应依赖于具体实现。具体实现应该依赖于抽象接口。
高层模块和低层模块的类的区分:
到底什么是依赖倒置?
首先,按照我们正向的思维,高层模块类要借助低层模块类来完成业务,这必然会导致高层模块类依赖于低层模块类。这种依赖会导致每当低层模块类因为迭代而修改其方法时,高层模块类都不得不进行适配。这超出了高层模块类的职责,因为高层模块的变化原因应当只能有一个,那就是来自用户的业务变更需求。也就是说,高层模块类不应该依赖低层模块类。
对此,我们可以把这个依赖关系倒置过来,这其中的关键就是抽象。我们可以忽略掉低层模块类的细节,抽象出一个稳定的接口,然后让高层模块依赖该接口,同时让低层模块依赖和实现该接口,从而实现了依赖关系的倒置:
实践GoF的23种设计模式:SOLID原则(上)
实践GoF的23的设计模式:SOLID原则(下)
设计模式:三大类、23种设计模式总览
三大分类
每种模式的介绍包括几个部分:
创建型模式对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离开来。客户端只需要了解对象的使用方法,不需要了解对象的创建方法,而创建对象的职责交给创建型模式来负责。这使得软件的结构更加清晰,使整个系统的设计更加符合单一职责原则。
最简单直白的例子,平时客户端是直接通过new的方式来创建并获取对象
// 客户端代码
int main() {
A a = new A(1,2,3,4,5,6,7,8);
}
而创建型模式则是让客户端通过接口的方式来创建和获取对象
// 创建型模式
A getA() {
return new A(1,2,3,4,5,6,7,8);
}
// 客户端代码
int main() {
A a = getA();
}
即使是这么简单的例子,都能看出来,在创建者模式下,客户端创建一个对象更方便了。
当然,创建型模式比例子里复杂的多。
创建型模式:工厂方法
简单工厂模式,工厂方法模式,抽象工厂模式
工厂方法定义一个用于创建对象的接口,让子类决定实例化哪一个类。
工厂方法使一个类的实例化延迟到其子类。
工厂模式的主要功能就是帮助我们实例化对象。之所以叫工厂模式,是因为它通过工厂来执行对象的实例化过程,即用工厂代替new操作。以一个例子来讲解,在没有使用工厂方法的情况下,我们是怎么做的:
小明刚从学校毕业,来到一座大城市工作,住在公司附近的他想要通过摩托车来上下班通勤,但是他没有摩托车,怎么办呢。车辆工程专业毕业的的小明,毫不犹豫的决定自己拼装(生产)一辆摩托车,用来上下班通勤。以此类推,小明也决定自己拼装(生产)一辆汽车,周末开到海边兜风。
在这个例子里,小明就是客户端开发者,摩托车/汽车是产品。客户端直接与产品对接,直接生产产品。
伪代码
// 交通工具
interface IVehicle is
method run()
// 摩托车
class Motorcycle implements IVehicle is
method run() is
print("骑摩托车")
// 汽车
class Car implements IVehicle is
method run() is
print("开汽车")
// 客户端(小明)
class Client is
// 业务代码
method main() is
// 自己生产一台摩托车
IVehicle motorcycle = new Motorcycle()
// 一系列的数据填充过程...
// 骑摩托车
motorcycle.run()
// 生产一台汽车
IVehicle car = new Car()
// 一系列的数据填充过程...
// 开汽车
car.run()
类图
代码解读
由图可见,客户端依赖Motorcycle类和Car类,从而知道如何生产产品对象。客户端依赖IVehicle接口,从而知道如何使用产品对象。
这种原始的方式其实就是生产与使用耦合,并不值得推荐,因为这对个人能力提出了很高的要求:
所以,更推荐的方式应该是生产与使用解耦:
如上所述,小明无法自己生产摩托车/汽车,但是他需要使用摩托车/汽车,怎么办?他如何能够得到一台摩托车/汽车?
小明可以向车厂购买交通工具,这个车厂可以生产多种类型的车:汽车、摩托车等等
伪代码
// 交通工具
interface IVehicle is
method run()
// 摩托车
class Motorcycle implements IVehicle is
method run() is
print("骑摩托车")
// 汽车
class Car implements IVehicle is
method run() is
print("开汽车")
// 车厂
class VehicleFactory is
method getVehicle(type):IVehicle is
if (type == "car")) then
IVehicle car = new Car()
// 一系列的数据填充过程...
return car
else if (type == "motorcycle") then
IVehicle motorcycle = new Motorcycle()
// 一系列的数据填充过程...
return motorcycle
else
throw new Exception("错误!未知的车型");
// 客户端
class Client is
// 业务代码
method main() is
// 车厂
VehicleFactory vehicleFactory = new VehicleFactory()
// 让车厂生产一台摩托车
IVehicle motorcycle = vehicleFactory.getVehicle("motorcycle")
// 骑摩托车
motorcycle.run()
// 让车厂生产一台汽车
IVehicle car = vehicleFactory.getVehicle("car")
// 开汽车
car.run()
类图
代码解读
车厂 VehicleFactory可以生产汽车 Car 和摩托车 Motorcycle,小明要买车的时候,就传递参数给 vehicleFactory.getVehicle(),指明要什么类型的车,然后车厂就把对应的车生产出来,交付给小明后,小明就可以把车开走了。
客户端从头到尾都不知道Motorcycle和Car这两个对象是如何构造的,只需要调用工厂类的方法获取到产品对象,然后按照IVehicle接口的run方法来使用即可。可见客户端只需要依赖工厂类,而不依赖具体的产品类,实现了生产与使用的解耦。
这就是简单工厂模式,它是最简单的工厂方法实现方式。它有一点点小缺陷,就是扩展性不够好。
在上面代码中,车厂只能生产摩托车或者开汽车,如果小明想要买自行车呢?势必得在车厂里加上自行车的生产线了,反映在代码上,就是在VehicleFactory类中添加自行车逻辑的 if 分支,这明显违反了类的开闭原则(允许扩展,拒绝修改)。
车厂这个类,一旦实现好功能,就不应该再改动。因为改动车厂类,新增自行车的逻辑,可能会导致原有的汽车和摩托车的生产逻辑错误,影响车厂原有的功能。比如说小明想买汽车的时候,可能就会发现车厂给他返回了一辆自行车。
那么怎么样才能在不修改已有类的前提下,增加新类型的车呢?
在简单工厂方法里,小明只与一个车厂对接,每次加入新的车型,车厂肯定要加新的if分支逻辑。但是假如我们有多个车厂呢?
伪代码
// 交通工具
interface IVehicle is
method run()
// 摩托车
class Motorcycle implements IVehicle is
method run() is
print("骑摩托车")
// 汽车
class Car implements IVehicle is
method run() is
print("开汽车")
// 车厂接口
interface VehicleFactory is
method getVehicle():IVehicle
// 摩托车车厂
class MotorcycleFactory implements VehicleFactory is
method getVehicle():IVehicle is
IVehicle motorcycle = new Motorcycle()
// 一系列的数据填充过程...
return motorcycle
// 汽车车厂
class CarFactory implements VehicleFactory is
method getVehicle():IVehicle is
IVehicle car = new Car()
// 一系列的数据填充过程...
return car
// 客户端
class Client is
// 业务代码
method main() is
// 让摩托车车厂生产一台摩托车
VehicleFactory motorcycleFactory = new MotorcycleFactory();
IVehicle motorcycle = motorcycleFactory.getVehicle()
// 骑摩托车
motorcycle.run()
// 让汽车车厂生产一台汽车
VehicleFactory carFactory = new CarFactory();
IVehicle car = carFactory.getVehicle()
// 开汽车
car.run()
类图
代码解读
小明家附近有多个车厂,小明想买摩托车,就跟摩托车车厂对接,想买汽车,就跟汽车车厂对接。如果小明想买自行车,那么等附近新开了一个自行车车厂之后,小明跟自行车车厂对接即可。
自行车厂是新增的类,不会对摩托车车厂和汽车车厂类进行修改,自然不可能会影响到原有的两个车厂的功能,符合开闭原则。
但是也可以发现,原本只需要一个车厂,现在需要多个车厂,虽然遵循了开闭原则,但是代码的复杂程度也增加了。因此,选择使用简单工厂模式还是工厂方法模式,还是得看场景。
优点
缺点
1、作为一种创建类模式,在任何需要生成复杂对象(需要调用一堆函数才能拼装完成)的地方,都可以使用工厂方法模式。将产品的实例化过程封装起来,使得调用者无需关心产品的实例化过程。
值得注意的是,简单对象(特别是只需要通过new就可以完成创建的对象),无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。
2、当需要系统有比较好的扩展性时,可以考虑工厂方法模式。不同的产品用不同的工厂来组装,每当需要新增产品时,新增一个产品类和工厂类即可,符合开闭原则,不会影响原有的逻辑。
为创建一组相关或相互依赖的对象提供一个接口,而且无须指定它们的具体类。
抽象工厂模式是工厂方法模式的升级版本,用一个简单例子来说明:
多种产品及其不同变体(风格):
继续说上面的例子,假如你需要购买一张椅子,那么你想买什么风格就买什么风格,根据工厂方法,你直接到向对应风格的椅子工厂购买椅子即可:
但是当你需要购买一套家具时,你肯定不想买到风格不一致的产品。因此,如果仍然沿用工厂方法,那么你肯定需要逐个地、向同一种风格的、负责生产不同产品的工厂来购买产品,比如说我要购买现代风格的一套家具,那么我需要
可见,整个过程非常累,每一种产品都需要单独向工厂订购,而且假如在购买某种产品时,不小心拨错了电话,就可能导致购买了不同风格的产品,比如现代风格的沙发,跟维多利亚风格的椅子格格不入:
总的来说,以上的做法有这些缺点:
抽象工厂的解决方案就是,为创建一组相关或相互依赖的对象提供一个接口
抽象工厂只需要三个工厂
从同一个工厂类对象中取出来的椅子和沙发,能够确保是同一个风格,保证不会出现混搭的问题。
伪代码
为了代码简略,
// 抽象产品接口
interface Chair is
method sit()
interface Sofa is
method sit()
// 具体产品
// 椅子
class VictorianChair implements Chair is
method sit()
class ModernChair implements Chair is
method sit()
// 沙发
class VictorianSofa implements Sofa is
method sit()
class ModernSofa implements Sofa is
method sit()
// 抽象工厂
// 提供给客户端的,用于创建一组相关或相互依赖的对象的接口
interface FurnitureFactory is
method getChair():Chair
method getSofa():Sofa
// 具体工厂
// 维多利亚风格
class VictorianFactory implements FurnitureFactory is
method getChair():Chair is
return new VictorianChair()
method getSofa():Sofa is
return new VictorianSofa()
// 现代风格
class ModernFactory implements FurnitureFactory is
method getChair():Chair is
return new ModernChair()
method getSofa():Sofa is
return new ModernSofa()
// 客户端
class Client is
private field factory: FurnitureFactory
method main() is
config = readApplicationConfigFile()
if (config.style == "Victorian") then
// 维多利亚风格家具工厂
factory = new VictorianFactory()
else if (config.style == "Modern") then
// 现代风格家具工厂
factory = new ModernFactory()
// 获取一系列产品
Chair chair = factory.getChair()
Sofa sofa = factory.getSofa()
// 使用这一系列产品(业务中往往要求这一系列产品是同一种变体)
chair.sit()
sofa.sit()
类图
代码解读
可见,整个抽象工厂架构包括了下面四个组成部分:
可见,通过抽象工厂,客户端可以得到以下好处就是:
优点
当然抽象工厂本身就是工厂方法的进阶版本,抽象工厂也具备工厂方法的优点
缺点
正因为抽象工厂模式存在“开闭原则”的倾斜性,适合新增产品变体,而不适合新增产品类型。因此要求开发人员在开发之初就能够全面考虑,不会在开发完成之后向系统中增加新的产品类型,否则将需要修改大量代码,难以维护。
因此,在满足以下条件的情况下适合使用抽象工厂方法:
1、业务场景需要生成复杂对象(需要调用一堆函数才能拼装完成)。使用工厂模式将产品的实例化过程封装起来,使得调用者无需关心产品的实例化过程。
2、业务场景包含多个产品类型和多个产品变体的情况下,如果后续会频繁新增产品变体,而且不会新增产品类型,那么使用抽象工厂将使代码方便扩展,符合开闭原则。
别名建造者模式。
秒懂设计模式之生成器模式(Builder pattern)
生成器模式其实分为传统生成器和简化版生成器两种,Gof的书中讲的是传统生成器模式,而后来Java社区经常使用的是简化版的生成器。由1.5章节可知,Java盛行,网上的设计模式博客基本都是面向Java开发者的,导致当你在网上浏览生成器模式的博客时候,会发现有一部分博客所说的生成器模式跟书上的不一致,导致混乱。
在这里明确区分一下:
支持通过链式写法来构造复杂对象,更直观和简洁
在我们日常开发过程中,可能会遇到这样的场景:一个类包含多个属性,在实例化对象时,一部分属性是必填的,一部分属性是选填的。
例子:一个台式电脑,主机部分是必填的(cpu和内存的型号),而外设部分是选填的(usb接口数、键盘和显示器)
class Computer {
private string cpu; //cpu,必填
private string ram; //内存,必填
private int usb_count; //usb接口数,选填
private string keyboard; //键盘,选填
private string display; //显示设备,选填
// 其他属性,省略
}
上面例子写的简单,只列出了几个属性,但是实际上一个电脑所包含的属性是庞大的,完全算得上是复杂类。
在这种情况下,大概有以下两种创建该复杂对象的方法
1、包括所有可能参数的超级构造函数
class Computer {
...........
..........
public:
// 构造函数,可选参数放在后面,有设置默认参数值(默认参数可以让你在调用时少填参数,但能力有限,当你需要给很后面的一个参数赋值的时候,前面的默认参数也必须赋值)
Computer(string cpu, string ram, int usb_count = 0, string keyboard = "", string display = ""){
this.cpu = cpu;
this.ram = ram;
this.usb_count = usb_count;
this.keyboard = keyboard;
this.display = display;
}
}
int main() {
Computer computer1("I5处理器", "三星125", 2, "苹果键盘", "苹果显示器");
Computer computer2("I7处理器", "海力士222", 4, "联想键盘", "联想显示器");
}
在日常开发中,我们经常使用这种超级构造函数来创建一个复杂对象,这种方法的缺点是:
2、属性设置模式
class Computer {
...........
..........
public:
Computer(){}
void set_display(string display) {
this.display = display;
}
// 其余set函数省略
}
int main() {
Computer computer;
computer.set_display("联想显示器");
// 省略其余一系列set函数
}
相比于超级构造函数,使用这种属性设置方式来创建复杂对象,好处是能够直观地看到我给哪个属性赋值了,可读性很高,避免了填写超级构造函数的参数时填错位置的问题。
在我们的代码中,经常用这种方式来创建一个C++的protobuf message对象(Protobuf 自动生成的 message 类会为每个字段提供相应的 setter 方法)。
但是也有缺点,因为一个对象应该是先构建完,然后才允许使用。而在属性设置方式中,构建过程交给了客户端来负责,客户端分步设置属性,有可能客户端还没设置完属性就直接使用了,导致某些必要属性在使用时还未初始化而导致程序出错。
那么我们有什么更好的方式来构建这个复杂对象呢?
简化版生成器给出了它的答案:在产品类中,增加一个生成器类。通过生成器来构建这个复杂对象,可以达到链式调用的效果,使得更直观、简洁、易用。
Java代码
仍然是上面列举的电脑类例子
public class Computer {
private final String cpu;//必须
private final String ram;//必须
private final int usbCount;//可选
private final String keyboard;//可选
private final String display;//可选
// 注意构造函数传参为Builder
private Computer(Builder builder){
this.cpu=builder.cpu;
this.ram=builder.ram;
this.usbCount=builder.usbCount;
this.keyboard=builder.keyboard;
this.display=builder.display;
}
// 定义在产品类内部的Builder类
public static class Builder{
private String cpu;//必须
private String ram;//必须
private int usbCount;//可选
private String keyboard;//可选
private String display;//可选
// 注意构造函数和setter函数的返回值都是Builder自身
public Builder(String cup,String ram){
this.cpu=cup;
this.ram=ram;
}
public Builder setUsbCount(int usbCount) {
this.usbCount = usbCount;
return this;
}
public Builder setKeyboard(String keyboard) {
this.keyboard = keyboard;
return this;
}
public Builder setDisplay(String display) {
this.display = display;
return this;
}
// build函数返回产品对象
public Computer build(){
// 这里可以做校验
return new Computer(this);
}
}
}
客户端使用链式调用,一步一步的把对象构建出来:
Computer computer = new Computer.Builder("因特尔","三星")
.setDisplay("三星24寸")
.setKeyboard("罗技")
.setUsbCount(2)
.build();
代码解读
过程是先创建一个Builder对象,然后链式调用Builder的各个setter函数,最后调用build函数返回产品对象。优点如下:
在 Java 中,有许多知名的框架和库使用了生成器模式来创建对象。
例子:Java11新特性-效能翻倍的HttpClient
HttpRequest 是用于描述请求体的类,也支持通过生成器模式构建复杂对象,主要的参数有:
- 请求地址
- 请求方法:GET,POST,DELETE 等(默认是GET)
- 请求体 (按需设置,例如 GET 不用 body,但是 POST 要设置)
- 请求超时时间(默认)
- 请求头
//使用参数组合进行对象构建,读取文件作为请求体 HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("http://www.baidu.com")) .timeout(Duration.ofSeconds(20)) .header("Content-type","application/json") .POST(HttpRequest.BodyPublishers.ofFile(Paths.get("data.json"))) .build();
使你能够分步骤创建复杂对象。
该模式允许你使用相同的创建代码生成不同类型和形式的对象。
当一个类的内部属性过多、赋值逻辑过于复杂的时候,客户端想要创建这个复杂对象就需要了解这个类的每个属性的含义和赋值逻辑,这就会增加客户端开发者的学习成本。
生成器模式(Builder Pattern)则是将客户端与复杂对象的创建过程分离开来,由生成器(Builder)负责组装负责对象,客户端无须知道复杂对象的内部组成部分与赋值逻辑,只需要知道所需生成器的类型即可创建对象,实现了解耦。
还是电脑的例子,组装一个电脑其实很复杂,
硬件的安装顺序是有讲究的,一般来说电源要先于主板安装,模组电源需要先把需要的模组线插上,不用的线材收纳好。 装好电源后将电源线理顺经背线孔放置机箱后面。 CPU的安装要注意方向,在CPU的一个角有一个三角形,主板上同样会有一个三角形,只要对齐他们就说明安装方向无误。
现在我们的需求是,如何高效地,拼装出不同品牌的电脑呢。梳理一下目前的问题:
现在我们有一个Computer类,我们要如何表示不同的电脑呢?
class Computer {
private string cpu; //cpu,必填
private string ram; //内存,必填
private int usb_count; //usb接口数,选填
private string keyboard; //键盘,选填
private string display; //显示设备,选填
// 其他属性,省略
}
在不使用生成器模式的情况下,常用的有两个方法,他们都难以令人满意
方法 | 描述 | 优点 | 缺点 |
---|---|---|---|
方法一 | Computer基类只包含必选属性(cpu、ram),然后为每一种具体类型的电脑派生出一个子类,其中包含了该类型电脑配备的可选属性(外设)。然后在每个子类的构造函数中执行装机步骤。 | 每个子类在实例化时,客户端只需要关心当前对象需要赋值的属性,不需要关心当前对象不需要赋值的属性。 | 会制造很多子类,增加代码复杂度。 |
方法二 | 只有一个Computer类,不派生。所有必选和可选属性都写在Computer类中,使用一个超级构造函数来给所有属性赋值。在超级构造函数中执行装机步骤。 | 简单直白,没有增加类,也就不会增加代码复杂度 | 客户端需要关心当前对象不需要赋值的属性,构造函数的调用非常不简洁,填写参数时会感到非常痛苦。 |
生成器模式,其实是在方法二的基础上进行改进的,也就是说不派生出子类,所有必选和可选属性都写在Computer类中,但是生成器模式不使用超级构造函数,那么它是如何执行创建步骤的呢?
生成器模式的核心思想就是,将复杂对象的构造代码从产品类中抽取出来, 并将其放在一个名为生成器(Builder)的独立对象中。
生成器模式包含以下几个组成部分:
主管不是必须的,但是使用主管类可以进一步精简客户端的工作量:
伪代码
一个简化版本的电脑类的例子,有两个类型的电脑,联想电脑和mac电脑,其中他们都配备有cpu、ram和usb接口,但是
/*产品类*/
class Computer is
private field cpu:string //cpu,必填
private field ram:string //内存,必填
private field usb_count:int //usb接口数,选填
private field keyboard:string //键盘,选填
private field display:string //显示设备,选填
/*构造函数*/
constructor Computer():Computer
/*setter函数*/
method setCpu(cpu:string) is
this.cpu = cpu
method setRam(ram:string) is
this.ram = ram
method setUsbCount(usb_count:int) is
this.usb_count = usb_count
method setKeyboard(keyboard:string) is
this.keyboard = keyboard
method setDisplay(display:string) is
this.display = display
}
/* 生成器接口,每一个函数是一个构建步骤(这里把每个步骤简化成给一个属性赋值,实际的业务场景下没这么简单)*/
interface ComputerBuilder is
method setCpu()
method setRam()
method setUsbCount()
method setKeyboard()
method setDisplay()
method getComputer():Computer
/*具体生成器:联想某型号的电脑*/
class LenovoComputerBuilder implements ComputerBuilder is
/*具体生成器一定会包含着产品对象*/
private field computer:Computer
constructor LenovoComputerBuilder() is
computer = new Computer() /*生成一个产品对象的雏形*/
/*提供每一个构建步骤的不同实现*/
method setCpu() is
computer.setCpu("I7处理器")
method setRam() is
computer.setRam("三星内存")
method setUsbCount() is
computer.setUsbCount(4)
method setKeyboard() is
computer.setKeyboard("联想键盘")
method getComputer():Computer is
return computer
/*具体生成器:苹果某型号的电脑*/
class MacComputerBuilder implements ComputerBuilder is
private field computer:Computer
constructor MacComputerBuilder() is
computer = new Computer()
/*提供每一个构建步骤的不同实现*/
method setCpu() is
computer.setCpu("mac处理器")
method setRam() is
computer.setRam("mac内存")
method setUsbCount() is
computer.setUsbCount(2)
method setDisplay() is
computer.setDisplay("mac显示器")
method getComputer():Computer is
return computer
/*主管*/
class Director is
/*按照一定的顺序调用生成器的构建步骤来生成联想电脑,不需要构建显示器*/
method createLenovoComputer(builder:ComputerBuilder) is
builder.setCpu()
builder.setRam()
builder.setUsbCount()
builder.setKeyboard()
/*按照一定的顺序调用生成器的构建步骤来生成mac电脑,不需要构建键盘*/
method createMacComputer(builder:ComputerBuilder) is
builder.setDisplay()
builder.setCpu()
builder.setRam()
builder.setUsbCount()
/*客户端*/
class Client is
method main() is
/* 创建主管 */
Director director = new Director()
/* 创建联想电脑的生成器 */
ComputerBuilder lenovo_computer_builder = new LenovoComputerBuilder()
/* 把生成器交给主管,让主管来按一定顺序调用生成器的构建步骤,生成联想电脑 */
director.createLenovoComputer(lenovo_computer_builder)
/* 从生成器手里获取联想电脑产品 */
Computer lenovo_computer = lenovo_computer_builder.getComputer()
/* 创建mac电脑的生成器 */
ComputerBuilder mac_computer_builder = new MacComputerBuilder()
/* 把生成器交给主管,让主管来按一定顺序调用生成器的构建步骤,生成mac电脑 */
director.createMacComputer(mac_computer_builder)
/* 从生成器手里获取mac电脑产品 */
Computer mac_computer = mac_computer_builder.getComputer()
类图
代码解读
可以看到,客户端生成一个联想电脑非常简单,只需要指定联想电脑的生成器,然后交给主管执行创建逻辑就可以了,
不妨再回顾一下之前的两种创建方式:
1、超级构造函数:
int main() {
Computer computer2("I7处理器", "三星内存", 4, "联想键盘", "");
}
2、setter函数:
int main() {
Computer computer;
computer.set_cpu("I7处理器");
computer.set_ram("三星内存");
// 省略其余一系列set函数
}
假如你是客户端开发者,现在你需要创建一个复杂对象,你更喜欢哪种创建方式呢?
优点
缺点
在同时满足以下条件的情况下,可以考虑使用生成器模式:
to be continue…