从场景入手学设计模式

By | 13 1 月, 2019

这两周看完了博览网的设计模式的课程,有很多感触。平时工作中代码写的很熟练,很多时候看着代码,觉得这里应该这么写才对,那样写就会觉得很别扭,但一直没有向上抽象出一套模式或方法论。
这次看设计模式的过程中,一直有一种“这个好像说的就是那个我一直觉得写得很别扭的地方”、“对对对!就是这种套路!”的感觉。这篇文章是看完这套教程后,我自己的一点总结和归纳,希望也对你有一些帮助。
单纯一个个的总结模式似乎并不符合常规认知,我还是喜欢从实际场景入手,来一个个对照总结。看看有没有哪个场景是正好戳中你的?

场景:组件协作

这一类的设计模式是解决多个组件之间沟通和协作问题的。考虑下面的场景:

  • 一个业务执行流程中的几个步骤是固定的,但是每个步骤在不同场景下的解决方式(执行代码)不同,导致需要为每个场景写很多重复的公共流程控制代码
  • 一个计算或业务处理过程中,有很多种不同的情况,写了很多 if else 或者 switch,来判定当前是哪个情况,之后针对这个情况做对应的运算或处理;假如后面突然又多了一种情况,特别是已有的代码是别人写的,我还不敢改,算了,我直接加个 else if 吧,结果就造成了 else if 的无限膨胀,可维护性持续下降
  • 我自己写的这个组件是一个基础组件,有很多其他人依赖我这个组件,并且需要自己在状态变化的时候能够通知到这些人;但问题是,我写这个组件的时候,压根就不知道有谁会来依赖我自己的状态变化

上面三个场景其实分别对应了三个设计模式,分别是 Template Method / Strategy Mode / Observer Mode,下面来一个个的说明。

Template Method

问题:
对于某一个业务流程,它常常有稳定的整体操作结构,但各个子步骤却又很多改变的需求,或者由于固有的原因而无法和任务的整体结构同时实现。如何在确定稳定操作结构的前提下,来灵活应对各个子步骤的变化?
实现 & 解析:
这里我们需要明确,这一个业务流程的稳定的操作流程是什么?抽象层次上应该如何控制整个流程?
明确了这一点,就可以写一个抽象类出来,并实现那些已经确定稳定的方法,同时在这个抽象类中实现业务流程的控制方法。对于那些需要变化的实现方法则设置为 abstruct,由子类来实现。
这里其实是避免依赖腐烂掉的一个很好用的方法,高层组件定义了计算的框架,但是它们依赖于几个低层组件的实现。如果直接代码上写死去依赖低层组件的实现,那就腐烂掉了,因为会有很多种不同的低层组件实现,并且你在写高层组件框架的时候不知道会有哪些低层组件。
最好的方案是在计算框架中提供钩子,好让低层组件能够直接挂上来并参与计算过程。这个过程就可以用方法的多态调用来实现,也就是 早绑定 变成 晚绑定
示例代码:

abstract class Library {
    public void run() {
        step1();
        if (step2()) {
            step3();
        }
    }
    protected void step1() {}
    protected void step3() {}
    abstract boolean step2();
}
class Application extends Library {
    @Override
    protected boolean step2() {
        return true;
    }
    public static void main(String[] args) {
        Library lib = new Application();
        lib.run();
    }
}

上面代码中,Library 是稳定的业务流程定义,Application 是实际的不同业务实现方法的调用及实现方,整个稳定的流程控制都在 Library 中的 run 方法中,稳定的 step1 和 step3 方法也一并实现了,但是 step2 是根据实际场景而不停变化的,所以 step2 设置为 abstruct,由子类来实现。
子类继承自 Library,同时只要实现了 step2 就可以跑起来整个业务流程,最大限度的提升了复用性,减少了重复代码。
关键点:

  • 多态调用
  • Don’t call me, I’ll call you.

Strategy

