设计模式与原则


设计模式的定义:在某情境下,针对某问题的某种解决方案。但是满足此定义的方案并不一定是设计模式,设计模式要求解决方案必须是可复用的。 设计模式的作用大体上是:优化结构,消除依赖,将面向过程转为面向对象。按照功能,一般可以将设计模式分为创建型行为型结构型三大类。 本文将列举这些设计模式,并对每个设计模式进行简要描述,描述格式为:名称,定义,案例,适用性,结构,效果,应用,相关。

设计模式是工程师们从工作中总结出来的经验之谈,这些经验除了设计模式,还有一些设计原则,严格来讲,这些东西都是教条,它告诉我们只要按照规矩来,就不容易犯错。当遇到一特殊情况时,打破原则也没什么大不了的。

这些原则包括:

单一责任原则:一个类应该只有一个引起变化的原因。

里氏替换原则:子类可以扩展父类的功能,但不能改变父类原有的功能。

依赖倒置原则:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。

接口隔离原则:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。

最少知识原则:一个对象应该对其他对象保持最少的了解。

开闭原则:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

以及一些叫不上名的原则:

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

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

多用组合少用继承。

创建型

单例模式

定义

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

案例

需要对系统的注册表进行操作,如果同时存在多个注册表对象的话,将无法对并发访问或者临界值进行控制。

建立一个注册表获取类,从其静态方法中获取注册表对象,单例采用双重检查法创建。

适用性

当对象的操作目标是当前环境中的唯一资源时

结构

Singleton

效果

将可以进行并发访问控制

应用

Runtime

NumberFormat

相关

简单工厂

工厂模式

分类

工厂方法,抽象工厂,(简单工厂算工厂方法的特例)

定义

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

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

案例

现有系统需要获取一组批萨饼的调料,但是同样的调料在不同的地点会有不同,如海边的海鲜是新鲜的,而内地的海鲜是冷冻的,为了方便扩展,获取的调料不能依赖具体的地点。

解决方案:将获取调料的地点抽象,由运行时的具体地点生成一组调料。

适用性

当需要依赖运行时上下文来决定生成的产品家族时使用抽象工厂

区别

工厂方法:一个抽象产品,可以派生出多种具体产品;一个抽象工厂类,可以派生出多个具体工厂。

抽象工厂:多个抽象产品类,每个抽象产品类可以派生出多个具体产品类。一个抽象工厂类,可以派生出多个具体工厂类。每个具体工厂类可以创建多个具体产品类的实例。

结构

工厂方法

fatocymethod

抽象工厂

absfactory

效果

能轻松方便地构造对象实例,而不必关心构造对象实例的细节和依赖条件。

类数量暴增

应用

基本装箱类

Collection的iterator()

log4j

行为型

策略模式

定义

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

案例

存在一个模拟鸭子的程序,现在需要为其增加飞翔的功能,在鸭子的抽象类上增加fly()方法后,飞翔能力将传播到所有的子类中。但是,本不应该会飞的 “模型鸭” 也继承了飞翔能力,如果将飞翔能力抽象为 Flayable接口,那么随着程序的扩展,那么所有子类都必须重新实现fly()方法,如此一来将出现大量的重复代码,如果已经现存很多扩展类,修改这些类也是很大的工作量。

此时的解决方案是:将飞翔行为抽象,并提供几个通用实现,将鸭子的飞翔委托给飞翔行为,在抽象类中增加set方法注册委托,这样的改动不会影响现有的结构。

适用性

当类的某个行为随着扩展不断发生变化,而且这种变化只有有限的几种时。

结构

Strategy

效果

积极:

1.类与行为可以分别扩展,而且扩展类可以自由设置行为,甚至在运行时改变。

2.类预置了多个算法,需要要判断调用时上下文从而选择不同算法。策略模式可以消除代码中的 if lese 将选择权交给客户

消极:

1.客户端必须知道所有的策略类,并自行决定使用哪一个策略类 2.策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。

应用

ThreadPoolExecutor中的RejectedExecutionHandler

SpringSecurity中的AccessDecisionManager

相关

模板方法模式

命令模式

状态模式

模板方法

定义

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

案例

现有一个冲泡饮料的算法,泡咖啡和泡茶的步骤基本相同,但是在加调料的这一步有所区别,如何在不需要重写整个冲泡过程的前提下扩展?

解决方案:将冲泡过程中的放调料步骤抽象出来做成hook,待具体的实现者来补全算法,而不必重写整个冲泡过程。

适用性

当算法的整体骨架可被复用,只有其中某个步骤可变时

结构

templatemethod

效果

复用算法骨架

算法可变步骤中的每一个特例都要为其创建一个新类

应用

Collections.sort

相关

与策略模式不同,策略模式定义一个算法家族,算法可以互换,而模板方法定义一个算法大纲,其中个别的步骤可以有不同实现。

