4

Head First 设计模式-读书简记

 3 years ago
source link: http://lanbing510.info/2016/03/25/HeadFirst-Design-Patterns.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Head First 设计模式-读书简记

2016年03月25日




最近再次温习设计模式,将之前写的《Head First 设计模式》的读书笔记又整理了一番。分享出来,共同学习。


定义算法族,分别封装起来,让它们之间可以相互替换,此模式让算法的变化独立于使用算法的客户。

TIP1:找出应用中可能需要变化之处,将其独立出来,不要和那些不需要变化的代码混在一起。

TIP2:针对接口编程,而不是针对实现编程。

理解与实践

设计一套鸭子模拟游戏,游戏中会出现各种鸭子,鸭子的飞行、游泳、叫声等行为各异。

1 如果用继承来实现,例如鸭子类中加入fly(),quack(),很难知道鸭子的全部行为,加入一个新的行为后,也造成了不需要此行为的鸭子的改动;同时会造成代码在多个子类中重复;运行时的行为也不容易改变。违背TIP1,2。

2 如果使用接口,例如使用Flyable,Quackable,每个使用接口的类都要负责实现,无法达到代码复用,如果需要修改某个行为,必须追踪到每个定义此行为的类中修改。违背TIP2。

3 解决方法,使用策略模式,即可以将行为算法从类中独立出来,建立一组新类。鸭子的飞行、叫等行为委托给行为类。这样我们还可以在鸭子类中通过设定方法来动态改变鸭子的行为。

Duck.java

观察者模式


定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会受到通知并自动更新。

TIP3:为了交互对象之间的松耦合设计而努力。对象之间的松耦合使对象之间的互赖性降到最低,能让我们建立有弹性的OO系统,应对变化。

理解与实践

设计一个应用,能讲气象站提供的信息分别更新到状况布告板、气象统计布告板及预报布告板。

WeatherDataWrong.java

1 错误示范代码针对具体实现编程,导致以后增删布告板时必须修改程序。

2 根据TIP1,将改变的地方,也就是观察者(布告板),封装起来。观察者只需订阅主题(气象站)或者移除订阅既可。正确代码如下。

Subject.java

Observer.java

WeatherData.java

CurrentConditionsDisplay.java

WeatherStation.java

装饰者模式


动态地将责任附加到对象上,若要扩展功能,装饰者提供了比继承更有弹性的替代方案。

TIP4:类应该对扩展开放,对修改关闭。

理解与实践

为星巴克咖啡店设计一个订单结账系统,饮料有深培咖啡、脱因咖啡、独家调配咖啡等,每种咖啡有不同的价格,消费者还可以加各种不同价格的不同剂量的调料。

1 如果对每个类别和不同剂量调料的咖啡都设计一个类,造成了“类爆炸”,一旦有新的需要、价钱变化、一些饮料不可以加一些调料的情况就等进行大量代码的更改。

2 对此,我们可以以饮料为主题,然后在运行时用各种调料来“装饰”饮料,调用cost()方法并依赖委托(delegate)将调料价格加上去。

3 装饰者和被装饰者有共同的超类(通过继承达到类型匹配,而不是获得行为),行为来自装饰者和基础组件或与其他装饰者的组合关系。

Beverage.java

CondimentDecorator.java

Milk.java

DarkRoast.java

StarbuzzCoffee.java


工厂方法模式:定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。

抽象工厂模式:提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体的类。

TIP5:要依赖抽象,不要依赖具体类。

理解与实践

为对象村匹萨店设计应用系统,匹萨有希腊匹萨、素食匹萨,允许加入新的类型的匹萨(比如蛤蜊匹萨),允许加盟店做自己特色的匹萨,但原料和打包等可以统一管理方便监督。

PizzaStoreWrong.java

1 上例代码没有对修改封闭。如果披萨店改变所提供的比萨风味,就得去orderPizz()里面修改。

2 我们可以封装创建对象的代码,将其搬到另一个对象中,这个对象只负责生产匹萨,称此对象为“工厂”。参见下面代码(简单工厂模式),很常用。

