Note8- 详解七大设计模式
本文结构图:
除了类本身,设计模式更强调多个类/对象之间的关系和交互过程——比接口/类复用的粒度更大
创建型模式 (Creational patterns)
工厂方法模式 (Factory Method pattern)
工厂方法也被称作虚拟构造器(Virtual Constructor)
即定义一个用于创建对象的接口,让子类类决定实例化哪个类,也就是说,工厂方法允许类将实例化推迟到子类
什么时候用?
- 当使用者不确定要创建哪个具体类的具体实例,或者不想在代码中指明要具体创建的实例时,用工厂方法
举例
设计一个 Trace
接口:
public interface Trace {
// turn on and off debugging
public void setDebug( boolean debug );
// write out a debug message
public void debug( String message );
// write out an error message
public void error( String message );
}
我们可能要设计两种实现,比如将信息直接打印到屏幕,或者输出到文件:
// Concrete product 1
public class FileTrace implements Trace {
private PrintWriter pw;
private boolean debug;
public FileTrace() throws IOException {
pw = new PrintWriter( new FileWriter( "t.log" ) );
}
public void setDebug( boolean debug ) {
this.debug = debug;
}
public void debug( String message ) {
if( debug ) {
pw.println( "DEBUG: " + message );
pw.flush();
}
}
public void error( String message ) {
pw.println( "ERROR: " + message );
pw.flush();
}
}
// Concret product 2
public class SystemTrace implements Trace {
private boolean debug;
public void setDebug( boolean debug ) {
this.debug = debug;
}
public void debug( String message ) {
if( debug )
System.out.println( "DEBUG: " + message );
}
public void error( String message ) {
System.out.println( "ERROR: " + message );
}
}
那么用户要怎么使用呢?
//... some code ...
Trace log1 = new SystemTrace();
log1.debug("entering log");
Trace log2 = new FileTrace();
log2.debug("... ");
这样做,客户端在创建对象时就要指定具体实现类,也就是说客户端的代码与实现类紧密耦合,如果有了新的具体类添加时,客户端的代码很可能也要修改
那么利用工厂方法该怎么实现呢?
先设计一个用于创建对象的接口:
interface TraceFactory {
Trace getTrace();
void otherOperation();
}
然后分别设计两个工厂类,并设计工厂方法返回对应的具体类:
public class SystemTraceFactory implements TraceFactory {
public Trace getTrace() {
... //other operations
return new SystemTrace();
}
}
public class FileTraceFactory implements TraceFactory {
public Trace getTrace() {
return new FileTrace();
}
}
客户端使用工厂方法来创建实例,这样在有新的具体产品类加入时,可以增加新的工厂类或修改已有工厂类,不会影响到客户端代码,这样使用:
//... some code ...
Trace log1 = new SystemTraceFactory().getTrace();
log1.debug("entering log");
Trace log2 = new FileTraceFactory().getTrace();
log2.debug("...");
当然也可以有另一种实现方式,根据类型决定创建哪个具体产品
interface TraceFactory {
Trace getTrace(String type);
void otherOperation();
}
public class Factory implements TraceFactory {
public getTrace(String type) {
if(type.equals("file" )
return new FileTrace();
else if (type.equals("system")
return new SystemTrace();
}
}
客户端代码:
Trace log = new Factory().getTrace("system");
log.setDebug(false);
log.debug("...");
甚至可以直接用静态工厂方法实现:
public class SystemTraceFactory{
public static Trace getTrace() {
return new SystemTrace();
}
}
public class TraceFactory {
public static Trace getTrace(String type) {
if(type.equals("file")
return new FileTrace();
else if (type.equals("system")
return new SystemTrace();
}
}
这样客户端直接用类名调用就可以得到相应的对象
相比于通过构造器(new) 创建对象:
- 静态工厂方法可具有指定的更有意义的名称
- 不必在每次调用的时候的都创建新的工厂对象
- 可以返回原返回类型的任意子类型
优点与不足分析
优点
- 不会在客户端代码中含有特定的实现类
- 代码只处理接口,所以它可以与用户定义的实现类一起使用
不足:
- 每增加一种产品就需要增加一个新的工厂子类
结构型模式 (Structural patterns)
适配器模式 (Adapter)
适配器模式意图将某个类/接口转换为用户期望的其他形式,它可以
- 解决类之间接口不兼容的问题
- 通过增加一个接口,将以存在的子类封装起来,用户只需面向接口编程,从而隐藏了具体子类
说白了,就是将旧的组件包装 (wrapper) 一下,用于新系统,下图就很直观:
适配器可以用两种形式实现:继承(Inheritance) 和委托(Delegation)
举例
比如有一个显示矩形图案的程序,LegacyRectangle
中的 display()
方法接受左上角坐标 (x, y),宽 (w),高 (h) 四个参数来实现功能
但是客户端想要通过传递左上角和右下角的坐标来实现功能
这样就造成了接口与用户期望的不协调,这种不协调就可以通过一个适配器对象利用委托机制来解决:
代码:
// Adaptor类实现抽象接口
interface Shape {
void display(int x1, int y1, int x2, int y2);
}
//具体实现方法的适配
class Rectangle implements Shape {
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) {...}
}
//对适配器编程,与LegacyRectangle隔离
class Client {
Shape shape = new Rectangle();
public display() {
shape.display(x1, y1, x2, y2);
}
}
装饰器模式 (Decorator)
装饰模式允许通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为
假设要为对象增加不同侧面的特性,那么就可以通过装饰器模式来解决,为每一个特性构造子类,通过委托机制增加到对象上
Component
接口定义装饰器执行的公共操作ConcreteComponent
起始对象,在其基础上增加功能,将通用的方法放到此对象中Decorator
抽象类使所有装饰类的基类,里面包含的成员变量component
指向了被装饰的对象ConcreteDecorator
使添加了功能的类,每个类都代表一个可以添加的特性
举例
比如,假设想要扩展 Stack
数据结构:
UndoStack
:可以撤销之前pop
或push
操作的栈SecureStack
:需要密码的栈SynchronizedStack
:序列化并发访问的栈
我们可以通过继承原始的父类来实现这些特性
但是如果需要这些特性的组合呢?比如:
SecureUndoStack
:需要密码的栈,还允许撤消之前的操作SynchronizedUndoStack
:序列化并发访问的栈,还允许撤消之前的操作
那么这个时候来怎么办呢?如果简简单单地继续往下继承:
这样不仅会使继承树变得很深,还会在子类中多次实现相同的功能,有大量的代码重复
这时就能用装饰器模式解决问题:
首先实现接口,完成最基础的 Stack
功能:
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;
public StackDecorator(Stack stack) {
this.stack = stack;
}
public void push(Item e) {
stack.push(e);
}
public Item pop() {
return stack.pop();
}
...
}
具体功能的实现类就通过委托基类实现:
public class UndoStack extends StackDecorator
{
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
}
...
}
如果客户端只需要最基础的功能:
Stack s = new ArrayStack();
如果需要某个特性,比如撤销功能:
Stack t = new UndoStack(new ArrayStack());
如果既需要撤销,又需要密码功能 SecureUndoStack
,就可以这样实现:
Stack t = new SecureStack(
new SychronizedStack(
new UndoStack(s))
)
客户端需要的多个特性通过一层一层的装饰来实现,就像一层一层的穿衣服
有同学可能对这个代码执行过程还存有疑问,我们再举一个更直观的例子:
我们设计一个冰淇凌,冰淇淋的顶部可以有多种水果:
分别设计顶层接口,基础实现,和装饰器基类:
public interface IceCream { //顶层接口
void AddTopping();
}
public class PlainIceCream implements IceCream{ //基础实现,无填加的冰激凌
@Override
public void AddTopping() {
System.out.println("Plain IceCream ready for some
toppings!");
}
}
/*装饰器基类*/
public abstract class ToppingDecorator implements IceCream{
protected final IceCream input;
public ToppingDecorator(IceCream i){
this.input = i;
}
public abstract void AddTopping(); //留给具体装饰器实现
}
然后再添加不同的特性:
public class CandyTopping extends ToppingDecorator{
public CandyTopping(IceCream i) {
super(i);
}
public void AddTopping() {
input.AddTopping(); //decorate others first
System.out.println("Candy Topping added!");
}
}
public class NutsTopping extends ToppingDecorator{
//similar to CandyTopping
}
public class PeanutTopping extends ToppingDecorator{
//similar to CandyTopping
}
当我们的客户端需要一个添加 Candy
,Nuts
,Peanut
的冰淇淋时就可以这样调用:
IceCream a = new PlainIceCream();
IceCream b = new CandyTopping(a);
IceCream c = new PeanutTopping(b);
IceCream d = new NutsTopping(c);
d.AddTopping();
当调用 d.AddTopping()
时:
- 由于
d
是子类NutsTopping
的对象,所以先会调用NutsTopping
中的AddTopping()
方法,而该方法先调用input.AddTopping()
也就是上一层的AddTopping()
方法 - 所以,
d
会先从基础实现开始依次实现特定功能
这时一个类似于递归的过程,最后输出如下:
Plain IceCream ready for some toppings!
Candy Topping added!
Peanut Topping added!
Nuts Topping added!
Decorator vs. inheritance
装饰器:
- 在运行时将特性组合起来
- 由多个对象协作组成
- 可以混合多种特性
继承:
- 在编译时将特性组合
- 继承产生一个单一、形式明确的对象
- 多继承是很难实现的
java.util.Collections
中的装饰器
Java
中的一些可变聚合类型 (List, Set, Map),可以利用 Collections
类中的装饰器添加某些特性
比如变成不可变类型
List<Trace> ts = new LinkedList<>();
List<Trace> ts2 = (List<Trace>) Collections.unmodifiableCollection(ts);
行为类模式 (Behavioral patterns)
策略模式 (Strategy)
策略模式要解决这样的问题:定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换
比如,排序有很多算法(冒泡,归并,快排)
我们可以为不同的实现算法构造抽象接口,利用委托,在运行时动态传入客户端倾向的算法类实例
优点
- 可以轻松扩展新的算法实现
- 将算法从客户端代码中分离出来
举例
在电子商务应用中需要实现各种支付方法, 客户选中希望购买的商品后需要选择一种支付方式: Paypal 或者信用卡
先设计支付策略接口:
public interface PaymentStrategy {
public void pay(int amount);
}
实现信用卡支付策略:
public class CreditCardStrategy implements PaymentStrategy {
private String name;
private String cardNumber;
private String cvv;
private String dateOfExpiry;
public CreditCardStrategy(String nm, String ccNum,
String cvv, String expiryDate){
this.name=nm;
this.cardNumber=ccNum;
this.cvv=cvv;
this.dateOfExpiry=expiryDate;
}
@Override
public void pay(int amount) {
System.out.println(amount +" paid with credit card");
}
}
实现 Paypal 支付策略:
public class PaypalStrategy implements PaymentStrategy {
private String emailId;
private String password;
public PaypalStrategy(String email, String pwd){
this.emailId=email;
this.password=pwd;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid using Paypal.");
}
}
然后利用委托调用相应算法:
public class ShoppingCart {
...
public void pay(PaymentStrategy paymentMethod){
int amount = calculateTotal();
paymentMethod.pay(amount);
}
}
客户端代码:
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
Item item1 = new Item("1234",10);
Item item2 = new Item("5678",40);
cart.addItem(item1);
cart.addItem(item2);
//pay by paypal
cart.pay(new PaypalStrategy("[email protected]", "mypwd"));
//pay by credit card
cart.pay(new CreditCardStrategy("Alice", "1234", "786", "12/18"));
}
模板模式 (Template Method)
模板方法模式在超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤
就是做事情的步骤一样,但是具体方法不同:
比如从不同类型的文件中进行读操作,对于不同文件,这个操作的步骤相同,但是具体实现不同
这时,可以把共性的步骤放在抽象类内公共实现,差异化的步骤在各个子类中实现。模板方法定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现
模板模式通过继承和重写来实现,这种模式广泛用于各种框架(frameworks) 中
举例
我们需要实现两种汽车生产的过程,显然步骤都相同,但是具体实现会有差异
先实现一个抽象类,BuildCar
中给出了生产车辆的步骤:
public abstract class CarBuilder {
protected abstract void BuildSkeleton();
protected abstract void InstallEngine();
protected abstract void InstallDoor();
// Template Method that specifies the general logic
public void BuildCar() { //通用逻辑
BuildSkeleton();
InstallEngine();
InstallDoor();
}
}
然后再为车进行具体实现:
public class PorcheBuilder extends CarBuilder {
protected void BuildSkeleton() {
System.out.println("Building Porche Skeleton");
}
protected void InstallEngine() {
System.out.println("Installing Porche Engine");
}
protected void InstallDoor() {
System.out.println("Installing Porche Door");
}
}
public class BeetleBuilder extends CarBuilder {
protected void BuildSkeleton() {
System.out.println("Building Beetle Skeleton");
}
protected void InstallEngine() {
System.out.println("Installing Beetle Engine");
}
protected void InstallDoor() {
System.out.println("Installing Beetle Door");
}
}
再比如白盒框架,框架已经将某个功能的步骤再抽象类中写好了,我们使用该框架时只需要重写对应的方法即可
迭代器模式 (Iterator)
解决这样的问题:在不暴露集合底层表现形式 (列表、 栈和树等) 的情况下遍历集合中所有的元素
结构
该模式的结构:
Iterator
接口:声明了遍历集合所需的操作: 获取下一个元素、 获取当前位置和重新开始迭代等Concrete Iterators
实现类:实现遍历集合的一种特定算法。 迭代器对象必须跟踪自身遍历的进度。 这使得多个迭代器可以相互独立地遍历同一集合Collection
接口:声明一个或多个方法来获取与集合兼容的迭代器。 注意, 返回方法的类型必须被声明为迭代器接口, 因此具体集合可以返回各种不同种类的迭代器Concrete Collections
实现类:在客户端请求迭代器时返回一个特定的具体迭代器类实体
简单来说,就是让自己的集合类实现 Iterable
接口,并实现自己的独特 Iterator
迭代器 (hasNext, next, remove),允许客户端利用这个迭代器进行显式或隐式的迭代遍历:
for (E e : collection) { … }
Iterator<E> iter = collection.iterator();
while(iter.hasNext()) { … }
举例
public class Pair<E> implements Iterable<E> {
private final E first, second;
public Pair(E f, E s) { first = f; second = s; }
public Iterator<E> iterator() {
return new PairIterator();
}
private class PairIterator implements Iterator<E> {
private boolean seenFirst = false, seenSecond = false;
public boolean hasNext() { return !seenSecond; }
public E next() {
if (!seenFirst) { seenFirst = true; return first; }
if (!seenSecond) { seenSecond = true; return second; }
throw new NoSuchElementException();
}
public void remove() {
throw new UnsupportedOperationException();
}
}
}
访问者模式 (Visitor)
它能将算法与其所作用的对象隔离开来
这种模式可以为 ADT 预留一个将来可扩展功能的接入点,外部实现的功能代码可以在不改变 ADT 本身的情况下在需要时通过委托接入 ADT
举例
想想这样一个问题,需要查看超市货架的物品
先设计数据操作的抽象接口:
/* Abstract element interface (visitable) */
public interface ItemElement {
public int accept(ShoppingCartVisitor visitor);
}
然后为每种商品具体实现:
/* Concrete element */
public class Book implements ItemElement{
private double price;
...
int accept(ShoppingCartVisitor visitor) {
// 处理数据的功能委托给visitor
visitor.visit(this);
}
}
public class Fruit implements ItemElement{
private double weight;
...
int accept(ShoppingCartVisitor visitor) {
visitor.visit(this);
}
}
设计访问者接口:
/* Abstract visitor interface */
public interface ShoppingCartVisitor {
int visit(Book book);
int visit(Fruit fruit);
}
客户端实现一种 visitor
:
public class ShoppingCartVisitorImpl implements ShoppingCartVisitor {
public int visit(Book book) {
int cost=0;
if(book.getPrice() > 50){
cost = book.getPrice()-5;
}else
cost = book.getPrice();
System.out.println("Book ISBN::"+book.getIsbnNumber() + " cost ="+cost);
return cost;
}
public int visit(Fruit fruit) {
int cost = fruit.getPricePerKg()*fruit.getWeight();
System.out.println(fruit.getName() + " cost = "+cost);
return cost;
}
}
客户端均通过 visitor
的实现类来访问数据,比如设计计算价格和的方法:
public class ShoppingCartClient {
public static void main(String[] args) {
ItemElement[] items = new ItemElement[]{
new Book(20, "1234"),new Book(100, "5678"),
new Fruit(10, 2, "Banana"), new Fruit(5, 5, "Apple")};
int total = calculatePrice(items);
System.out.println("Total Cost = "+total);
}
private static int calculatePrice(ItemElement[] items) {
ShoppingCartVisitor visitor = new ShoppingCartVisitorImpl();
int sum=0;
for(ItemElement item : items)
sum = sum + item.accept(visitor);
return sum;
}
}
这样,如果后续想要改变算法,只要更换 visitor
即可
Visitor vs. Iterator
这两者功能很相似,我们来做个对比:
- 迭代器:以遍历的方式访问集合数据而无需暴露其内部表示,将遍历这项功能委托给到外部的
iterator
对象 - 访问者:在特定 ADT 上执行某种特定操作,但该操作不在 ADT 内部实现,而是委托给独立的
visitor
对象,客户端可灵活扩展/改visitor
的操作算法,而不影响 ADT
Strategy vs. Visitor
相同点:
两者都是通过委托建立两个对象的动态联系
区别:
- Visitor 强调的是外部定义某种对 ADT 的操作,该操作与 ADT 自身关系不大(只是访问 ADT),故 ADT 内部只需要开放
accept(visitor)
即可,客户端通过它设定visitor
操作并在外部调用 - Strategy 则强调是对 ADT 内部某些要实现的功能的相应算法的灵活替换。这些算法是 ADT 功能的重要组成部分,只不过是委托到外部
strategy
类而已
设计模式的共性
共性样式:继承
特点就是只使用继承,而不使用委托
比如,适配器模式(Adaptor)
适用场合:已经有了一个类,但是其方法与目前客户端的需求不一致
根据 OCP 原则,不能修改这个类,所以扩展一个 adptor
和一个统一的接口
模板模式(Template)
适用场合:有共性的算法流程,但算法各步骤有不同的实现,典型的“将共性提升至超类型,将个性保留在子类型”
注意:如果某个步骤不需要有多种实现,直接在该抽象类里写出共性实现即可(需要时将方法设置为 final
,不允许 override
)
共性样式:继承 + 委托
特点就是有两颗继承树,有两个层次的委托
比如,策略模式(Strategy)
根据 OCP 原则,想有多个算法的实现,在右侧树里扩展子类型即可,在左侧子类型里传入不同的类型实例
迭代器模式(Iterator)
工厂方法模式(Factory Method)
左右两棵树的子类型一一对应,如果工厂方法里使用 Type
表征右侧的子类型,那么左侧的子类型只要 1 个即可
访问者模式(Visitor)
左右两侧的两棵树的子类型,基本上是一一对应,但左侧树中的不同子类型可能对应右侧树中的同一个子类型 visitor
总结
- Creational patterns
- Factory method: 创建对象而不指定确切的类
- Structural patterns
- Adapter: 通过将自己的接口包装在已有的类来使不兼容接口的类一起工作
- Decorator: 动态添加/重写类中的方法
- Behavioral patterns
- Strategy: 允许在运行时选择算法
- Template method: 将算法的骨架定义为抽象类,让子类来实现
- Iterator: 顺序访问对象的元素而不暴露其底层表示
- Visitor: 将某些方法分离出来,委派给独立的对象