状态模式

定义

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

案例

需要模拟一个自动售货机程序,如果没有投币则只能投币;如果已投币则不能再投币,可以选择退币或者出货;选择出货后如果有货则出货并结束,如果断货则提示断货。

可以用大量的 if else 将如上描述写成过程话的判断语句以完成功能,然而现在需要增加一个功能,有十分之一的几率双倍出货。这样一来就需要在每个动作方法内判断当前操作者是不是幸运儿。

解决方案:将状态封装成独立的类,并将动作委托到代表当前状态的对象。省去了类中冗长的状态判断。

适用性

行为因为上下文的不同而需要随之变化

代码中包含大量与对象状态有关的条件语句:一个操作中含有庞大的多分支的条件(if else(或switch case)语句,且这些分支依赖于该对象的状态。

结构

state

效果

它将与特定状态相关的行为局部化,并且将不同状态的行为分割开来

它使得状态转换显式化

状态对象可被共享

状态模式的使用必然会增加系统类和对象的个数。

状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。

应用

java.util.Iterator

动作游戏连招

相关

是策略模式的增强版,区别在于意图,策略模式虽然也可以在运行时改变行为,但是策略模式通常有一个最适合的策略对象;而状态模式需要在多个状态对象中游走,没有所谓的最适合状态。

命令模式

定义

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

案例

通用遥控器,当目标为电视时,上下键可以是换台,调整音量;当目标为空调时,上下键可以是调温或者调整定时;而且可以方便地扩展以兼容不同电器。

解决方案,将遥控器与电器接偶,将遥控器每个按钮抽象为一个命令,由使用者提供命令,遥控器呼叫此命令,由命令自身呼叫真实目标。

适用性

当请求的真实目标不明确,或者对请求进行不明确的处理时(如队列,宏命令,日志记录等)。

结构

cmd

效果

积极的:

降低系统的耦合度:Command模式将调用操作的对象与知道如何实现该操作的对象解耦。

组合命令:你可将多个命令装配成一个组合命令,即可以比较容易地设计一个命令队列和宏命令。一般说来,组合命令是Composite模式的一个实例。

增加新的Command很容易,因为这无需改变已有的类。

可以方便地实现对请求的Undo和Redo。用栈存储操作序列,可以实现多层次撤销。

消极的:

导致某些系统有过多的具体命令类

应用

java.lang.Runnable

javax.swing.Action

游戏中的自定义键位

相关

策略模式聚焦的是对相同请求更换解决方案的灵活性;而命令模式聚焦的是对多请求变化的封装以及对相同请求不同的请求形式解决方法的可复用性

组合模式

观察者模式

定义

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

案例

需要在展板上实时显示当前温度,温度数据来自温度计。初步方案1,是定时询问温度计然后更新数据,这样就需要再开一个定时器线程,如果以后会有更多的功能需要获取温度计数据,那么线程数量将失去控制。数步方案2是当温度计数据发生变化时,直接通知这些数据请求者,但是当有新的请求者时,需要打开温度计类增加一个请求者。

结局方案:将需要获取温度计数据的请求者抽象,然后注册到温度计中形成一个列表,每当温度计数据发生变化,就依次通知这些注册的对象。

适用性

当一个抽象模型有两个方面, 其中一个方面依赖于另一方面的状态。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。

当对一个对象的改变需要同时改变其它对象 , 而不知道具体有多少对象有待改变。

当一个对象必须通知其它对象,而它又不能假定其它对象是谁。换言之 , 你不希望这些对象是紧密耦合的。

结构

Observer

效果

Observer模式允许你独立的改变目标和观察者

观察者模式可以实现表示层和数据逻辑层的分离

支持广播通信

应用

java.util.EventListener

相关

终结者模式

单例模式

迭代器模式

定义

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

结构

Iterator

效果

封装性良好,用户只需要得到迭代器就可以遍历,而对于遍历算法则不用去关心。

增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加

应用

java.util.Iterator java.util.Enumeration

相关

组合模式,常用来便利组合模式的对象图

备忘录模式

工厂方法模式,迭代器对象由实现类提供

结构型

装饰者模式

定义

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

案例

模拟一个冲泡咖啡的程序,调料种类未知,而且以后可能增减,每种调料价格不同,客户可自行选择调料份量,现在需要计算该咖啡的价格。初步方案是列出所有可能的组合,然后计算组合的价格,但是如果调料种类较多,那么组合将会爆炸,考虑到调料的份量有无限种可能,列举组合方式不可行。

解决方案:将咖啡抽象,将每种调料对应一种咖啡,如加奶的咖啡,加摩卡的咖啡等,这些调料的咖啡成分由客户自己提供。

适用性

当需要对一个对象以可选的方式增强功能时

当无法使用继承的方式增强功能时

结构

decorater

抽象组件角色(Component):定义一个对象接口,以规范准备接受附加责任的对象,

即可以给这些对象动态地添加职责。

具体组件角色(ConcreteComponent) :被装饰者,定义一个将要被装饰增加功能的类。

可以给这个类的对象添加一些职责

抽象装饰器(Decorator):维持一个指向构件Component对象的实例,

并定义一个与抽象组件角色Component接口一致的接口

具体装饰器角色(ConcreteDecorator):向组件添加职责。

效果

运行时扩展现有对象,比继承灵活。

产生很多小类

应用

java.io.BufferedInputStream(InputStream)

java.io.DataInputStream(InputStream)

java.io.BufferedOutputStream(OutputStream)

java.util.zip.ZipOutputStream(OutputStream)

java.util.Collections# checkedList Map Set SortedSet SortedMap

相关

可与工厂模式和生成器模式配合使用

Decorator模式不同于Adapter模式,因为装饰仅改变对象的职责而不改变它的接口;而适配器将给对象一个全新的接口。

Strategy模式:用一个装饰你可以改变对象的外表;而Strategy模式使得你可以改变对象的内核。这是改变对象的两种途径。

适配器模式

定义

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

案例

插座有插头型号对不上,用适配器抓换下。

适用性

想使用一个已经存在的类,而它的接口不符合需求时。

结构

类适配器

adapter1

对象适配器

adapter2

应用

java.util.Arrays# asList()

java.io.InputStreamReader(InputStream)

java.io.OutputStreamWriter(OutputStream)

相关

类似装饰者,而者都是包装对象,但是装饰者从不转换接口,只是增加职责,而适配器正是转换接口。

外观模式

代理模式

定义

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

案例

需要编写一个类来监控远程服务器上的一些数据,但是希望这个类易于使用。结局方案,将远程服务上的数据抽象成一个接口,用RMI进行通信,看起来就好象直接操纵了远程对象。

适用性

1) 远程代理(Remote Proxy)为一个位于不同的地址空间的对象提供一个本地的代理对象。这个不同的地址空间可以是在同一台主机中,也可是在另一台主机中,远程代理又叫做大使(Ambassador)