SimplePizzaFactory.java

PizzaStore0.java

1 现在考虑加入加盟店,在推广SimpleFactory时,每个区域的加盟店需要创建各自的PizzaFactory,加入自己的改良。但其他部分,如切片、烘烤方法、盒子模具,可能会产生不统一。如果想加入多一些的质量控制,把加盟店和创建比萨捆绑到一起同时保持一定的弹性,则需要下面的框架(工厂模式)。

PizzaStore1.java

NYPizzaStore0.java

1 现在为了确保每家加盟店使用高质量的原料,我们打算建造一家生产原料的工厂,但加盟店坐落在不同的城市,原料也会有差异。对此需要用到(抽象工厂模式),见下面的框架。

PizzaStore1.java

PizzaIngredientFactory.java

NYPizzaStore1.java

NYPizzaIngredientFactory.java

Pizza.java

CheesePizza.java

Cheese.java

小结 1 工厂模式使用的是继承的方法;抽象工厂模式使用的方法是对象的组合。

2 当需要把客户代码从需要实例化的具体类中解耦,或者当目前还不知道将来需要实例化那些具体类时,使用工厂方法。使用方式:将其继承成子类,并实现其工厂方法即可。

3 当需要创建产品家族和想让相关产品集合起来时,使用抽象工厂方法。使用组合来实现,对象的创建被实现在工厂所暴露的方法中。

4 简单工厂虽然不是真正的设计模式,但仍不失为一个简单的方法,可以将客户程序从具体类解耦。

5 所有的工厂都是用来封装对象的创建。

6 依赖倒置原则,指导我们避免依赖具体的类型,而要尽量依赖抽象。


确保一个类只有一个实例,并提供一个全局访问点。

Singleton0.java

理解与实践

设计一个巧克力锅炉控制器,锅炉做的事就是把巧克力和牛奶融在一起,然后送到下一个阶段,制成巧克力棒。注意,锅炉满的时候不能填入原料,锅炉排出时必须是煮过的,满的,煮混合物是锅炉必须是满的等条件。

ChocolateBoiler.java

ChocolateController.java

1 可以想到,如果有多于一个巧克力锅炉的实例存在,就可能发生很糟糕的事情。

2 第一眼看上述程序没问题,但是如果用到多线程,很可能在第一个线程创建完实例的时候,第二个线程也已经进入创建的条件判断。

3 解决方法是,利用双重检查加锁,程序如下。

Singleton1.java


将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。

理解与实践

设计一个家电自动化的遥控系统,遥控上有七个插槽,每个插槽旁边有开和关两个按钮用来控制分配到改插槽的家电,可支持撤销操作,party 模式。其中每个家电的接口都不是统一的,比如电灯的on off,门的 up down,音效打开后初始的音量等。

1 封装的一个全新境界:将方法调用封装起来。通过封装方法,调用此运算的对象不需要关心事情是如何进行的,只需要用包装成型的方法来完成它既可。

2 我们可以类比对象村餐厅,顾客不需要知道汉堡需要怎么做,只需要下订单,厨师也不需要关心是谁要买汉堡,接到订单只需要去做,中间的传递者--服务员,所做的工作只是接受订单,然后放到订单柜台通知厨师orderup。插槽可以类比为服务员,command相当于订单,receiver--各种家电相当于厨师,取走订单相当于setcommand,execute相当于orderup,顾客相当于client。

3 命令模式,实现了请求调用者和请求接受者之间的解耦。command是接口。

4 命令模式的更多用途:队列请求、日志请求等。

Command.java

MacroCommand.java

RemoteControl.java

RemoteLoader.java

Stereo.java

StereoOnCommand.java

StereoOffCommand.java

StereoOnWithCDCommand.java

适配器模式与外观模式


将一个类的接口,转换成客户期望的另一个接口。适配器让原本不兼容的类可以合作无间。

