尝试用最最通俗的方式把设计模式讲解清楚。
打个比方,设计模式就是一个工具箱,需要钉钉子时要用锤子,需要拧螺丝时用螺丝刀,大多数初学者没有想清楚这一点,就会想着,我手里有锤子和螺丝刀,到底该用在哪里呢。
另外一个令初学者想不清楚的问题是:设计模式有什么好处。通常讲解设计模式的书都会说些高内聚低耦合之类的话,仍旧想不明白什么叫高内聚低耦合。用最通俗的话来说:代码中有些地方是固定不变的,比如约定好的流程,有些地方是要经常变化的,比如具体实现,现在我想把实现方式A替换为实现方式B,我可以非常方便地替换掉。
几个设计原则,都是为了这个目的服务的。稍后会举例解释。
大多数地方把设计模式分为构建型,结构型等,为了能把设计模式通俗地讲解清楚,我们把它们大概分成两类:
- 以继承/多态为实现原理的,比如工厂,策略
- 不是以继承/多态为实现原理的,比如单例,原型模式
主要会讲类型1
假设我们的起床上班流程是这样的: (1)穿上迪卡侬的格子衫 --> (2)穿上优衣库的大裤衩 --> (3)洗脸刷牙 --> (4)吃早饭 --> (5)坐地铁1号线 --> (6)到达公司 如果每天都是这样固定不变,那确实不需要什么设计模式,直接一步步地按顺序写代码,反而清晰。
但是,假设某一天:我想把迪卡侬的格子衫换成优衣库的条纹衫,把优衣库的大裤衩换成牛仔裤,那我就需要找到(1), (2)修改代码,如果像很多女生一样每天都要换,那么(1), (2)处的代码就要经常修改。 此时,我们发现,有些东西是经常变化的,比如具体的上衣和裤子,有些东西是固定不变的,比如穿上衣/裤子这件事, 在没有设计模式时,代码可能是这样的:
void 穿迪卡侬的格子衫();
void 穿迪卡侬的条纹衫();
void 穿优衣库的格子衫();
void 穿优衣库的条纹衫();
void 起床上班() {
if (某条件) {
穿迪卡侬的格子衫();
} else if (别的什么条件) {
void 穿优衣库的条纹衫();
} ...
}
现在我想明白了,不管条件多么复杂,我最终要做的,就是穿一件上衣。假设我有一个管家,我把条件告诉他,让他拿一件衣服给我,我穿上就可以了:
class 上衣
class 迪卡侬的格子衫 extends 上衣
class 迪卡侬的条纹衫 extends 上衣
class 优衣库的格子衫 extends 上衣
class 优衣库的条纹衫 extends 上衣
class 管家 {
上衣 根据条件给主人拿一件上衣(判断条件) {
return 某上衣的具体实现类();
}
}
我.穿上衣(管家.根据条件给主人拿一件上衣(某条件));
这就是工厂模式想要解决的问题,我想要一个的东西,我不需要关心它到底是具体哪个东西,一切交给XXFactory类。 代码看上去似乎清晰了一些,但也似乎只是把一些代码挪了个地方而已。 但事实上,这样做还有一个很大的好处:原来的手动判断方式,我需要清楚地知道我的每一件衣服,不然代码就卡在那里,无法继续;而使用工厂模式后,我知道我需要的仅仅是一件上衣,一个抽象,此时此刻,哪怕我的衣柜是空的,我也可以继续往下写代码,代码写好以后,就算新添加了上衣的子类,我也不需要做任何改变,由管家帮我打理就可以了。
这里就引出了六大原则的依赖倒置原则: 依赖抽象而非具体实现,上衣就是抽象,迪卡侬/优衣库的格子衫/条纹衫就是具体实现。依赖抽象,意思着我可以随时轻松替换掉具体实现,也就是所谓的扩展性和维护性。
现在出门坐地铁1号线,假设今天坐地铁的人特别多,都排队到站外了,我准备坐公交,在公交站发现公交迟迟不到,我只好打车。
根据上衣的例子,可以发现:我必须乘坐某公交工具从家到公司,这件事固定不变,具体的交通工具有多种选择。作为抽象的扩展,我还可以骑共享单车,甚至步行。这样可以让管家随时根据条件,给我提供/更新交通工具。
class 交通工具 {
void 把某人从A带到B(某人, 地点A, 地点B);
}
class 地铁 extends 交通工具
class 公交 extends 交通工具
class 出租 extends 交通工具
class 共享单车 extends 交通工具
class 步行 extends 交通工具
交通工具 某具体的交通工具 = 管理.帮我选择一个交通工具(某条件)
某具体的交通工具.把某人从A带到B(我, 家, 公司)
// 下班: 某具体的交通工具.把某人从A带到B(我, 公司, 家)
此处可以引申出六大原则的里氏替换原则: 所有引用基类的地方必须能透明地使用其子类的对象,打个比方,我想要一个交通工具,飞机作为交通工具,没毛病吧,它能实现把某人从A地点带到B地点,但我上班,又不能坐飞机。在语法上,你要一个交通工具,我给你一个飞机,编译运行都很正常,但对于业务逻辑明显不合适,所以,抽象出通勤交通工具类。 所谓的方便地替换实现类,首先要保证代码逻辑不会出错,假设现在没有通勤交通工具的抽象,我还把这份代码交给了不熟悉的同事,很难保证他不会出错。实现了此原则,同样提供了维护性和扩展性。
class 交通工具 {
void 把某人从A带到B(某人, 地点A, 地点B);
}
class 飞机 extends 交通工具
class 通勤交通工具 extends 交通工具
class 地铁 extends 通勤交通工具
class 公交 extends 通勤交通工具
class 出租 extends 通勤交通工具
class 共享单车 extends 通勤交通工具
class 步行 extends 通勤交通工具
很多人经常会纠结,这两个模式有什么异同,明明代码看着很像,为什么就是两个模式呢? 震惊的是,在23种设计模式中,有很多个都是代码一样,比如外观模式和中介者模式,代理模式和装饰者模式,稍后会有举例。 道理很简单:这些模式的实现原理是继承/多态,再通俗点说就是父类和子类,再加上一些组合,没多少花样可以发挥。 那么为什么会出现这种 "代码一样,模式不一样" 的情况呢?因为它们的关注点不一样,打个比方,同样是上班(代码一样),我是为了赚钱糊口,老板是为了实现理想(模式不一样)。 在回到工厂和策略的对比之前,需要先回忆一下类的组成:字段和方法。 字段表示这个类有些什么特征,比如上衣的颜色,材质;方法表示这个类能提供什么功能,比如通勤工具的把我从家带到公司()。而字段呢,大多数情况下都是private然后提供getter()。 区别就在这里了:工厂返回的对象,主要是关心它的字段,策略返回的对象,主要关心它的方法,而因为字段多数情况下是通过getter()提供,导致代码看上去就更加没有区别了。
class Product {
private String name;
private int color;
public String getName() {
return name;
}
public int getColor() {
return color;
}
}
class Factory() {
public static Product getProduct(int type) {
if (type == 1) return new Product1();
else if (type == 2) return new Product2();
else return new Product3();
}
}
class Strategy {
void doSomething(int num);
}
class StrategyContext {
public static Strategy getStrategy(int type) {
if (type == 1) return new Strategy1();
else if (type == 2) return new Strategy2();
else return new Strategy3();
}
}
class Client {
void main() {
Product product = Factory.getProduct(1);
Strategy strategy = StrategyContext.getStrategy(1);
System.out.println(product.getName() + ", " + product.getAge());
strategy.doSomething(1);
}
}
这是一段非常简单的两种模式的示例代码,现在对它手动混淆
class P {
public String N() = name;
public int C() = color;
}
class F() {
public static Product getP(int type) {
if (type == 1) return new P1();
else if (type == 2) return new P2();
else return new P3();
}
}
class S {
void doSomething(int num);
}
class SC {
public static Strategy getS(int type) {
if (type == 1) return new S1();
else if (type == 2) return new S2();
else return new S3();
}
}
class Client {
void main() {
P p = F.getP(1);
S s = SC.getS(1);
System.out.println(p.getN() + ", " + p.getA());
s.doSomething(1);
}
}
简化后发现,真的就一模一样了, 它们的区别仅仅在:工厂得到的对象,主要用它的字段,策略得到的对象,主要用它的方法。
回到(1), (2)的穿衣步骤,有新的要求:上衣和裤子要成套,要么都是迪卡侬的,要么都是优衣库的,或者某天要参加年会,公司要求必须穿西装西裤。
经过工厂模式的改造后,我们现在有上衣Factory和裤子Factory,只是它俩之间没有建立联系,通俗点说:管家你随便给我件上衣和随便给我条裤子就行。虽然理论上可以让两个工厂根据相同的条件返回对应的产品,但它们之间没有约束关系,完全允许type=1,却分别返回迪卡侬上衣和优衣库裤子。
抽象工厂就是用来建立约束条件的,仅此而已。
class 迪卡侬Factory {
迪卡侬上衣 get上衣();
迪卡侬裤子 get裤子();
}
class 优衣库Factory {
优衣库上衣 get上衣();
优衣库裤子 get裤子();
}
在上衣的例子中,上衣有两种分类方式:
- 品牌:迪卡侬,优衣库
- 样式:格子衫,条纹衫
在不使用桥接模式时,会有这些类:
class 上衣
class 迪卡侬上衣
class 迪卡侬格子衫
class 迪卡侬条纹衫
class 优衣库上衣
class 优衣库格子衫
class 优衣库条纹衫
假设现在它们都有新的样式,纯色衫,就只能增加两个新的子类,迪卡侬纯色衫,优衣库纯色衫;而且,在迪卡侬格子衫中,样式的代码为:println("我是格子衫"),在优衣库格子衫,样式代码也同样是打印这句话,样式的代码还不能复用。
同时,不光样式可以扩展,品牌也可以扩展,比如我想新增无印良口的上衣。
此种场景就是桥接模式要怎么的:两个维度都会变化,先不考虑桥接,其实用组合,效果完全相同,两个字段都可以用自己的子类随意组合。
class 品牌
class 迪卡侬 extends 品牌
class 优衣库 extends 品牌
class 无印良品 extends 品牌
class 样式
class 格子 extends 样式
class 条纹 extends 样式
class 纯色 extends 样式
class 上衣 {
品牌 具体的品牌;
样式 具体的样式;
两个setter/getter()
}
而桥接,就是把其中一个字段定义成类,另一个保留为字段:
class 品牌上衣 {
样式 具体的样式;
setter/getter()
}
class 迪卡侬上衣 extends 品牌上衣
class 优衣库上衣 extends 品牌上衣
这样,想要增加品牌,就继承品牌上衣,想要增加样式,就新增样式子类,然后set注入到品牌上衣类。
####以甲乙双方为例解释外观模式,中介者模式,命令模式
到公司后,开始做项目,一个项目组的成员有项目经理、产品经理、UI、前端、后端、测试。
#####外观模式 现在我是甲方,我来跟乙方对接,对我来说,我需要知道乙方有哪些人吗?如果乙方有个全栈工程师,可以自己把前端后端和UI全都做了,跟我甲方有什么关系。我只需要跟项目经理沟通,约定好需求啦,时间啦之类的,就足够了。
外观模式就这么简单,不管你内部有多复杂,对外接口要简单。
#####中介者模式
对于项目组内部呢,假设产品经理改需求,改完以后呢,他需要通知UI提供新的效果图,需要通知前端等UI提供新的效果图后按图开发,需要通知测试人员按新效果图测试,还要通知项目经理,开发时间可能会变化。假如测试发现有个可以优化的功能点,他要找产品、后端、项目。 这种交互方式,需要每个人都非常清楚做每件事要找的每个人。 现实工作中,开发经常会报怨,测试不顾项目时间紧急,将一个优化功能当作BUG提了出来要求修改,产品改了UI但测试不知道仍旧按旧UI测试。
中介者模式就是用来处理这种情况,所有人都只与项目经理交互,不能与其他人交互,这样,除了项目经理任务重一些,其他一切逻辑都会特别清晰。
#####外观模式和中介者模式的对比
这两个模式都侧重概念,在代码上没什么好表示的。 项目经理这个角色,从外部的甲方来看,他就是外观模式,一个模块给外部留一个简单的调用,模块内可以随意变化;从乙方内部看,他就是中介者模式,内部模块之间互相不知道对方,任意模块就可以随意变化。
这里可以引申出最少知道原则: 当一个模块不知道其他模块的存在时,其他模块就可以悄悄地更换掉。
#####命令模式 命令模式可以比喻为产品经理写了一份需求列表,指定具体的开发者。
class 需求 {
开发者 某个具体的开发者;
setter()
void execute() {
某个具体的开发者.按照他自己的方式去实现这个需求();
}
}
比如有一个Android的需求,给同事A,他用Java去写代码;给同事B,他用Kotlin写代码。要完成的事情固定不变,具体做事的人可以变。
####以产品经理给研发经理指派需求为例解释代理模式,装饰者模式,责任链模式,适配器模式
这四个模式的代码极其相似
#####代理模式和装饰者模式
这两个模式需要先从把代码展示出来:
代理模式的代码
interface Sth {
void sth();
}
class SthProxy implments Sth {
Sth actual;
void sth() {
// 代理类自己做一些事
actual.sth(); // 实际实现类继续做
}
}
// 装饰者模式的代码
interface Sth {
void sth();
}
class SthDecorator implements Sth {
Sth sth;
void sth() {
// 可以在前面做一些事
sth.sth();
// 也可以在后面做一些事
}
}
class Client {
void main() {
// 代理
Sth sth = new SthProxy(new SthImpl());
sth.sth();
// 装饰者
Sth sth = new SthDecorator(new SthImpl());
sth.sth();
}
}
代码一模一样哦,都是一个类A内部包含一个相同类型的成员B,在调用A的方法时,转调B的方法。
有一个研发经理和研发成员,产品经理发出的需求,首先要交给研发经理,研发经理检查一下需求,发现需求不合理,就打回给产品经理让他修改,检查通过后才交给研发成员去做,可以看作是代理模式。 研发经理收到需求后,把需求中一些不明确的部分给明确,比如需求说要更流畅,研发经理改成了响应时间加快100ms,然后交给研发成员去做,可以看作是装饰者模式。
它们的区别在于:代码模式侧重于添加限制条件,具体实现类非常单纯地执行任务,代理类可以添加一些条件,比如没有注册,就不用去具体执行,比如超过有效期,也不用去具体执行;装饰者模式侧重于添加功能,在具体实现类执行完之后,添加自己的功能,比如验收。
class 代码检查 {
protected 代码检查 checker;
void done();
}
class 研发成员 extends 代码检查 {
checker = null;
void done() {
// 写代码完成
}
}
class 研发经理 extends 代码检查 {
checker = 研发成员;
void done() {
checker.done();
// 研发经理自己检查一遍
}
}
class 测试人员 extends 代码检查 {
checker = 研发经理;
void done() {
checker.done();
// 测试人员执行测试流程
}
}
class 产品经理 extends 代码检查 {
checker = 测试人员;
void done() {
checker.done();
// 产品验收
}
}
装饰者一般会嵌套多层,用来增加在已有功能上扩展一些新操作。 代理的代码跟装饰者一模一样,当然也是可以嵌套多层的, 但它们的区别不在代码,而在意图:代理要添加限制条件,装饰者要增加额外的功能。
责任链的代码仍旧几乎一模一样:
class ChainItem {
ChainItem next;
void doSth() {
// 自己做一些事
// 自己做完了
if (next != null) {
next.doSth(); // 链条的下一项去做
}
}
}
同样的一个类内部有一个同类型的成员,自己做完后,转调成员的相同方法。 以刚刚的代码检查为例:
研发成员.next = 研发经理
研发经理.next = 测试人员
测试人员.next = 产品经理
研发成员.done()
研发成员 --> 研发经理 --> 测试人员 --> 产品经理
总结一下就是,它们全都是一个类包含一个同类型的成员,执行自己的方法时会转调成员的相同方法。
#####适配器模式
适配器的代码和代理/装饰者的代码几乎一模一样,区别仅在成员变量的类型不同。
class Adapter {
Adaptee adaptee;
void methodA() {
adaptee.methodB();
}
}
都是一个类持有一个成员,调用类的方法时,转调成员的方法。当然,跟前两个一样,仍旧可以在转调前后加自己的方法。 它的功能是,现在已经有一个功能实现类,只是它的接口跟我需要的接口不一样,做一下转换。
####以任务分解为例讲解模板方法模式,构建者模式
#####模板方法模式 研发经理收到需求后,对任务进行分解:
abstract class 开发流程 {
void 开发() {
// step1
登录注册模块();
// step2
会员模块();
// step3
支付模块();
}
}
研发经理规定好了任务执行顺序,研发成员实现这个类,按自己的方式去完成这些模块。固定不变的是流程,可以变化的是具体实现。
#####构建者模式
构建者分两种,一种是链式构建: new XX.Builder().setA().setB().setC().build(), 现在要说的是另外一种方式:
class Director {
Builder builder;
void construct() {
builder.title();
builder.body();
builder.foot();
}
产品 build() {
return builder.build();
}
}
构建流程已经被Director.construct()定义好了,传入不同的builder对象,就可以构建不同的产品。固定不变的是流程,变化的是具体的实现方式。 Director.construct()和开发流程.开发(),都是提前定义好了一套流程,子类具体完成不同的部分,就像工厂和策略一样,构建者关注的是产生对象,模板方式关注的是调用方法。