设计模式
设计模式:对软件设计中给定上下文中常见问题的通用的、可重用的解决方案
注重复用性,可维护性,可拓展性
可以分为三类:
- Creational patterns(创建型模式):关注对象的创建
- Structural patterns(结构型模式):解决类或对象的构成
- Behaviroal patterns(行为类模式):描述类或对象交互和分配职责的方式
Creational patterns(创建型模式)
Factory Method pattern(工厂方法模式)
为什么需要?
当client不知道创建哪个具体类的实例的时候,或者不想再client代码中指明要具体创建的实例时,用工厂方法。
原理:
定义一个用于创建对象的接口,让其子类来决定实例化哪一个类(对象的),从而使一个类(对象的)的实例化延迟到其子类。
具体实现:

优势:
- 无需将适用于特定目的的类绑定到client代码中,降低了耦合度。
- 代码只需以Product作为接口来使用,所以它可以和任何用户自定的任何ConcreteProduct工作。
- 可以简化一些复杂的创建过程。
潜在劣势:
- 用户可能需要创建Creator(PS:这是那里来的?)的子类,以便他们可以创建特定的ConcreteProduct。
- 如果客户端必须对创建者进行子类化,那么这是可以接受的,但 是如果不是这样,那么客户端必须处理另一个进化点。
使用工厂方法模式可以方便遵循OCP原则,也就是如果有了新的Product,那么只需要创建一个ConcreteFatory而不是在原来的Factory中修改。
冷知识:子类继承父类,那么它的方法抛出的异常要比父类抛出的异常要小。它的构造方法抛出的异常要比父类抛出的异常大。
Structural patterns(结构型模式)
Adapter(适配器)
为什么需要?
在实现某些功能的时候,我们将这些功能的行为抽象成了一个接口,然后已经按照这个接口实现了很多类了。这时候我们有了新的需求,这时候我们可以选择继续继承这个接口然后实现,但是我们在网上发现已经存在了相应的类(但是方法名字,参数等和现在的接口不一样),因此就会想办法利用到这个类,这时候就需要适配器的思想来实现中间的这个适配器。
原理:
可以使用extends(继承)或者delegation(委托)来实现,在适配器类使用extends就是继承lagacy类(也就是恰好存在的可以利用的类),然后再实现接口中的类方法。不过这种方法耦合度高。因此更好使用delegation。所谓delegation就是在适配器类中创建lagcacy类的对象,然后通过调用这个类来实现方法。这样在本类中不会存在多余的类方法。
优点:
- 客户端通过适配器可以透明地调用目标接口。
- 复用了现存的类,程序员不需要修改原有代码而重用现有的适配者类。
- 将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。
缺点:
- 对类适配器来说,更换适配器的实现过程比较复杂。
Decorator(装饰器)
为什么需要?
在不改变现有对象的结构情况下,动态地给该对象增加一些额外的功能。为什么不通过生成子类的方式呢?因为做不到,比如该类是final类或者即使做到了,通过继承的方式会产生大量的子类。还有就是新增的对象的功能要求可以动态地增加,也可以动态地撤销时。
原理:
它的结构如下

Component:这个接口定义被装饰物应该执行的公共操作
ConcreteComponent:这是一个具体的实现类,是装饰的起始对象,在其基础上添加功能,将通用的方法放到此对象中。
Decorator:这是一个抽象类并且是所有装饰类的基类。它实现了Component的接口所定义的方法,它也包含了一个protected的component指向被装饰的对象(也就是ConcreteComponent的具体实例)。将在构造方法中为这个component赋值。即如下:
public Decorator(Component input){
this.component = input;
}
ConcreteDecorator:这个类是实际的装饰类,只要你想,可以创建任意种装饰,它继承自Decorator类。
优点:
- 采用装饰模式扩展对象的功能比采用继承方式更加灵活。
- 可以设计出多个不同的具体装饰类,创造出多个不同行为的组合。
- 和使用继承的方式相比使用装饰类可以避免创建很多子类而导致如果需求是要求子类的功能自由组合而导致编写的子类因组合爆炸而写大量类。
缺点:
- 装饰模式增加了许多子类,如果过度使用会使程序变得很复杂。
冷知识:java中变量与函数的可见级别
| 成员类 | 包内 | 子类 | 所有类 | |
|---|---|---|---|---|
| public | T | T | T | T |
| protected | T | T | T | F |
| default | T | T | F | F |
| private | T | F | F | F |
Behaviroal patterns(行为类模式)
Strategy(整体地替换算法)
为什么需要?
有多种不同的算法实现同一个任务务,但需要client根据需要动态切换算法,而不是写死在代码里。
原理:
为不同的实现算法构造抽象接口,利用delegation,运行时动态传入client倾向的算法类实例
具体结构如下:

优点:
- 对OCP原则支持,如果有新的实现算法,可以方便拓展
- 将具体算法从Client的上下文中分隔开来
缺点:
- Client需要知道所有的算法类,并自行决定使用哪一个策略类。
Template Method(模板模式)
为什么需要?
当不同的客户端对某一类事情具有相同的算法步骤,但是每个步骤的具体实现不同的时候,使用Template Method方法比较好。
原理:
定义一个抽象父类或者接口,在其中定义通用逻辑和个步骤的抽象方法声明。在子类中进行各步骤的具体实现。具体结构如下:

优点:
- 封装不变部分,扩展可变部分。把认为不变部分的算法封装到父类中实现,而可变部分的则可以通过继承来继续扩展。
- 提取公共部分代码,便于维护。
- 行为由父类控制,子类实现
缺点:
- 算法骨架需要改变时需要修改抽象类。
- 按照设计习惯,抽象类负责声明最抽象、最一般的事物属性和方法,实现类负责完成具体的事务属性和方法,但是模板方式正好相反,子类执行的结果影响了父类的结果,会增加代码阅读的难度。
Iterator(迭代器模式)
为什么需要?
客户端希望遍历被放入 容器/集合类的一组ADT对象,无需关心容器的具体类型。也就是说,不管对象被放进哪里,都应该提供同样的遍历方式。在java中,ArrayList,LinkedList,HashSet,HashMap等都使用了这一模式。
原理:
具体结构如下:

抽象容器角色(Aggregate):负责提供创建具体迭代器角色的接口,一般是一个接口,提供一个iterator()方法,例如java中的Collection接口,List接口,Set接口等。
具体容器角色(ConcreteAggregate):就是实现抽象容器的具体实现类,比如List接口的有序列表实现ArrayList,List接口的链表实现LinkedList,Set接口的哈希列表的实现HashSet等。
抽象迭代器角色(Iterator):负责定义访问和遍历元素的接口。
具体迭代器角色(ConcreteIterator):实现迭代器接口,并要记录遍历中的当前位置。
在java中如果计划使用迭代器模式,应该让自己的聚集类实现Iterable接口,然后自己的迭代器实现Iterator接口。
优点:
- 迭代器模式使得访问一个聚合对象的内容而无需暴露它的内部表示,即迭代抽象。
- 迭代器模式为遍历不同的集合结构提供了一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作。
缺点:
- 迭代器模式在遍历的同时更改迭代器所在的集合结构会导致出现异常。所以使用foreach语句只能在对集合进行遍历,不能在遍历的同时更改集合中的元素。
冷知识:ArrayList实现了一个RandomAccess的空接口(接口中没有内容),这是一个标志接口,如果是实现了这个接口的List,那么使用for循环的方式获取数据会优于用迭代器获取数据。我也测试了一下,参考资料[5]中的大部分时间都是for快一点。不过要数据量大100倍。
Vistor(访问者模式)
为什么需要?
对特定类型的object的特定操作(visit),在运行时将二者动态绑定到一起,该操作可以灵活更改,无需更改被visit的类
原理:
具体结构如下:
Visitor抽象访问者接口:它定义了对每一个元素(Element)访问的行为,它的参数就是可以访问的元素,它的方法个数理论上来讲与元素个数(Element的实现类个数)是一样的,从这点不难看出,访问者模式要求元素类的个数不能改变(不能改变的意思是说,如果元素类的个数经常改变,则说明不适合使用访问者模式)。
ConcreteVisitor具体访问者角色:它需要给出对每一个元素类访问时所产生的具体行为。
Element抽象节点(元素)角色:它定义了一个接受访问者(accept)的方法,其意义是指,每一个元素都要可以被访问者访问。
ConcreteElement具体节点(元素)角色:它提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
ObjectStructure结构对象角色:这个便是定义当中所提到的对象结构,对象结构是一个抽象表述,具体点可以理解为一个具有容器性质或者复合对象特性的类,它会含有一组元素(Element),并且可以迭代这些元素,供访问者访问。
优点:
- 访问者模式使得易于增加新的操作 访问者使得增加依赖于复杂对象结构的构件的操作变得容易了。仅需增加一个新的访问者即可在一个对象结构上定义一个新的操作。相反, 如果每个功能都分散在多个类之上的话,定义新的操作时必须修改每一类。
- 访问者集中相关的操作而分离无关的操作 相关的行为不是分布在定义该对象结构的 各个类上,而是集中在一个访问者中。无关行为却被分别放在它们各自的访问者子类中。这 就既简化了这些元素的类,也简化了在这些访问者中定义的算法。所有与它的算法相关的数 据结构都可以被隐藏在访问者中。
缺点:
-
增加新的 ConcreteElement类很困难
Visitor模式使得难以增加新的 Element的子类。每添加一个新的 ConcreteElement都要在 Vistor中添加一个新的抽象操作,并在每一个 ConcretVisitor类中实现相应的操作。有时可以在 Visitor中提供一个默认的实现,这一实现可以被大多数的 ConcreteVisitor继承,但这与其说是一个规律还不如说是一种例外。
所以在应用访问者模式时考虑关键的问题是系统的哪个部分会经常变化,是作用于对象结构上的算法呢还是构成该结构的各个对象的类。如果老是有新的 ConcretElement类加入进来的话, Vistor类层次将变得难以维护。在这种情况下,直接在构成该结构的类中定义这些操作可能更容易一些。如果 Element类层次是稳定的,而你不断地增加操作获修改算法,访问者模式可以帮助你管理这些改动。
-
破坏封装
访问者方法假定ConcreteElement接口的功能足够强,足以让访问者进行它 们的工作。结果是,该模式常常迫使你提供访问元素内部状态的公共操作,这可能会破坏它 的封装性。
Commonality and Difference of Design Patterns(设计模式的共性与差异)
共性样式1:
只使用“继承”,不使用“delegation”
核心思路:OCP/DIP
依赖反转,客户端只依赖“抽象”,不能 依赖于“具体” 发生变化时最好是“扩展”而不是“修改”
主要使用:适配器模式和模板方法模式
共性样式2:
两棵“继承树”,两个层次的“delegation”
主要使用:整体地替换算法,迭代器模式,工厂方法模式,访问者模式
参考资料
[1] 为什么提倡面向接口编程
浙公网安备 33010602011771号