TIP6:最少知识原则:只和你的密友谈话。即减少对象之间的交互,只留下几个“密友”。这个原则希望希望我们在设计中不要让太多的类耦合到一起,免得修改系统中的一部分。

理解与实践

Java早期使用的枚举器Enumeration有hasMoreElements()、nextElement()操作,新的集合类开始使用迭代器Iterator接口,具有hasnext()、next()、remove()操作,现在有些遗留代码暴露出枚举器接口,但我们又希望在新的代码中使用迭代器。所以请设计一个适配器,将枚举适配到迭代器。

EnumerationIterator.java

EnumerationIteratorTestDrive.java

1 适配器就像我们现实生活中的电源转换头。

2 实际上有两种适配器,类的适配器和对象适配器,类的适配器通过继承来实现,后者则通过组合来实现。

提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统更容易使用。

理解与实践

假设你组装了一套家庭影院,有爆米花机、DVD播放器、投影机、自动屏幕、立体声等等,现在你要看一部电影,但是不得一个个打开爆米花、打开屏幕、投影机、立体声。外观模式可以通过实现一个提供更合理接口的外观类很好的解决这个问题,

HomeTheaterFacade.java

HomeTheaterTestDrive.java

1 装饰者模式意图是将一个接口转成两一个接口;适配器模式的意图是不改变接口,但加入责任;外观模式意图是让接口更简单。

2 外观不只是简单的简化接口,也将客户从组件的子系统中解耦。

3 当需要使用一个现有的类而其接口并不符合你的需要时,就使用适配器;当需要简化并统一一个很大的接口或者一群复杂的接口时,使用外观。


在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

TIP7:别调用(打电话给)给我们,我们会调用(打电话给)你。和依赖导致原则不同,依赖导致原则教我们尽量避免使用具体类,而多用抽象。而好莱坞则是用在创建框架或组件上的一种技巧,好让底层组件能够被挂钩进计算中,而且又不会让高层组件依赖底层组件,两者目标都是在解耦。

理解与实践

咖啡和茶的冲泡方式非常相似,咖啡:将水煮沸、用沸水冲泡咖啡、把咖啡倒进被子、加糖和牛奶;茶:把水煮沸、用沸水侵泡茶叶、将茶倒入被子、加柠檬。设计代码,使其有少的代码重复。

1 “模板方法”定义了算法的步骤,把步骤的实现延迟到了子类。

2 可以在抽象类咖啡因饮料中使用一个模板方法,避免子类改变这个算法的顺序(模板方法);对于共同的操作,比如煮沸、倒进杯,可以在抽象类中实现,也允许子类对其覆盖(钩子);对于不同的操作,声明为抽象的方法,要求子类进行实现(抽象方法)。

3 钩子是一种被声明在抽象类中的方法,但只有空的或者默认的实现。钩子的存在可以让子类有能力对算法的不同点进行挂钩。要不要挂钩有子类自行决定。下面程序中,customerWantsCondiments()是其中的一种。钩子可以让子类实现算法中可选的部分,其另一种用法是让子类能够用机会对模板方法中某些即将发生的或刚刚发生的步骤作出发应。

4 当你的子类“必须”提供算法中某个方法或步骤的实现时,用抽象方法。如果算法的这个部分是可选的,就用钩子。

5 策略模式是封装可交换的行为,然后使用委托来决定要采用哪一个行为;工厂方法是由子类决定实例化哪个具体类;模板方法则是,由子类决定如何实现算法中的步骤。策略模式和模板方法模式都封装算法,前者用组合,后者用继承;工厂方法是模板方法的一个特殊版本。

CaffeineBeverageWithHook.java

CoffeeWithHook.java

TeaWithHook.java

迭代器与组合模式


提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。

TIP8:一个类应该只有一个引起变化的原因。类的每个责任都有改变的潜在区域。超过一个责任,意味着超过一个改变的区域。这个原则告诉我们,尽量让每个类保持单一责任。当一个模块或一个类被设计成支持一组相关的功能时,我们说其有高内聚。