2) 虚拟代理(Virtual Proxy)根据需要创建开销很大的对象。如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。

3) 保护代理(Protection Proxy)控制对原始对象的访问。保护代理用于对象应该有不同的访问权限的时候。

4) 智能指引(Smart Reference)取代了简单的指针,它在访问对象时执行一些附加操作。

5) Copy-on-Write代理:它是虚拟代理的一种,把复制(克隆)操作延迟到只有在客户端真正需要时才执行。一般来说,对象的深克隆是一个开销较大的操作,Copy-on-Write代理可以让这个操作延迟,只有对象被用到的时候才被克隆。

结构

proxy

效果

封装实现细节,只暴露数据抽象

延迟加载

权限控制

应用

java.lang.reflect.Proxy

RMI

相关

与装饰者很类似,然而目的不同,装饰者增加行为,代理模式控制访问

外观模式

定义

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

案例

程序由多个子系统联合起来工作,而且这些子组件需要使用者自己组装,这样使用起来会很复杂,而且客户与子组件耦合较高。

解决方案,提供一套简化的接口,客户端可以直接操作此接口,而无需理会复杂的系统结构。

适用性

当需要为一个复杂子系统提供一个简单接口时。

结构

facade

效果

对客户屏蔽子系统组件,减少了客户处理的对象数目并使得子系统使用起来更加容易。

只是提供了一个访问子系统的统一入口,并不影响用户直接使用子系统类。

不能很好地限制客户使用子系统类

增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。

应用

java.lang.Class

相关

适配器模式

组合模式

定义

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

案例

菜单中有很多道菜,而且其中甜点是个子菜单,现在需要打印整个菜单。

解决方案是:将子菜单与总顶层菜单的接口统一起来,都为Menu类型,大Menu内部维护菜单列表以及子Menu。

适用性

想表示对象的部分-整体层次结构

希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。

需要注意的是,虽然通常情况下组合模式以树形结构体现,但其实不是一回事,组合模式拥有更抽象的语义,它是一种整体与部分一致的抽象。

结构

composive

效果

定义了包含基本对象和组合对象的类层次结构 基本对象可以被组合成更复杂的组合对象,而这个组合对象又可以被组合,这样不断的递归下去。

简化客户代码 客户可以一致地使用组合结构和单个对象。

使得更容易增加新类型的组件

更难以排除特定类型的组件

应用

javax.swing.JComponent# add(Component)

java.awt.Container# add(Component)

java.util.Map# putAll(Map)

java.util.List# addAll(Collection)

java.util.Set# addAll(Collection)

相关

可与迭代器模式搭配使用