Skip to content

Latest commit

 

History

History
497 lines (390 loc) · 18.8 KB

设计模式笔记.md

File metadata and controls

497 lines (390 loc) · 18.8 KB

设计模式笔记

尝试用最最通俗的方式把设计模式讲解清楚。

设计模式是干什么的?

打个比方,设计模式就是一个工具箱,需要钉钉子时要用锤子,需要拧螺丝时用螺丝刀,大多数初学者没有想清楚这一点,就会想着,我手里有锤子和螺丝刀,到底该用在哪里呢。

另外一个令初学者想不清楚的问题是:设计模式有什么好处。通常讲解设计模式的书都会说些高内聚低耦合之类的话,仍旧想不明白什么叫高内聚低耦合。用最通俗的话来说:代码中有些地方是固定不变的,比如约定好的流程,有些地方是要经常变化的,比如具体实现,现在我想把实现方式A替换为实现方式B,我可以非常方便地替换掉。

几个设计原则,都是为了这个目的服务的。稍后会举例解释。

设计模式的分类

大多数地方把设计模式分为构建型,结构型等,为了能把设计模式通俗地讲解清楚,我们把它们大概分成两类:

  1. 以继承/多态为实现原理的,比如工厂,策略
  2. 不是以继承/多态为实现原理的,比如单例,原型模式

主要会讲类型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裤子();
}
桥接模式

在上衣的例子中,上衣有两种分类方式:

  1. 品牌:迪卡侬,优衣库
  2. 样式:格子衫,条纹衫

在不使用桥接模式时,会有这些类:

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()和开发流程.开发(),都是提前定义好了一套流程,子类具体完成不同的部分,就像工厂和策略一样,构建者关注的是产生对象,模板方式关注的是调用方法。