理解与实践

对象村餐厅(午餐)和对象村煎饼屋(早餐)进行了合并,但两者的菜单实现不一致,一种使用的是真正的数组 MenuItem[] menuItems,一种使用的是ArrayList menuItems;现在的问题在于,女招待需要应对顾客的需要打印定制的菜单,甚至告诉顾客某个菜单项是素食的。

1 如果不封装由不同集合类型造成的遍历(变化的部分),一是违反封装,女招待需要知道每个菜单如何表达内部的菜单项集合(ArrayList用get和size方法,数组用字段和中括号);二是会有重复的代码,N个循环来遍历N个不同的菜单;三是这种方法是针对的具体实现进行的编码,而不是针对的接口。

MenuItem.java

Menu.java

PancakeHouseMenu.java

DinerMenuIterator.java

DinerMenu.java

Waitress.java

MenuTestDrive.java

1 迭代器模式把在元素间游走的责任交给了迭代器,而不是聚合对象。这不仅让聚合聚合的接口和实现变得简洁,也可以让聚合更专注在它所应该专注的事情上面(即管理对象集合),而不必理会遍历的事情。

允许你讲对象组合成树形结构来表现“整体/部分”层次结构。组合能让客户以一致的方式处理个别对象及对象组合。

理解与实践

现在考虑到顾客需要,对象村的餐厅菜单里希望能够加上一份餐后甜点的“子菜单”。

1 组合模式能让我们用树形方式创建对象的结构,树里面包含了组合Menu以及个别的对象MenuItem。我们需要一个菜单组件MenuComponent 来为叶节点MenuItem和组合结点Menu提供一个共同的接口。

2 使用组合结构,我们能把相同的操作应用在组合和个别对象上。即,在大多数情况下,我们可以忽略对象组合和个别对象之间的差别。

MenuComponent1.java

MenuItem1.java

Menu1.java

Waitress1.java

MenuTestDrive1.java

1 组合结构内的任意对象称为组件,组件可以是组合,也可以是叶节点。叶节点和组合结点的角色不同,所以有些方法可能不适合某些结点。面对这种情况,有时候你最好使抛出运行时异常。在实现组合模式时,有许多设计上的折中,你要根据需要平衡透明性和安全性。

2 上述方法是在print方法内部实用了迭代器,但是如果女招待需要遍历整个组合来挑出素食项,上述实现灵活度就不高了。我们需要使用下面的组合迭代器。

MenuMenuItem.java

CompositeIterator.java

NullIterator.java

Waitress2.java

MenuTestDrive2.java

1 策略模式是封装可互换的行为,并使用委托决定使用哪一个;观察者模式是当某个状态改变时,允许一群对象被通知到;适配器模式是改变一个或多个类的接口;外观模式是简化一群类的接口;迭代器模式是提供一个方式来遍历集合,而无须暴露集合的实现;组合模式是客户可以讲对象的集合以及个别对象一视同仁。


允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类

理解与实践

万能糖果公司需要设计一个糖果机的控制器,其工作状态如图,需要让设计能够尽量有弹性而且好维护,将来可能扩展更多的行为。

tangguo.jpg

GumballMachine0.java

GumballMachineTestDrive0.java

1 上述程序考虑周详,但很不容易进行扩展,加入一个新的状态的话会大篇幅修改程序。

2 遵循“封装变化”的原则,我们应该试着局部化每个状态的行为,这样一来,如果我们针对某个状态做个改变,就不会把其他的代码给搞乱了。

3 我们可以这样做,首先定义一个State接口。接口内糖果机每个动作都有一个对应的方法;然后为状态机中的每个状态实现状态类,这些类负责在对应的状态下进行机器的行为;最后,摆脱旧的条件代码,将动作委托到状态类。

GumballMachine.java

State.java

HasQuarterState.java

NoQuarterState.java

SoldState.java

SoldOutState.java

WinnerState.java

GumballMachineTestDrive.java

