七大设计模式理解和总结
设计模式一般指软件设计模式。 软件设计模式(Design pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。
软件设计模式一共有23种,在我们课程以及本篇博客中将只涉及其中的七种,它们通常采用继承或委托(delegation)的方式使得程序易于扩展。
1、Creational patterns创建型模式
1.1 Factory Method pattern工厂方法模式
通常的创建类的实例的方式是采用new关键字,这需要我们知道要创建实例的类的名字,不过在一些特定情况下,例如client不知道/不确定/不关心要创建哪个具体类的实例(回忆在Lab2中创建Graph<L>接口的实现类对象,client不关心这个Graph是怎样实现),或者不想在client代码中指明要创建的实例时,这样的方法就显得不那么合适。这时我们可以采用工厂方法来创建实例,定义一个用于创建对象的接口,让该接口的子类型来决定实例化哪一个类,从而使一个类的实例化延迟到其子类。
如何来实现的?
Item(需被创建实例相关):假设有一批item,类别相同但是有些许差异。那么我们可以构造一个Item接口,由各种具体的类ConcreteItemx实现。
Factory(工厂方法相关):为了实现工厂方法模式,我们构造一个ItemFactory接口,在这个接口中定义一个getItem方法,它返回一个Item实例,其他方法可以视需要自己决定。对于每一种具体的item即ConcreteItemx都要设计一个ConcreteItemxFactory类,实现ItemFactory接口,在重写的getItem方法中构造ConcreteItemx实例并返回。
还有另一种常用方式,就是只设计一个ItemFactory接口的实现类Factory,它的getItem方法接收字符串参数“ConcreteItemx”,根据这个参数它创建ConcreteItemx实例返回。
Client(使用者):new一个特定的ConcreteItemxFactory对象调用它的getItem方法,注意得到的实例类型应是抽象接口Item而非具体类ConcreteItemx。
静态工厂方法:既可以在ADT内部实现(回忆Graph<L>的empty方法),也可以构造单独的工厂类,相比通过构造器创建对象,静态工厂方法可具有指定的更有意义的名称,不必在每次调用的时候都创建新的工厂对象,可以返回原返回类型的任意子类型。
下面的文章中提供了更为全面的介绍以及代码示例:
2、Structural patterns结构型模式
2.1 Adapter 适配器模式
右下的这副图非常形象,有助于理解适配器模式,贴给大家。
适配器模式是用来解决类之间接口不兼容问题的一种模式,有继承式或委托式两类,类图结构如下:
解释一下,例如原本有一个方法method做加法a+b,参数是a,b两个加数,我想扩展一下,仍用这个method方法的基本实现,制作一个做减法a-b的方法,在新的方法中可以对b做一个取相反数的操作变成c (= -b)再调用method方法,这就实现了适配,怎么实现?
Item: 某个原来有的类,它拥有要被适配的method方法。
Adapter(适配器相关):设计一个抽象接口Adapter,声明适配后的新方法,它仍然叫做method,但是参数是a,c。构造它的实现类MethodAdapter,重写method方法:创造一个Item对象并调用它的method方法,传入的参数为a,-b。这是一个委托delegation的例子。
Client:创建一个Client类,它有一个成员变量是Adapter类型实例,注意要对抽象接口Adapter编程(也就是要构造Adapter类型的实例而不是声明为具体的MethodAdapter类型),在Client的method方法中通过委托调用对应Adapter的method方法。
下面的文章中提供了更为全面的介绍以及代码示例:
interface Shape{
void display(int x1,int y1,int x2,int y2);
}
class Rectangle implements Shape{
public void display(int x1,int y1,int x2,int y2) {
new LegacyRectangle().display(x1,y1,x2-x1,y2-y1);
}
}
class LegacyRectangle{
void display(int x1,int y1,int w,int h) {
//……
}
}
class Client{
Shape shape = new Rectangle();
int x1 = 1,x2 = 2,y1 = 3,y2 = 4;
public void display() {
shape.display(x1, y1, x2, y2);
}
}
2.2 Decorator 装饰器模式
装饰器模式很有特点,它通常用于对原本的ADT添加一些特性的任意组合,实际上是让ADT的方法拥有扩展的功能(而不是扩展ADT的方法种类),表现上通常是递归的、一层套一层的new。Decorator模式采用继承+委托(聚合式)实现,修饰功能主要体现在对通用操作的重写上。
我们把装饰器模式分成三部分来理解:
Item:接口,定义最基本的,被装饰物ConcreteItem拥有的、装饰物Decorator可以做修饰的操作。
ConcreteItem:起始的,要被修饰的最基本版本的对象,我们在其基础上增加功能。在这个类中,把通用的这些方法写好。
抽象类Decorator和它的子类ConcreteDecoratorX:
Decorator要implements之前定义的Item接口,它一般是个抽象类。很关键的一点是,Decorator要采用聚合委托,也就是说我们需要定义一个Item成员变量。Decorator的方法:构造方法,需要传入一个Item对象来初始化成员变量;对于接口中的通用方法,可以选择重写(委托给Item实现),也可以是抽象方法。
ConcreteDecoratorX是Decorator的子类,除了要写一个接受Item参数的构造器(调用父类构造方法)外,你可以在其中添加一些个性化的功能,最重要的是要进行对通用操作的重写——调用父类重写好的这个方法(super.method),这归根结底其实就是ConcreteItem这个基本对象的操作,然后再添加一些你想要的其他的操作,也就是对基本对象method方法的修饰;另一种情况是Decorator中你将所有通用操作都写成了抽象方法,那么就在ConcreteDecoratorX重写的通用方法中委托Item对象即可。
//Item
interface Stack {
void push(Item e);
Item pop();
}
public class ArrayStack implements Stack {
... //rep
public ArrayStack() {...}
public void push(Item e) {...}
public Item pop() { ... }
}
//Decorator
public abstract class StackDecorator implements Stack {
protected final Stack stack; //delegation
public StackDecorator(Stack stack) {
this.stack = stack;
}
public void push(Item e) {
stack.push(e);
}
public Item pop() {
return stack.pop();
}
}
//ConcreteDecorator1
public class UndoStack extends StackDecorator implements Stack {
private final UndoLog log = new UndoLog();
public UndoStack(Stack stack) {
super(stack);
}
public void push(Item e) {
super.push(e);
log.append(UndoLog.PUSH, e); //修饰的功能
}
public void undo() {
//implement decorator behaviors on stack
}
...
}
Client:一层一层地new对象,最里层是最原始的对象,越往外则添加的修饰操作越晚实现。
Stack s = new ArrayStack();
Stack us = new UndoStack(s);
Stack ss = new SecureStack(new SynchronizedStack(us));
// 一层一层装饰
这是一个装饰器模式实例的类图。
3、Behavioral patterns 行为类模式
3.1 Strategy 策略模式
另一个形象的图,在右下角,有助于大家理解。
Strategy 策略模式什么时候用?
假设某个ADT中有一个方法叫做pay,实现支付功能,我们可以想到,支付可以用多种手段实现,例如信用卡支付、现金支付、扫码支付都可以,如果我想方便地在这几种支付方式中转换该怎么来实现?这时Strategy 策略模式就派上了用场,也就是:有多种不同的算法来实现同一个任务,但需要client根据需要动态切换算法,而不是写死在代码里。解决方案是为不同的实现算法构造抽象接口,利用delegation,运行时动态传入client倾向的算法类实例。类图如下:
Strategy接口:定义有客户端需要实现的任务方法,按照上面给的例子就是pay方法。
ConcreteStrategyX:Strategy接口的实现类,是实现客户端需求任务的各种方法,按照上面给的例子就是CreditCardSrategy、PayPalStrategy等,它们可以有个性化的实现,重写pay方法。
Client:设置一个任务方法,接受一个Strategy参数,委托Strategy实现类中重写的个性化的任务方法实现不同的算法选择。按照上面的例子,使用者要传递给pay方法一个Strategy对象paymentWay,pay方法会通过paymentWay对象调用一个具体的pay方法。这是一种依赖形式的委托。
//Strategy
interface PaymentStrategy{
public void pay(int amount);
}
//ConcreteStrategyX
class CreditCardStrategy implements PaymentStrategy{
//……
@Override
public void pay(int amount) {
//……
}
}
class PaypalStrategy implements PaymentStrategy{
//……
@Override
public void pay(int amount) {
//……
}
}
//Client
class ShoppingCart{
//……
public void pay(PaymentStrategy paymentmethod) {
int amount = calculateTotal();
paymentmethod.pay(amount);
}
}
3.2 Template Method 模板模式
模板模式比较简单,它的特点是步骤是相同的,但是每一个步骤的实现可以有不同的选择,共性的步骤在抽象类内公共实现,差异化的步骤在各个子类中实现。模板方法定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。这就像一个模板一样,步骤是模板挖好的槽,我们往里填不同的东西。这样的特性和白盒框架很像,通常使用继承和重写实现模板模式。
Template抽象类:它的某个方法method包含n个步骤,抽象成n个依次调用的方法step1、step2、……stepn。这n个方法在Template抽象类中声明为抽象方法。(如有不需要多种实现的step直接在Template抽象类中写出共性实现即可,需要时将这个方法设置为final禁止子类重写)
ImplementationX子类:继承自Template抽象类,重写n个step抽象方法,为它们每一步赋予个性化的实现。
3.3 Iterator迭代器模式
客户端希望对放入容器/集合类的一组ADT对象进行遍历访问,而无需关心容器的具体类型,也就是说,不管对象被放进哪里,都应该提供同样的遍历方式,这时采用Iterator Pattern。
我将迭代器模式分为两部分理解:
ConcreteAggregate:你希望能够提供统一遍历机制的ADT,它要实现Iterable<E>接口,E应为需要被遍历的类型,因此必须重写iterator方法(返回一个Iterator对象)。
CertainIterator:某个你自定义的迭代器类,它要实现Iterator<E>接口,E应为需要被遍历的类型,因此必须重写hasNext、next方法。我一般会将CertainIterator类作为ConcreteAggregate的内部类,这样方便访问ConcreteAggregate的成员变量,为了重写那两个方法我还会在ConcreteAggregate增加一个int值index。这里我研究不深,建议大家看看这篇文章深入了解。
3.4 Visitor 访问者模式
本质上是将数据和作用于数据上的某种/些特定操作分离开来,即对特定类型object的特定操作(visit),在运行时将二者动态绑定到一起,该操作可以灵活更改,无需更改被visit的类。为ADT预留一个将来可扩展功能的“接入点”,外部实现的功能代码可以在不改变ADT本身的情况下在需要时通过delegation接入ADT。
我将Visitor模式分为两大部分理解:
Item接口和它的实现类ConcreteItemX:在Item接口中要定义一个accept方法,接收一个Visitor类型的对象参数visitor,这个accept方法就是让某个ConcreteItemX可以被外部访问到的一个接入点。在ConcreteItemX类中重写accept方法,只需要一行代码,就是委派visitor调用visit方法,将需被访问对象也就是自己(this)传递过去。这是一种依赖委托。
Visitor接口和它的实现类ConcreteVisitorX:在Visitor接口中定义有重载的visit方法,接收一个ConcreteItemX对象参数,有多少种ConcreteItemX就重载多少个visit方法。在ConcreteVisitorX中,重写这些visit方法,实现对各种ConcreteItemX的不同操作。
我们发现一件很有意思的事情,在Visitor模式中,Visitor对象被传递到Item对象的accept方法中用作dependency delegation,而Item对象也通过visit方法被传递到了ConcreteVisitorX,这是Visitor模式的一个特点,也是与Strategy模式的一点差别。Visitor模式与Strategy模式都是通过delegation建立两个对象的动态联系,但是Visitor强调是的外部定义某种对ADT的操作,该操作于ADT自身关系不大(只是访问ADT),故ADT内部只需要开放accept(visitor)即可,client通过它设定visitor操作并在外部调用;而Strategy则强调是对ADT内部某些要实现的功能的相应算法的灵活替换。这些算法是ADT功能的重要组成部分,只不过是delegate到外部strategy类而已。
总结起来,就是visitor是站在外部client的角度,灵活增加对ADT的各种不同操作(哪怕ADT没实现该操作),strategy则是站在内部ADT的角度,灵活变化对其内部功能的不同配置。
这篇文章提供了代码示例:
4、共性样式
4.1 共性样式1
典型例子:Adapter、Template
4.2 共性样式2
典型例子:Strategy、Iterator、Factory Method、Visitor