「补课」进行时:设计模式(16)——简单又实用的门面模式

840 阅读7分钟

1. 前文汇总

「补课」进行时:设计模式系列

2. 从银行转账说起

当我们在银行进行转账操作的时候,整个操作流程我们可以简化为账户 A 扣费,然后账户 B 增加余额,最后转账操作成功。

这两个操作缺一不可,同时又不能颠倒顺序。

简单定义一个转账的接口 ITransfer

public interface ITransfer {
    // 首先发起转账
    void start(String amount);
    // 账户 A 进行扣费
    void subtractionA();
    // 账户 B 增加金额
    void addB();
    // 转账完成
    void end();
}

然后增加一个接口实现类:

public class TransferImpl implements ITransfer {
    @Override
    public void start(String amount) {
        System.out.println(String.format("账户 A 开始向账户 B 进行转账: %s 元。", amount));
    }

    @Override
    public void subtractionA() {
        System.out.println("账户 A 扣费成功");
    }

    @Override
    public void addB() {
        System.out.println("账户 B 余额增加成功");
    }

    @Override
    public void end() {
        System.out.println("转账完成");
    }
}

来一个测试类:

public class Test {
    public static void main(String[] args) {
        ITransfer transfer = new TransferImpl();
        transfer.start("1000");
        transfer.subtractionA();
        transfer.addB();
        transfer.end();
    }
}

最后运行的结果如下:

账户 A 开始向账户 B 进行转账: 1000 元。
账户 A 扣费成功
账户 B 余额增加成功
转账完成

我们回过头来看看这个过程,它与高内聚的要求相差甚远,更不要说迪米特法则、接口隔离原则了。

如果我们要进行转账操作,那么我们必须要知道这几个步骤,而且还要知道它们的顺序,一旦出错,转账操作就无法完成,这在面向对象的编程中是极度地不适合,它根本就没有完成一个类所具有的单一职责。

那怎么办呢?这时候银行柜台出现了,我们只需要把需求告诉银行柜台,柜台会直接帮我们完成转账操作。

银行柜台:

public class BankCounter {
    private ITransfer transfer = new TransferImpl();
    // 转账操作一体化
    public void transferAmount(String amount) {
        transfer.start(amount);
        transfer.subtractionA();
        transfer.addB();
        transfer.end();
    }
}

接下来修改一下测试类:

public class Test1 {
    public static void main(String[] args) {
        BankCounter counter = new BankCounter();
        counter.transferAmount("1000");
    }
}

和刚才的执行结果一样,但是整个测试类却简化了很多,只要关心和银行柜台进行交互就行,完全不用自己操心之前的账户 A 扣费,再给账户 B 加余额,但是,每次转账就这么直接转账有点不大安全,假如账户 A 的余额根本不足转账的费用,那么就不应该转账成功。

增加一个余额校验类 Balance 对账户余额进行校验:

public class Balance {
    Boolean checkBalance() {
        System.out.println("账户余额校验成功");
        return true;
    }
}

这时候,测试类无需改动,只需修改银行柜台类就可以:

public class BankCounter {
    private ITransfer transfer = new TransferImpl();
    private Balance balance = new Balance();
    // 转账操作一体化
    public void transferAmount(String amount) {
        transfer.start(amount);
        transfer.subtractionA();
        // 增加余额校验
        if (balance.checkBalance()) {
            transfer.addB();
            transfer.end();
        }
    }
}

这里只增加了一个余额校验类,并且对转账的过程进行了修改,这个过程对于我们来讲是完全透明的,我们完全不需要关心转账的过程,这个过程由银行柜台全部帮我们办好了。

高层模块没有任何改动,但是账户的余额已经被检查过了,不改变子系统对外暴露的接口、方法,只改变内部的处理逻辑,其他兄弟模块的调用产生了不同的结果。

是不是非常简单,没错,这就是门面模式或者说外观模式。

3. 门面模式

3.1 定义

门面模式(Facade Pattern)也叫做外观模式,是一种比较常用的封装模式,其定义如下:

Provide a unified interface to a set of interfaces in a subsystem.Facadedefines a higher-level interface that makes the subsystem easier to use.(要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。)

3.2 通用类图

门面模式注重「统一的对象」,也就是提供一个访问子系统的接口,除了这个接口不允许有任何访问子系统的行为发生,其通用类图:

是的,类图就这么简单,但是它代表的意义可是异常复杂,Subsystem Classes是子系统所有类的简称,它可能代表一个类,也可能代表几十个对象的集合。甭管多少对象,我们把这些对象全部圈入子系统的范畴:

再简单地说,门面对象是外界访问子系统内部的唯一通道,不管子系统内部是多么杂乱无章,只要有门面对象在,就可以做到「金玉其外,败絮其中」。我们先明确一下门面模式的角色。

  • Facade 门面角色:此角色知晓子系统的所有功能和责任。一般情况下,本角色会将所有从客户端发来的请求委派到相应的子系统去,也就说该角色没有实际的业务逻辑,只是一个委托类。
  • subsystem 子系统角色:可以同时有一个或者多个子系统。每一个子系统都不是一个单独的类,而是一个类的集合。子系统并不知道门面的存在。对于子系统而言,门面仅仅是另外一个客户端而已。

3.3 通用代码

子系统:

// 
public class ClassA {
    public void doSomethingA() {
        // 执行逻辑 A
    }
}

public class ClassB {
    public void doSomethingB() {
        // 执行逻辑 A
    }
}

public class ClassC {
    public void doSomethingC() {
        // 执行逻辑 A
    }
}

门面类:

public class Facade {
    private ClassA classA = new ClassA();
    private ClassB classB = new ClassB();
    private ClassC classC = new ClassC();
    public void methodA() {
        this.classA.doSomethingA();
    }
    public void methodB() {
        this.classB.doSomethingB();
    }
    public void methodC() {
        this.classC.doSomethingC();
    }
}

4. 注意

有一点需要注意的是:门面不参与子系统内的业务逻辑。

这句话怎么理解?举一个简单的例子:

把上面的通用代码稍微改一下,在 methodC() 方法上先调用 ClassAdoSomethingA() 方法,然后再调用 ClassCdoSomethingC() 方法,修改后的门面类如下:

public class Facade {
    private ClassA classA = new ClassA();
    private ClassB classB = new ClassB();
    private ClassC classC = new ClassC();
    public void methodA() {
        this.classA.doSomethingA();
    }
    public void methodB() {
        this.classB.doSomethingB();
    }
    public void methodC() {
        this.classA.doSomethingA();
        this.classC.doSomethingC();
    }
}

非常简单,只是在 methodC() 方法中增加了 doSomethingA() 方法的调用,可以这样做吗?

我相信在大多数的日常开发中,我们很多时候都是直接这么写了,这么写有什么问题么?

当然有,因为这种做法让门面对象参与了业务逻辑,门面对象只是提供一个访问子系统的一个路径而已,它不应该也不能参与具体的业务逻辑,否则就会产生一个倒依赖的问题:子系统必须依赖门面才能被访问。

那么在这种情况下可以怎么处理呢?

也很简单,创建一个封装类,封装完毕后提供给门面对象:

public class Context {
    private ClassA classA = new ClassA();
    private ClassC classC = new ClassC();
    // 复杂的业务操作
    public void complexMethod() {
        this.classA.doSomethingA();
        this.classC.doSomethingC();
    }
}

这个封装类存在的价值就是产生一个复杂的业务规则 complexMethod() ,并且它的生存环境是在子系统内,仅仅依赖两个相关的对象,门面对象通过对它的访问完成一个复杂的业务逻辑,最后我们通过门面模式进行调用的时候直接调用封装类:

public class Facade1 {
    private ClassA classA = new ClassA();
    private ClassB classB = new ClassB();
    private Context context = new Context();
    public void methodA() {
        this.classA.doSomethingA();
    }
    public void methodB() {
        this.classB.doSomethingB();
    }
    public void methodC() {
        this.context.complexMethod();
    }
}

通过这样一次封装后,门面对象又不参与业务逻辑了,在门面模式中,门面角色应该是稳定,它不应该经常变化,一个系统一旦投入运行它就不应该被改变,它是一个系统对外的接口,你变来变去还怎么保证其他模块的稳定运行呢?但是,业务逻辑是会经常变化的,我们已经把它的变化封装在子系统内部,无论你如何变化,对外界的访问者来说,都还是同一个门面,同样的方法——这才是架构师最希望看到的结构。


文章持续更新,可以微信搜一搜「 极客挖掘机 」第一时间阅读,回复关键字有我准备的各种教程,欢迎阅读。