1 状态模式是讲一群行为封装在状态对象中,context的行为随时可以委托到那些状态对象中的一个。随着时间的流逝,当前状态在状态对象中游走改变。而以策略模式而言,客户通常主动指定context所要组合的策略对象是哪一个。固然策略模式能让我们具有弹性,能够在运行时改变策略,但对于某个context对象来说,通常只有一个最适当的策略对象。

2 状态模式允许一个对象基于内部状态而拥有不同的行为。状态模式和策略模式有相同的类图,但是他们的意图不同;策略模式通常会用行为或算法来配置context类。

3 策略模式是将可以互换的行为封装起来,然后使用委托的方法,决定使用哪一个行为;模板方法是由子类决定如何实现算法中的某些步骤;策略模式是将可以互换的行为封装起来,然后使用委托的方法,决定使用哪一个行为。


为另一个对象提供一个替身或占位符以控制对这个对象的访问

理解与实践

对之前我们实现的糖果机添加新的功能,使得总裁可以远程查看不同地域糖果机的不同状态

1 对此问题我们可以用远程代理,就好比“远程对象的本地代表”,你的客户所做的就像是在做远程方法调用。

2 看下面的例子,使用了Java的RMI。

GumballMachine1.java

State1.java

SoldState1.java

GumballMachineRemote1.java

GumballMonitor1.java

GumballMachineTestDrive1.java

GumballMonitorTestDrive1.java

1 远程代理可以作为另一个JVM上对象的本地代表。调用代理的方法,会被代理李永网络转发到远程执行,并且结果会通过网络返给代理,再由代理将结果转给客户。

2 上述代码首先为GumballMachine创建一个远程接口GumballMachineRemote,该口提供了一组和口译远程调用的方法,并确定接口的所有返回类型都是可序列化的。

3 服务完成后,在RMI registry中注册,好让客户可以找到他,参考GumballMachineTestDrive1中代码;然后GumbalMonitor就可以代理调用。

4 使用带领模式创建代表对象,让代表对象控制某对象的访问,被代理的对象处了像上述的远程对象外,还可以是开销大的对象或需要安全控制的对象,下面的例子分别是虚拟代理和保护代理。

理解与实践

建立一个应用程序来展现你最喜欢的CD封面,有事限于连接带宽和网络负载,下载可能需要一段时间,我们想实现在等待图像加载的时候来显示一些东西,程序也不被挂起,一旦图像加载完成,就用刚下载的图像来替代显示。

ImageComponent.java

ImageProxy.java

ImageProxyTestDrive.java

1 虚拟代理作为创建开销大的对象的代表。虚拟代理经常直到我们真正需要一个对象的时候才创建它。但对象在创建前和创建中时,由虚拟代理来扮演对象的替身,对象创建后,代理就会讲请求直接委托给对象。

2 上例中ImageProxy是控制ImageIcon的访问,代理将客户从ImageIcon解耦了。

3 下面是个保护代理的例子,使用了Java的内置功能。

理解与实践

对象村的约会配对服务,顾客不可以改变自己的HotOrNot评分,也不可以改变其他顾客的信息。

PersonBean.java

PersonBeanImpl.java

OwnerInvocationHandler.java

NonOwnerInvocationHandler.java

MatchMakingTestDrive.java

1 远程代理管理客户和远程对象之间的交互。

2 虚拟代理控制访问实例化开销大的对象。

3 保护代理基于调用者控制对象方法的访问。

4 还有很多其他代理,如缓存代理、同步代理、防火墙代理、写入时复制代理。


复合模式结合两个或以上的模式,组成一个解决方案,解决以再发生的一般性问题。

理解与实践

有一群会叫的鸭子,要实现如下功能:想要在使用鸭子的地方使用鹅、统计呱呱叫的次数、控制生产各种不同类型的鸭子、作为一个整体来管理鸭子、观察个别鸭子的行为

1 在使用鸭子的地方使用鹅,采用适配器模式。

2 统计呱呱叫的次数,采用装饰者模式。

