设计模式:针对软件设计中给定上下文中常见问题的一种通用的、可重用的解决方案。
除了类本身,设计模式更强调多个类/对象之间的关系和交互过程—比接口/类复用的粒度更大。
设计模式分类:
- 创建型模式:关注对象创建的过程
- 结构型模式:处理类或对象的组合
- 行为类模式:描述类或对象交互和分配责任的方式
创建型模式
工厂方法模式
也被称为“虚拟构造器”。
意图:
-
定义用于创建对象的接口,但让子类决定要实例化哪个类。
-
工厂方法允许类将实例化推迟到子类。
当client不知道要创建哪个具体类的实例,或者不想在client代码中指明要具体创建的实例时,用工厂方法。定义一个用于创建对象的接口,让其子类来决定实例化哪一个类,从而使一个类的实例化延迟到其子类。
工厂方法:
-
定义工厂类接口
-
定义工厂类
-
客户端使用时构造工厂类,然后利用工厂类生成对象
(多一层封装,便于将生成对象的具体逻辑封装)
静态工厂方法
既可以在ADT内部实现,也可以构造单独的工厂类
使用静态方法,从而避免对工厂类的构造。
工厂方法优点:
- 无需将特定于应用程的类绑定到代码中。
- 代码仅处理产品接口,因此它适合任何用户定义的具体类
工厂方法潜在缺点:
- 客户可能必须构造工厂类,这样他们才可以创建一个特定的对象。
- 如果客户端必须对工厂类进行子类化,那么这是可以接受的,但如果不是,那么客户端就必须处理另一个演化点。
工厂方法满足OCP原则。
结构型模式
适配器模式
意图:将一个类/接口转换为客户端希望获得的另一个类/接口。
适配器模式允许类因为不兼容的接口而不能一起工作。
通过增加一个接口,将已存在的子类封装起来,client面向接口编程,从而隐藏了具体子类。
将旧组件重用到新系统(也称为“包装器”)。
适配器模式:
- 新建一个接口,接口供客户端调用
- 新建一个类实现接口,类将方法的实现委托给了原来的不匹配的类。
增加了一层调用,处理参数不匹配的情况。
装饰器模式
对同一个类有很多不同的子类继承它,每个子类需要实现不同的特性
每个子类的功能是功能集合的任意组合:比如对于A类有B1,B2,B3…等等子类,B1需要实现功能1、2、3,B2需要实现功能2,B3需要实现功能1、3…
继承的限制:如果为每一个组合创建一个类并定义一系列继承关系——
- 组合爆炸 2. 大量代码重复
装饰器模式
需求:需要对单个对象进行任意的或动态的可组合的扩展。
方法: 对每一个特性构造子类,通过委派机制增加到对象上
好处:比静态继承更灵活;可定制的、内聚的扩展
装饰器模式使用子类型和委托
装饰器模式:
-
创建一个接口以及基础类,基础类实现接口,在基础类里面实现所有基础功能
-
创建一个装饰器A,内部保有一个final的接口类型的对象,在装饰器的构造函数里传入并绑定。装饰器A实现接口,方法的基础逻辑委托给保有的对象实现。可以扩展方法逻辑或者添加新方法。
-
如果客户端需要一个具有多种特性的object,则通过一层一层的装饰来实现,例:
Stack t = new SecureStack(new SynchronizedStack(new UndoStack(s));
外层方法要具有内层方法并将实现委派给内层。
装饰和继承
- 装饰器在运行时组成功能,继承会在编译时组成特性
- 装饰器由多个协作对象组成,继承将生成一个类型清晰的对象
- 可以混合和匹配多种装饰器,但继承只能是单重的
行为类模式
策略模式
需求:有多种不同的算法来实现同一个任务,但需要client根据需要动态切换算法,而不是写死在代码里。例:对客户列表进行排序(气泡排序、合并排序、快速排序)
解决方法:为不同的实现算法构造抽象接口,利用delegation,运行时动态传入client倾向的算法类实例
好处:易于扩展新的算法实现;将算法与客户端上下文分离
策略模式:
- 定义策略接口,执行逻辑的类通过的调用接口定义的策略方法
- 为每一种可行方法定义一个方法类并继承策略接口
- 客户端通过动态传入方法类来实现对不同方法的调用
模板模式
问题:一些客户共享相同的算法,但在具体细节上有所不同,即,一个算法由可定制的部分和不变的部分组成。公共步骤不应该在子类中被重复,而是需要被重用。做事情的一部分方法一样,但某一部分方法不同。
例:执行一个测试用例的测试套件——打开、阅读、撰写不同类型的文件。
解决方法:定义抽象类,共性的步骤在抽象类内公共实现,差异化的步骤在各个子类中实现。子类为上述每个步骤提供了不同的实现。
模板方法和策略方法:
模板方法模式使用继承+重写方法来改变算法,适应不同场景需求;而策略模式则使用委托来改变算法(接口和特殊多态性)。
模板方法被广泛地应用于框架中。该框架实现了算法的不变量。
客户端定制为算法提供了专门的步骤。原则:“Don’t call us, we’ll call you”。
模板模式:
- 定义模板类(抽象类)。确定通用逻辑,并直接实现在模板类内部。所有非通用逻辑采用抽象方法的方式定义。
- 为每一种场景编写具体类,继承模板类并实现抽象方法。
- 执行逻辑时传入具体类,并直接调用模板类方法即可。
迭代器模式
需求:客户端需要统一的策略来访问容器中的所有元素,而且独立于容器类型。也就是说,不管对象被放进哪里,都应该提供同样的遍历方式。
解决方法:采用迭代器模式,定义迭代器并支持按特定方式遍历容器内的元素。
好处:隐藏了底层容器的内部实现;支持具有统一接口的多种遍历策略;易于更改容器类型;方便了程序各部分之间的通信
迭代器结构:
- 抽象的迭代器(迭代器接口)类定义了遍历协议
- 每个容器类的具体迭代器(实现迭代器接口的方法)
- 容器对象构造具体迭代器对象
- 容器对象保留对具体迭代器对象的引用
Iterable接口:实现该接口的集合对象是可迭代遍历的
public interface Iterable<T> {
...
Iterator<T> iterator();
}
Iterator接口:迭代器
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
迭代器模式:
-
构造自己的独特Iterator迭代器并实现迭代器接口(hasNext, next, remove)
-
使自己的集合类实现Iterable接口,在
iterator()
方法中返回自己构造的迭代器类型 -
允许客户端利用这个迭代器进行显式或隐式的迭代遍历:
隐式遍历:
for (E e : collection) { … }
显式遍历:
Iterator<E> iter = collection.iterator(); while(iter.hasNext()) { … }
访问者模式
访问者模式:对特定类型的object的特定操作(visit),在运行时将二者动态绑定到一起,该操作可以灵活更改,无需更改被visit的类。
-
访问者模式实际上所做的是创建一个使用其他类中的数据的外部类。
-
如果操作的逻辑发生了变化,那么我们只需要在访问者实现中进行更改,而不是在所有的类中进行更改。
本质上:将数据和作用于数据上的某种特定操作分离开来。
为ADT预留一个将来可扩展功能的“接入点”,外部实现的功能代码可以在不改变ADT本身的情况下通过delegation接入ADT。
访问者模式:
-
定义Visiter接口以及相应的具体访问逻辑
-
为ADT定义
accept(visitor)
方法,内部调用传入的visitor的访问方法,传入自身
访问者模式 & 策略模式
-
二者都是通过delegation建立两个对象的动态联系
-
但是Visitor强调是的外部定义某种对ADT的操作,该操作于ADT自身关系不大(只是访问ADT),故ADT内部只需要开放accept(visitor)即可,client通过它设定visitor操作并在外部调用。
-
而Strategy则强调是对ADT内部某些要实现的功能的相应算法的灵活替换。这些算法是ADT功能的重要组成部分,只不过是delegate到外部strategy类而已。
区别:visitor是站在外部client的角度,灵活增加对ADT的各种不同操作(哪怕ADT没实现该操作),strategy则是站在内部ADT的角度,灵活变化对其内部功能的不同配置。
设计模式的共性与差异性
设计模式的对比:共性样式1
只使用“继承”,不使用委托。
核心思路:OCP/DIP(依赖反转,客户端只依赖“抽象”,不能依赖于“具体”)
发生变化时最好是“扩展”而不是“修改”。
例:Adapter,Template
设计模式的对比:共性样式2
两棵“继承树”,两个层次的委托。
例:Strategy,Iterator,Factory Method,Visitor