问题:
在开发过程中,很多对象使用的算法或处理策略多种多样,需要写很多 if else 或者 switch 来解决,慢慢的这一块的处理逻辑就会复杂且难以维护,如何在运行时根据需要 透明 的更改对象自身的处理逻辑,以及将处理逻辑与对象自身解耦?
实现 & 解析:
这里其实是设计原则中 “开闭原则” 的一个体现。开闭原则是什么?对扩展开放,对修改封闭。也就是说,当需求发生变化的时候,我们可以通过增加一个类的实现来解决,而不是说我去修改已有的代码来支持这个新的需求。
这里的变化是什么?是对象所使用的处理策略。那么这就是我们需要封装起来的部分,定义一个接口来屏蔽不同的处理策略差异,使得上下游的代码可以通过调用这个接口,来独立于变化的对象处理策略而运行。这样,真正的执行策略是在什么时候确定呢?运行时。这就完成了代码的解耦合。
示例代码:

abstract class TaxStrategy {
    public abstract double Calculate(String context);
}
class FRTax extends TaxStrategy {
    @Override
    public double Calculate(String context) {
        return 0;
    }
}
class CNTax extends TaxStrategy {
    @Override
    public double Calculate(String context) {
        return 0;
    }
}
class SalesOrder {
    TaxStrategy strategy;
    public SalesOrder(TaxStrategy strategy) {
        this.strategy = strategy;
    }
    public double CalculateTax() {
        return strategy.Calculate("xxx");
    }
}

每个国家的税都有不同的计算方法,如果用了 switch 那就变成了每增加一个新国家,那就要多一个 case 去解决。
用 Strategy Mode 修改后,就变成了每多一个国家,就增加一个对应国家的税计算的实现类,已有的代码骨架则不需要修改,保持了稳定性。
关键点:

  • 多态调用
  • 碰到过多的 if else 的时候就想一下,是不是可以通过 Strategy Mode 来解决
  • 消除条件判断语句,就是在解耦合。 含有很多条件判断语句的代码一般都是 变化点

比较:
Strategy Mode 可以和 Template Method 设计模式比较一下,可以发现其中的不同之处:

  • Strategy Mode 定义了一系列的算法,并且这些算法可以相互替换,而且因为使用接口的相同,所以调用方可以轻易地使用不同的算法
  • Template Method 则是定义了一个算法的大纲,由子类定义其中一些步骤的具体实现,整个算法的结构是保持不变的
  • Strategy Mode 是通过 组合 对象来完成目的(参见上面示例代码中的 SalesOrder),Template Method 则是通过 继承 来完成目的(参见上面示例代码中的 Application)

Observer

问题:
开发过程中,我们需要为某些对象建立一种通知依赖的关系,也就是说,某个对象发生了变化,所有的依赖对象都能够得到通知。而且很多时候,在我们写这个被依赖对象的时候,我们是不知道哪些对象会依赖于它。那如何弱化这种依赖关系并可以轻松抵御变化?
实现 & 解析:
对于原始对象而言,这里需要明确变化点是什么?不停变动且未知的需要通知的其他对象。那么如何抵御变化?定义一个抽象类,并写清楚自己支持哪几种通知接口。每一个希望接收到通知的对象都必须实现该抽象类,然后注册到自己(原始对象)身上。常用的方式直接写个 List 就 OK。
示例代码:

public class Observer {
    private List<Watcher> listeners;
    public void addListener(Watcher watcher) {
        if (listeners == null) {
            listeners = new ArrayList<>();
        }
        listeners.add(watcher);
    }
    public void somethingChanged() {
        if (listeners != null) {
            for (Watcher watcher : listeners) {
                watcher.changed();
            }
        }
    }
}
abstract class Watcher {
    abstract void changed();
}
class Application {
    public void usage() {
        Observer observer = new Observer();
        observer.addListener(new Watcher() {
            @Override
            void changed() {
                // ...
            }
        });
        observer.somethingChanged();
    }
}

上面的代码中,Observer 类和 Watcher 抽象类是稳定的,并且 observer 也是稳定的,调用方可以不断地 addListenter 来增加通知依赖关系而不改变已有代码。
关键点:

  • 紧耦合 -> 松耦合
  • Observer Mode 的应用,可以使得我们独立的改变原始对象和众多的观察者对象,从而解除它们之间的耦合
  • 观察者对象可以自己决定是不是需要订阅原始对象的通知,而原始对象对此则一无所知

场景:单一职责

这一类设计模式是解决组件的责任划分问题的。有点抽象?考虑下面的场景:

  • 现在有一个对象,比如是水。我们有三种类型的水(纯净水/矿泉水/崂山白花蛇草水),有三种不同型号的杯子(S/M/L),有两种不同包装(结婚礼盒/塑料袋子)。现在我们需要一个“结婚礼盒装的M号杯子的崂山白花蛇草水”和一个“塑料袋子装的S号杯子的纯净水”,想到了什么?继承?那为了满足将来所有可能的需求,你要写 1+3*3*2=19 个类,是不是想死?注意这个问题隐含着一个条件,存在先后关系,比如你不能获取一个“M号杯子装的结婚礼盒的纯净水”,但你可以获取一个“塑料袋子装的水”(假设塑料袋子和结婚礼盒都不漏水)
  • 现在有另外一个对象,比如是一个日志类。写入方式有三种(控制台输出/写入文件/上报到指定API),同时支持三种不同形式的日志格式(JSON/XML/YAML)。现在我需要一个“朝控制台输出且格式为JSON的日志类”,或者一个“写入文件且格式为YAML的日志类”,想到了什么?还是继承?那你又要写 1+3*3=10 个类了

上面两个场景分别对应了两个设计模式,分别是 Decorator 和 Bridge 模式。下面来一一说明。

Decorator

问题:
我们经常会遇到需要给某个对象增加一些额外的功能和职责,通常我们会用继承的方式来实现,但像上面第一个场景中所描述那样,我们会发现类的数量会急剧膨胀,很快我们就会被类淹没。
实现 & 解析:
有一个设计原则是我们经常提到的:组合优于继承。 这里是不是看到了一些端倪?想一下如果我们用正常继承的方式写,问题出现在哪里?super 的调用上。当你写一个明确的类的时候,你的父类的类型是确定的,也就是说,super 是静态的。有一个重构的技法是:静态转动态。我们可以依赖多态来帮我们做到这一点。
继承错了吗?没有。在上面水的例子中,“纯净水”是水,“M号杯子的纯净水”是水,“塑料袋子装的S号杯子的纯净水”也是水,这就是 is-a 的关系,一个非常明显的继承。
但同时可以看到,我们一直在给“水”这个对象附加额外的能力和属性,换一种说法,对于每一个维度,给我一个 “水” 类型的对象,我就可以在它的基础上包装我的能力。这是什么?has-a,组合。对于每一个维度代表的类型,我都可以拥有“水”这个类型的对象(组合),比如我们把这个类变量的名字叫 water。这样,虽然我们还是在继承父类“水”,但上面我们提到的 super.xxx() 的调用是不是就可以直接变成我们组合的对象 water.xxx() 的调用了?而具体调用哪个类型的 xxx 方法呢?这就变成运行时决定了。所以通过组合,我们完成了静态转动态。
在 Decorator 模式中,即满足 is-a 关系,是继承,又满足 has-a 关系,是组合。继承接口协议,组合复用实现
通过 Decorator 模式,原先需要 1+3*3*2 的实现类缩减成了 1+3+3+2,是不是很完美?
示例代码:

abstract class Water {
    public abstract void print();
}
class PurifiedWater extends Water {
    @Override
    public void print() {
        System.out.print("purified-water");
    }
}
class MineralWater extends Water {
    @Override
    public void print() {
        System.out.print("mineral-water");
    }
}
abstract class DecoratorWater extends Water {
    protected Water water;
    public DecoratorWater(Water water) {
        this.water = water;
    }
}
class MGlassWater extends DecoratorWater {
    public MGlassWater(Water water) {
        super(water);
    }
    @Override
    public void print() {
        System.out.print("M-glass ");
        water.print();
    }
}
class LGlassWater extends DecoratorWater {
    public LGlassWater(Water water) {
        super(water);
    }
    @Override
    public void print() {
        System.out.print("L-glass ");
        water.print();
    }
}
class GiftBoxWater extends DecoratorWater {
    public GiftBoxWater(Water water) {
        super(water);
    }
    @Override
    public void print() {
        System.out.print("gift-box ");
        water.print();
        System.out.println();
    }
}
class PlasticBagWater extends DecoratorWater {
    public PlasticBagWater(Water water) {
        super(water);
    }
    @Override
    public void print() {
        System.out.print("plastic-bag ");
        water.print();
        System.out.println();
    }
}
class Client {
    public static void main(String args[]) {
        new PlasticBagWater(new LGlassWater(new PurifiedWater())).print();
        new PlasticBagWater(new PurifiedWater()).print();
        new GiftBoxWater(new MGlassWater(new MineralWater())).print();
    }
}

这里省略了一些,看个意思就好。输出如下:

plastic-bag L-glass purified-water
plastic-bag purified-water
gift-box M-glass mineral-water

关键点:

  • 通过组合来实现运行时的多态调用
  • 继承接口协议,组合复用实现
  • 看到一个非常好的描述,这里抄过来,非常巧妙的描述了 Decorator 模式的精髓:是你还有你,一切拜托你。
  • Decorator 模式一般是需要满足上下级关系,即依赖关系是一棵树,某个节点的类中的对象可以容纳该节点上级层级中的任意类型的对象,但不能是同级和下级(这里根据实际情况,但如果发现也可以是同级和下级,说明并不严格满足依赖关系,那么可能更合适的设计模式是下面的 Bridge 模式,即进行维度切分并组合)

Bridge

问题:
对于上面提到的日志类这个场景,我们发现像这种类型的对象,它具有两个或多个变化的维度,那么应该如何做才能避免又出现普通继承时候的子类数量爆炸?
实现 & 解析:
Bridge 模式和 Decorator 模式非常相似,像上面日志类的场景,我们发现这两个维度其实是没有关联的,如果强行放入一个类里面,那么子类就要处理所有可能的维度关系,就爆炸了。既然两个维度没有关系,那么我们不就可以把它们拆开么?
比如写入方式我们单独抽出一个抽象类,文件格式我们再单独抽出一个抽象类,分别都定义好接口,之后要做的事情就是针对每个单独对应的类型写对应的实现。
现在我们有了 Log 基类,三个不同写入方式的日志实现类(ConsoleLog/FileLog/ApiLog,均实现 WritingLog 接口),以及三个不同文件格式的日志实现类(JsonLog/XmlLog/YamlLog,均实现 FormatLog 接口)。那接下来干啥?组合啊!
继承一下 Log 基类,然后组合 WritingLog 和 FormatLog 类型的实例对象,完事!
是不是又从 1+3*3 变成了 1+3+3 了?
示例代码:

interface WritingLog {
    void write();
}
interface FormatLog {
    void format();
}
class Log {
    protected WritingLog writingLog;
    protected FormatLog formatLog;
    public Log(WritingLog writingLog, FormatLog formatLog) {
        this.writingLog = writingLog;
        this.formatLog = formatLog;
    }
    public void print() {
        writingLog.write();
        formatLog.format();
        System.out.println();
    }
}
class ConsoleLog implements WritingLog {
    @Override
    public void write() {
        System.out.print(" console-writing ");
    }
}
class FileLog implements WritingLog {
    @Override
    public void write() {
        System.out.print(" file-writing ");
    }
}
class JsonLog implements FormatLog {
    @Override
    public void format() {
        System.out.print(" json-format ");
    }
}
class YamlLog implements FormatLog {
    @Override
    public void format() {
        System.out.print(" yaml-format ");
    }
}
class Client {
    public static void main(String args[]) {
        new Log(new ConsoleLog(), new YamlLog()).print();
        new Log(new FileLog(), new JsonLog()).print();
    }
}

关键点:

  • 仍然是使用组合来代替继承,通过多态来实现类型的运行时确定
  • Bridge 和 Decorator 模式其实可以混合使用,达到维度划分 + 功能包装的能力
  • 这里对比一下 Decorator 和 Bridge 模式,防止大家弄混:
    • Decorator 模式通常用于对已有对象的功能增强,是叠加的效果。单个对象自身而言非常稳定
    • Bridge 模式通常用于对象自身会沿着多个互不相关的维度进行变化。单个对象自身而言非常不稳定

场景:对象创建

这一类设计模式是解决如何创建对象自身的问题。考虑下面的场景:

  • 每次新创建对象都需要 new,但是因为需求的变化,我自己需要创建的对象也经常发生变化,如何能在需求发生变化的时候已有的创建对象的代码不动呢?
  • 假如现在需要创建不止一个对象,这些对象之间有相互依赖的关系,而且需求的变化会导致出现更多相互依赖的对象的创建,这个时候又要怎么做?
  • 有的时候需要创建一些复杂的对象,同时对于这些对象的操作会非常剧烈的影响这些对象的内部状态,但是这些对象拥有稳定一致的接口

上面三个场景分别对应了三个设计模式,分别是 Factory Method / Abstruct Factory / Prototype 模式。下面来一一说明。

Factory Method

问题:
当你在代码中想创建一个抽象类的实例,但是又不想在编译时就明确写死依赖某个具体的实现类的时候,就是需要 Factory Method 的时候。
实现 && 解析:
在 Java 中,创建对象的方式除了 new,反射,剩下的就是通过方法来返回对象了。既然不想在编译时就确定使用哪个具体的实现类,那么就要定义一个用于创建对象的接口,让子类决定实例化哪一个类。
简要的说,Factory Method 使得一个类的实例化延迟到子类。
下面的示例代码展示了一个抽象类 ISplitter 拥有多个具体的实现类,然后在 Activity 中并不想依赖具体的实现类。于是实现了一个 SplitterFactory 的抽象工厂类,并分别为每个具体的实现类实现了对应的工厂创建类,于是在 Activity 中,就可以不依赖于具体的实现类,而是依赖一个 SplitterFactory 的对象,由外界决定传入什么类型的工厂类。这样就完成了一个“多态”的 new,将变化丢给了运行时。
示例代码:

abstract class ISplitter {
    public abstract void split();
}
class BinarySplitter extends ISplitter {
    @Override
    public void split() {}
}
class PictureSplitter extends ISplitter {
    @Override
    public void split() {}
}
abstract class SplitterFactory {
    // 工厂方法
    public abstract ISplitter createSplitter();
}
class BinarySplitterFactory extends SplitterFactory {
    @Override
    public ISplitter createSplitter() {
        return new BinarySplitter();
    }
}
class PictureSplitterFactory extends SplitterFactory {
    @Override
    public ISplitter createSplitter() {
        return new PictureSplitter();
    }
}
class Activity {
    private SplitterFactory factory;
    public Activity(SplitterFactory factory) {
        this.factory = factory;
    }
    public void run() {
        ISplitter splitter = factory.createSplitter();
        splitter.split();
    }
}

关键点:

  • 用于隔离类对象的使用者和具体类型之间的耦合关系,一个经常变化的具体类型会导致软件的脆弱
  • 解决的问题是单个对象的需求变化,缺点在于要求创建方法/参数相同
  • 依赖倒置原则:尽可能依赖抽象类而不是具体的类

Abstruct Factory

问题:
当面临“一系列相互依赖的对象”的创建工作的时候,就是使用 Abstruct Factory 方法的时候。
实现 && 解析:
Abstruct Factory 和 Factory Method 很像,或者说它们其实是一类东西。
当我们希望创建一系列相互依赖对象的时候,比如在 DB 中读取数据,我们需要创建一个 DBConnection,然后初始化 DBCommand,最后通过 DataReader 来拿到数据。整套逻辑是可以跨数据库的,比如 MySQL 或者 Oracle 等,那么问题来了,如何保证你的 DBConnection / DBCommand / DataReader 都是同一类的呢?如果用正常的 Factory Method 的方式来做的话,外界给你传入了一个 MySQL 的 DBConnection 和一个 Oracle 的 DBCommand,这样肯定是不合预期的。
所以我们需要在工厂方法里面将这些相互依赖的对象的创建工作固定并组合起来,保证可控且符合预期。如下示例代码所示。
示例代码:

abstract class IDBFactory {
    public abstract IDBConnection createDBConnection();
    public abstract IDBCommand createDBCommand();
    public abstract IDataReader createDataReader();
}

其他代码和上面的 Factory Method 相同。
关键点:

  • 解决创建一系列相互依赖的对象的创建工作

Prototype

问题:
当某个对象经过了一系列复杂的流程后才完成了初始化的工作,此时,当你需要一个或若干个新的相同的对象,直接使用工厂方法来创建会消耗大量的时间或空间代价,那么是否可以直接通过原对象来“复制”一个出来呢?
实现 && 解析:
其实 Prototype 的实现方式和工厂方法大同小异。比如下面的工厂方法的代码:

abstract class ISplitter {
    public abstract void split();
}
abstract class SplitterFactory {
    public abstract ISplitter createSplitter();
}
class BinarySplitter extends ISplitter {
    @Override
    public void split() {}
}
class BinarySplitterFactory extends SplitterFactory {
    @Override
    public ISplitter createSplitter() {
        return new BinarySplitter();
    }
}

既然 ISplitterSplitterFactory 是相伴相随的,那么是不是可以合并起来呢?

abstract class ISplitter {
    public abstract void split();
    public abstract ISplitter createSplitter();
}
class BinarySplitter extends ISplitter {
    @Override
    public void split() {}
    @Override
    public ISplitter createSplitter() {
        return new BinarySplitter();
    }
}

此时,如果我们通过复杂的初始化方法,已经拿到了一个 BinarySplitter 的对象,那么通过调用自身的 createSplitter() 方法拿到自身当前状态的一个拷贝,岂不美哉?
当然实际的场景中,这个 createSplitter() 方法一般被叫做 clone(),大部分场景下,它都是深克隆,保证新创建的对象没有和原对象没有共享的成分,同时内容又保持相同。
一般情况下,clone 的实现方式有下面几种:

  1. Java 中有 memberwiseClone 方法可供调用,但要求当前类中的成员对象不能存在指针,不然就变成了假克隆
  2. 通过序列化+反序列化的方式来实现
  3. 通过拷贝构造器来实现,比如 public Intent(Intent o)

需要注意的是,不要直接调用原型对象的方法,标准的方式是通过不断的 clone 来生成对应的业务对象,然后再去操作业务对象的方法。
示例代码:

abstract class ISplitter {
    public abstract void split();
    public abstract ISplitter clone();
}
class BinarySplitter extends ISplitter {
    @Override
    public void split() {}
    @Override
    public ISplitter clone() {
        // 上面提到的三种方法
    }
}

关键点:

  • 原型模式是根据已经创建出来的一个原型状态的对象不停的 clone,工厂模式是重新创建对象,这是他们两个的区别

场景:对象性能

这一类设计模式是解决面向对象带来的性能问题。考虑下面的场景:

  • 在软件系统中,经常有这样一些特殊的类,必须保证它们在系统中只存在一个实例,才能确保它们的逻辑正确性,以及良好的效率,但是你是不能直接去限制你的用户,因为他们可以 new 一个,也可以 new 多个
  • 如果你现在面临这样一个场景,每个字符都对应一个字体对象,那么十万字就会需要十万个字体对象,占用大量的内存。但是字体其实不会超过一千个

上面两个场景分别对应了两个设计模式,分别是 Singleton / Flyweight 模式。下面来一一说明。

Singleton

问题:
在系统的设计中,如何保证你提供的这个类全局仅存在一个实例,且强制调用者不能绕过这个限制?
注意这是类设计者的责任,而不是使用者的责任。
实现 && 解析:
Singleton 模式的定义可以简要说明为:保证一个类仅有一个实例,并且提供一个该实例的全局访问点。
具体的实现参见下面的代码,有两种不同形式的代码可供参考。
示例代码:
普通形式:

package cn.bluecoder.patterns;
public class Singleton {
    // 加了 volatile 后,就不会再重排序了,会直接变成
    // 分配内存,调用构造函数,然后把内存地址赋值给 instance
    private static volatile Singleton instance;
    /**
     * 写了 private 构造器,编译器就不会自动生成 public 构造器了
     */
    private Singleton() {}
    public static Singleton getInstance() {
        // double check
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    // 由于 JVM 重排序的操作,下面两种情况都有可能发生
                    // 分配内存,调用构造函数,然后把内存地址赋值给 instance
                    // 分配内存,把内存地址赋值给 instance,然后调用构造函数
                    // 第二步的时候,外侧 instance==null 是不安全的
                    // 所以需要 volatile 关键字
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

精简形式,利用 static 的初始化是单线程来避免并发问题,同时 lazy load:

public class Singleton1 {
    private Singleton1() {}
    private static class SingletonHolder {
        private static final Singleton1 instance = new Singleton1();  // static 初始化时多线程安全的
    }
    public static Singleton1 getInstance() {
        return SingletonHolder.instance;
    }
}

关键点:

  • Singleton 模式中的实例构造器可以设置为 protected 以允许子类派生
  • Singleton 一般不要支持拷贝构造函数和 clone 接口,有可能导致多个实例

Flyweight

问题:
对于上面描述的场景:每个字符都对应一个字体对象,那么十万字就会需要十万个字体对象,占用大量的内存。但是字体其实不会超过一千个。
实现 && 解析:
这种情况下,直接存储肯定是不地道的,一般情况下,我们通过对象共享的做法来降低对内存的需求。避免大量细粒度的对象需要高昂的内存代价。
注意 Flyweight 主要解决面向对象的代价问题,一般不触及面向对象的抽象性问题。
示例代码:

public class Font {
    private String key;
    private String name;
    private String decorator;
    private boolean isBold;
    private int size;
    public Font(String key){
        //...
    }
}
public class FontFactory{
    private Map<String, Font> fonts = new ConcurrentHashMap<String, Font>();
    public Font getFont(String key) {
        if (!fonts.containsKey(key)) {
            fonts.put(key, new Font(key));
        }
        return fonts.get(key);
    }
}

关键点:

  • 通过对象共享的做法来降低对内存的需求

场景:接口隔离

面向对象的过程中,某些接口之间直接的依赖常常会带来很多问题,甚至根本无法实现。“接口隔离”模式就是为了解决这一问题。

Facade && Proxy

描述:
Facade 和 Proxy 其实很类似,两者都在阐述一点:间接即解耦。在计算机的进化史中,也是在不停地实践这一观点:

  • 人 -> 硬件
  • 人 -> 软件 -> 硬件
  • 人 -> 应用软件 -> 操作系统 -> 硬件
  • 人 -> 应用软件 -> 虚拟机 -> 操作系统 -> 硬件

这一小节没有示例代码,因为实现千变万化,但只要符合这一中心观点即可:
为子系统中的一组接口提供一个一致(稳定)的界面,Facade 模式定义了一个高层接口,这个接口使得这一子系统更加容易进行复用。
Proxy 更加具体一些,它针对的是对象。当由于某种原因,直接访问这个对象会给使用者、或者系统结构带来很多麻烦,那么就增加一层间接层,在不失去透明操作对象的同时来管理、控制这些对象特有的复杂性。
关键点:

  • 增加一层间接层是软件系统中对许多复杂问题的一种常见解决方式。在面向对象系统中,直接使用这些对象会带来很多问题,作为间接层的 Proxy 对象便是解决这一问题的常用手段
  • 具体 Proxy 设计模式的实现方法、实现粒度都相差很大,有些可能对单个对象做细粒度的控制,如 copy-on-write 技术,有些可能对组件模块提供抽象代理层,在架构层次对对象做 Proxy
  • Proxy 并不一定要求保持接口完整的一致性,只要能够实现间接控制,有时候损失一些透明性是可以接受的

Adapter

问题:
由于应用环境的变化,常常需要将一些现存的对象放到新环境中应用,但是新环境要求的接口是这些现存对象所不能满足的。
实现 && 解析:
将一个类的接口转换成客户希望的另外一个接口。Adapter 模式使得原先由于接口不兼容而不能一起工作的那些类可以一起工作。简要来说就是:
组合旧类实例,实现新类的接口
示例代码:

// 目标接口(新接口)
public interface ITarget {
    public void process();
}
// 目标环境(新环境)
public class TargetLibrary {
    public void invoke(ITarget target) {
        target.process();
    }
}
// 现存接口(老接口)
public interface IAdaptee {
    public void foo(int data);
    public int bar();
}
// 现存类型
public class OldClass implements IAdaptee {
    //....
}
// 对象适配器
public class Adapter implements ITarget { // 继承
    IAdaptee adaptee;  // 组合
    public Adapter(IAdaptee adaptee) {
        this.adaptee = adaptee;
    }
    public void process() {
        int data = adaptee.bar();
        adaptee.foo(data);
    }
}

场景:状态变化

在软件构建的过程中,某些对象的状态如果变化,其行为也会随之而发生变化,比如文档处于只读状态,其支持的行为和读写状态支持的行为就可能完全不同。
如何在运行时根据对象的状态来透明的更改对象的行为?而不会为对象操作和状态转化之间引入紧耦合?

State

实现 && 解析:
State 模式将所有与一个特定状态相关的行为都放入一个 State 的子类对象中,在对象状态切换时,切换相应的对象;但同时维持 State 的接口,这样实现了具体操作与状态转换之间的解耦。
另外为不同的状态引入不同的对象使得状态转换变得更加明确,而且可以保证不会出现状态不一致的情况,因为转换是原子性的——即要么彻底转换过来,要么不转换。
如果 State 对象没有实例变量,那么各个上下文可以共享同一个 singleton 的 State 对象,从而节省对象开销。
示例代码:

import sun.security.krb5.internal.NetClient;
public abstract class NetworkState {
    public NetworkState next;
    public abstract void Operation1();
    public abstract void Operation2();
    public abstract void Operation3();
}
public class OpenState extends NetworkState {
    @Override
    public void Operation1() {
        next = new CloseState();
    }
    @Override
    public void Operation2() {
        next = new ConnectState();
    }
    @Override
    public void Operation3() {
        next = new OpenState();
    }
}
public class CloseState extends NetworkState {
    @Override
    public void Operation1() {
        next = new ConnectState();
    }
    @Override
    public void Operation2() {
        next = new OpenState();
    }
    @Override
    public void Operation3() {
        next = new CloseState();
    }
}
public class ConnectState extends NetworkState {
}
public class WaitingState extends NetworkState {
}
class NetworkProcessor {
    NetworkState state;
    public NetworkProcessor(NetworkState state) {
        this.state = state;
    }
    public void Operation1() {
        state.Operation1();
        state = state.next;
    }
    public void Operation2() {
        state.Operation2();
        state = state.next;
    }
    public void Operation3() {
        state.Operation3();
        state = state.next;
    }
}

关键点:
状态对象化,用多态的方式来进行状态的变化。

总结

啰里啰嗦了这么多,一句话可以概括上面所有模式的中心主旨:在变化中寻求复用性。 从另一个方面来说,能够随意组合模式,解决实际问题才是真谛。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注