3 控制生产各种不同类型的鸭子,采用工厂模式,使用抽象工厂创建鸭子,不会取得没有经过装饰的鸭子。

4 作为一个整体来管理鸭子,采用组合模式和迭代器模式。

5 观察个别鸭子行为,采用观察者模式,将呱呱叫学家注册为观察者,将要观察的鸭子注册为主题对象,当呱呱叫 时他就会得到通知。

6 具体实现参见程序代码。

理解与实践

设计一个MP3播放器

1 我们使用MVC来实现,MVC,被称为复合模式之王,Model-View-Controller,即模型-视图-控制器

2 如下图,模型持有所有的数据、状态和程序逻辑图可以随最新的状态而更新。使用观察者模式让模型完全独立于视图和控制器,同一个模型可以使用不同的视图,甚至可以同时使用多个视图;视图和控制器实现了经典的策略模式,视图是一个对象,可以被调整使用不同的策略,而控制器提供了策略,如果希望有不同的行为,可以直接换一个控制器;视图内部使用组合模式来管理窗口、按钮以及其他组件。

mvc.jpg

3 Web开发人员也都在适配MVC,使它符合浏览器/服务器模型。我们称这样的适配为Model2。Model2工作模型见下图。

mvc_web.jpg

与设计模式相处


是在某情景下,针对某问题的某种解决方案。

1 情境就是应用某个模式的情况;问题就是你想在某情境下达到的目标;解决方案就是你追求的一个通用的设计。

2 模式是在特定的问题和情境下,解决重复出现的问题。

1 装饰者模式:包装一个对象,以提供新的行为。

2 状态模式:封装了基于状态的行为,并使用委托在行为之间切换。

3 迭代器模式:在对象的集合之中游走,而不暴露集合的实现。

4 外观模式:简化一群类的接口。

5 策略模式:封装可以互换的行为,并使用委托在行为之间切换。

6 代理模式:包装对象,以控制对此对象的访问。

7 工厂方法模式:有子类决定要创建的具体类是哪一个。

8 适配器模式:封装对象,并提供不同的接口。

9 观察者模式:让对象能够在状态改变时被通知。

10 模板方法模式:由子类决定如何实现一个算法中的步骤。

11 组合模式:客户用一致的方式处理对象集合和单个对象。

12 单件模式:确保有且只有一个对象被创建。

13 抽象工厂模式:允许客户创建对象的家族,而无需指定它们的具体类。

14 命令模式:封装请求成为对象。

1 设计模式通常被归为三类:创建型、行为型和结构型。

2 创建型模式涉及到将对象实例化,这类模式都提供一个方法,将客户从所需要的实例化对象中解耦。

3 行为型模式都涉及到类和对象如何交互及分配职责。

4 结构型模式可以让你把类或对象组合到更大的结构中。

5 分类见下图。

moshizuzhi.jpg

TIP9:保持简单,你的目标是解决问题,而不是使用模式,让设计模式自热而然的出现在你的设计中,而不是为了使用而使用。

1 桥接模式:通过将实现和抽象放在两个不同的类层次中而使它们可以独立改变。

2 生成器模式:封装一个产品的构造过程,并允许按步骤构造。

3 责任链模式:让一个以上的对象有机会能够处理某个请求,将请求的发送者和接受者解耦。

4 蝇量模式:让某个类的一个实例能够提供很多虚拟实例,集中管理,减少运行时对象实例个数,接受内存。

5 解释器模式:为语言创建解释器。

6 中介者模式:集中相关对象之间负责的沟通和控制,通过将对象彼此解耦,增加对象的复用性。

7 备忘录模式:让对象返回之前的状态。

8 原型模式:当创建给定类的实例的过程很昂贵或复杂时,使用此模式。

9 访问者模式:当你想要为一个对象的组合增加新的能力,且封装并不重要时,用此模式。


上文只是《Head First 设计模式》的一个读书简记,方便进行知识点回顾,更详细的内容可移步原书籍。读书简记的PDF版可点击这里进行下载。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK