门面模式

Posted gendway

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了门面模式相关的知识,希望对你有一定的参考价值。


23.1 我要投递信件
我们都写过纸质信件吧, 比如给女朋友写情书什么的。 写信的过程大家应该都还记得
——先写信的内容, 然后写信封, 再把信放到信封中, 封好, 投递到信箱中进行邮递, 这个
过程还是比较简单的, 虽然简单, 但是这4个步骤都不可或缺! 我们先把这个过程通过程序
实现出来, 如图23-1所示。
图23-1 写信过程类图
这一个过程还是比较简单的, 我们看程序的实现, 先看接口, 如代码清单23-1所示。代码清单23-1 写信过程接口
public interface ILetterProcess {
//首先要写信的内容
public void writeContext(String context);
//其次写信封
public void fillEnvelope(String address);
//把信放到信封里
public void letterInotoEnvelope();
//然后邮递
public void sendLetter();
}
在接口中定义了完成的一个写信过程, 这个过程需要实现, 其实现类如代码清单23-2所
示。
代码清单23-2 写信过程的实现
public class LetterProcessImpl implements ILetterProcess {
//写信
public void writeContext(String context) {
System.out.println("填写信的内容..." + context);
}/
/在信封上填写必要的信息
public void fillEnvelope(String address) {
System.out.println("填写收件人地址及姓名..." + address);
}/
/把信放到信封中, 并封好
public void letterInotoEnvelope() {
System.out.println("把信放到信封中...");
}/
/塞到邮箱中, 邮递
public void sendLetter() {
System.out.println("邮递信件...");
}
}
在这种环境下, 最累的是写信人, 为了发送一封信要有4个步骤, 而且这4个步骤还不能
颠倒, 我们先看看这个过程如何通过程序表现出来, 有人开始用这个过程写信了, 如代码清
单23-3所示。
代码清单23-3 场景类
public class Client {
public static void main(String[] args) {
//创建一个处理信件的过程
ILetterProcess letterProcess = new LetterProcessImpl();//开始写信
letterProcess.writeContext("Hello,It‘s me,do you know who I am? I‘m
//开始写信封
letterProcess.fillEnvelope("Happy Road No. 666,God Province,Heaven")
//把信放到信封里, 并封装好
letterProcess.letterInotoEnvelope();
//跑到邮局把信塞到邮箱, 投递
letterProcess.sendLetter();
}
}
运行结果如下所示:
填写信的内容...Hello,It‘s me,do you know who I am? I‘m your old lover. I‘d like to...
填写收件人地址及姓名...Happy Road No. 666,God Province,Heaven
把信放到信封中...
邮递信件...
我们回过头来看看这个过程, 它与高内聚的要求相差甚远, 更不要说迪米特法则、 接口
隔离原则了。 你想想, 你要知道这4个步骤, 而且还要知道它们的顺序, 一旦出错, 信就不
可能邮寄出去, 这在面向对象的编程中是极度地不适合, 它根本就没有完成一个类所具有的
单一职责。
还有, 如果信件多了就非常麻烦, 每封信都要这样运转一遍, 非得累死, 更别说要发个
广告信了, 那怎么办呢? 还好, 现在邮局开发了一个新业务, 你只要把信件的必要信息告诉
我, 我给你发, 我来完成这4个过程, 只要把信件交给我就成了, 其他就不要管了。 非常好
的方案! 我们来看类图, 如图23-2所示。图23-2 增加现代化邮局的类图
这还是比较简单的类图, 增加了一个ModenPostOffice类, 负责对一个比较复杂的信件处
理过程的封装, 然后高层模块只要和它有交互就成了, 如代码清单23-4所示。
代码清单23-4 现代化邮局
public class ModenPostOffice {
private ILetterProcess letterProcess = new LetterProcessImpl();
//写信, 封装, 投递, 一体化
public void sendLetter(String context,String address){
//帮你写信
letterProcess.writeContext(context);
//写好信封
letterProcess.fillEnvelope(address);
//把信放到信封中
letterProcess.letterInotoEnvelope();
//邮递信件
letterProcess.sendLetter();
}
}
这个类是什么意思呢, 就是说现在有一个Hell Road PostOffice(地狱路邮局) 提供了一种新型服务, 客户只要把信的内容以及收信地址给他们, 他们就会把信写好, 封好, 并发送
出去。 这种服务推出后大受欢迎, 这多简单, 客户减少了很多工作, 谁不乐意呀。 那我们看
看客户是怎么调用的, 如代码清单23-5所示。
代码清单23-5 场景类
public class Client {
public static void main(String[] args) {
//现代化的邮局, 有这项服务, 邮局名称叫Hell Road
ModenPostOffice hellRoadPostOffice = new ModenPostOffice();
//你只要把信的内容和收信人地址给他, 他会帮你完成一系列的工作
//定义一个地址
String address = "Happy Road No. 666,God Province,Heaven";
//信的内容
String context = "Hello,It‘s me,do you know who I am? I‘m your old l
//你给我发送吧
hellRoadPostOffice.sendLetter(context, address);
}
}
运行结果是相同的。 我们看看场景类是不是简化了很多, 只要与ModenPostOffice交互就
成了, 其他的什么都不用管, 写信封啦、 写地址啦……都不用关心, 只要把需要的信息提交
过去就成了, 邮局保证会按照我们指定的地址把指定的内容发送出去, 这种方式不仅简单,
而且扩展性还非常好, 比如一个非常时期, 寄往God Province(上帝省) 的邮件都必须进行
安全检查, 那我们就很好处理了, 如图23-3所示。图23-3 扩展后的系统类图
增加了一个Police类, 负责对信件进行检查, 如代码清单23-6所示。
代码清单23-6 信件检查类
public class Police {
//检查信件, 检查完毕后警察在信封上盖个戳: 此信无病毒
public void checkLetter(ILetterProcess letterProcess){
System.out.println(letterProcess+" 信件已经检查过了...");
}
}
我们再来看一下封装类ModenPostOffice的变更, 它封装了这部分的变化, 如代码清单
23-7所示。
代码清单23-7 扩展后的现代化邮局
public class ModenPostOffice {
private ILetterProcess letterProcess = new LetterProcessImpl();
private Police letterPolice = new Police();
//写信, 封装, 投递, 一体化了
public void sendLetter(String context,String address){
//帮你写信letterProcess.writeContext(context);
//写好信封
letterProcess.fillEnvelope(address);
//警察要检查信件了
letterPolice.checkLetter(letterProcess);
//把信放到信封中
letterProcess.letterInotoEnvelope();
//邮递信件
letterProcess.sendLetter();
}
}
只是增加了一个letterPolice变量的声明以及一个方法的调用, 那这个写信的过程就变成
这样: 先写信、 写信封, 然后警察开始检查, 之后才把信放到信封, 最后发送出去, 那这个
变更对客户来说是透明的, 他根本就看不到有人在检查他的邮件, 他也不用了解, 反正现代
化的邮件系统都帮他做了, 这也是他乐意的地方。
场景类还是完全相同, 但是运行结果稍有不同, 如下所示:
填写信的内容...Hello,It‘s me,do you know who I am?I‘m your old lover.I‘d like to...
填写收件人地址及姓名...Happy Road No.666,God Province,Heaven
com.cbf4life.common3.LetterProcessImpl@15ff48b 信件已经检查过了...
把信放到信封中...
邮递信件...
高层模块没有任何改动, 但是信件却已经被检查过了。 这正是我们设计所需要的模式,
不改变子系统对外暴露的接口、 方法, 只改变内部的处理逻辑, 其他兄弟模块的调用产生了
不同的结果, 确实是一个非常棒的设计。 这就是门面模式。23.2 门面模式的定义
门面模式(Facade Pattern) 也叫做外观模式, 是一种比较常用的封装模式, 其定义如
下:
Provide a unified interface to a set of interfaces in a subsystem.Facade defines a higher-level
interface that makes the subsystem easier to use.(要求一个子系统的外部与其内部的通信必须通
过一个统一的对象进行。 门面模式提供一个高层次的接口, 使得子系统更易于使用。 )
门面模式注重“统一的对象”, 也就是提供一个访问子系统的接口, 除了这个接口不允许
有任何访问子系统的行为发生, 其通用类图, 如图23-4所示。
图23-4 扩展后的系统类图
是的, 类图就这么简单, 但是它代表的意义可是异常复杂, Subsystem Classes是子系统
所有类的简称, 它可能代表一个类, 也可能代表几十个对象的集合。 甭管多少对象, 我们把
这些对象全部圈入子系统的范畴, 其结构如图23-5所示。图23-5 门面模式示意图
再简单地说, 门面对象是外界访问子系统内部的唯一通道, 不管子系统内部是多么杂乱
无章, 只要有门面对象在, 就可以做到“金玉其外, 败絮其中”。 我们先明确一下门面模式的
角色。
● Facade门面角色
客户端可以调用这个角色的方法。 此角色知晓子系统的所有功能和责任。 一般情况下,
本角色会将所有从客户端发来的请求委派到相应的子系统去, 也就说该角色没有实际的业务
逻辑, 只是一个委托类。● subsystem子系统角色
可以同时有一个或者多个子系统。 每一个子系统都不是一个单独的类, 而是一个类的集
合。 子系统并不知道门面的存在。 对于子系统而言, 门面仅仅是另外一个客户端而已。
我们来看一下门面模式的通用源码, 先来看子系统源代码。 由于子系统是类的集合, 因
此要描述该集合很花费精力, 每一个子系统都不相同, 我们使用3个相互无关的类来代表,
如代码清单23-8所示。
代码清单23-8 子系统
public class ClassA {
public void doSomethingA(){
//业务逻辑
}
}p
ublic class ClassB {
public void doSomethingB(){
//业务逻辑
}
}p
ublic class ClassC {
public void doSomethingC(){
//业务逻辑
}
}
我们认为这3个类属于近邻, 处理相关的业务, 因此应该被认为是一个子系统的不同逻
辑处理模块, 对于此子系统的访问需要通过门面进行, 如代码清单23-9所示。
代码清单23-9 门面对象
public class Facade {
//被委托的对象
private ClassA a = new ClassA();
private ClassB b = new ClassB();
private ClassC c = new ClassC();
//提供给外部访问的方法
public void methodA(){
this.a.doSomethingA();
} p
ublic void methodB(){this.b.doSomethingB();
} p
ublic void methodC(){
this.c.doSomethingC();
}
}23.3 门面模式的应用
23.3.1 门面模式的优点
门面模式有如下优点。
● 减少系统的相互依赖
想想看, 如果我们不使用门面模式, 外界访问直接深入到子系统内部, 相互之间是一种
强耦合关系, 你死我就死, 你活我才能活, 这样的强依赖是系统设计所不能接受的, 门面模
式的出现就很好地解决了该问题, 所有的依赖都是对门面对象的依赖, 与子系统无关。
● 提高了灵活性
依赖减少了, 灵活性自然提高了。 不管子系统内部如何变化, 只要不影响到门面对象,
任你自由活动。
● 提高安全性
想让你访问子系统的哪些业务就开通哪些逻辑, 不在门面上开通的方法, 你休想访问
到。
23.3.2 门面模式的缺点
门面模式最大的缺点就是不符合开闭原则, 对修改关闭, 对扩展开放, 看看我们那个门
面对象吧, 它可是重中之重, 一旦在系统投产后发现有一个小错误, 你怎么解决? 完全遵从
开闭原则, 根本没办法解决。 继承? 覆写? 都顶不上用, 唯一能做的一件事就是修改门面角
色的代码, 这个风险相当大, 这就需要大家在设计的时候慎之又慎, 多思考几遍才会有好收
获。23.3.3 门面模式的使用场景
● 为一个复杂的模块或子系统提供一个供外界访问的接口
● 子系统相对独立——外界对子系统的访问只要黑箱操作即可
比如利息的计算问题, 没有深厚的业务知识和扎实的技术水平是不可能开发出该子系统
的, 但是对于使用该系统的开发人员来说, 他需要做的就是输入金额以及存期, 其他的都不
用关心, 返回的结果就是利息, 这时候, 门面模式是非使用不可了。
● 预防低水平人员带来的风险扩散
比如一个低水平的技术人员参与项目开发, 为降低个人代码质量对整体项目的影响风
险, 一般的做法是“画地为牢”, 只能在指定的子系统中开发, 然后再提供门面接口进行访问
操作。23.4 门面模式的注意事项
23.4.1 一个子系统可以有多个门面
一般情况下, 一个子系统只要有一个门面足够了, 在什么情况下一个子系统有多个门面
呢? 以下列举了几个。
● 门面已经庞大到不能忍受的程度
比如一个纯洁的门面对象已经超过了200行的代码, 虽然都是非常简单的委托操作, 也
建议拆分成多个门面, 否则会给以后的维护和扩展带来不必要的麻烦。 那怎么拆分呢? 按照
功能拆分是一个非常好的原则, 比如一个数据库操作的门面可以拆分为查询门面、 删除门
面、 更新门面等。
● 子系统可以提供不同访问路径
我们以门面模式的通用源代码为例。 ClassA、 ClassB、 ClassC是一个子系统的中3个对
象, 现在有两个不同的高层模块来访问该子系统, 模块一可以完整的访问所有业务逻辑, 也
就是通用代码中的Facade类, 它是子系统的信任模块; 而模块二属于受限访问对象, 只能访
问methodB方法, 那该如何处理呢? 在这种情况下, 就需要建立两个门面以供不同的高层模
块来访问, 在原有的通用源码上增加一个新的门面即可, 如代码清单23-10所示。
代码清单23-10 新增门面
public class Facade2 {
//引用原有的门面
private Facade facade = new Facade();
//对外提供唯一的访问子系统的方法
public void methodB(){
this.facade.methodB();
}
}
增加的门面非常简单, 委托给了已经存在的门面对象Facade进行处理, 为什么要使用委托而不再编写一个委托到子系统的方法呢? 那是因为在面向对象的编程中, 尽量保持相同的
代码只编写一遍, 避免以后到处修改相似代码的悲剧。
23.4.2 门面不参与子系统内的业务逻辑
我们这节的标题是什么意思呢? 我们举一个例子来说明, 还是以通用源代码为例。 我们
把门面上的methodC上的逻辑修改一下, 它必须先调用ClassA的doSomethingA方法, 然后再
调用ClassC的doSomethingC方法, 如代码清单23-11所示。
代码清单23-11 修改门面
public class Facade {
//被委托的对象
private ClassA a = new ClassA();
private ClassB b = new ClassB();
private ClassC c = new ClassC();
//提供给外部访问的方法
public void methodA(){
this.a.doSomethingA();
} p
ublic void methodB(){
this.b.doSomethingB();
} p
ublic void methodC(){
this.a.doSomethingA();
this.c.doSomethingC();
}
}
还是非常简单, 只是在methodC方法中增加了doSomethingA()方法的调用, 可以这样做
吗? 我相信大部分读者都说可以这样做, 而且已经在实际系统开发中这样使用了, 我今天告
诉各位, 这样设计是非常不靠谱的, 为什么呢? 因为你已经让门面对象参与了业务逻辑, 门
面对象只是提供一个访问子系统的一个路径而已, 它不应该也不能参与具体的业务逻辑, 否
则就会产生一个倒依赖的问题: 子系统必须依赖门面才能被访问, 这是设计上一个严重错
误, 不仅违反了单一职责原则, 同时也破坏了系统的封装性。
说了这么多, 那对于这种情况该怎么处理呢? 建立一个封装类, 封装完毕后提供给门面对象。 我们先建立一个封装类, 如代码清单23-12所示。
代码清单23-12 封装类
public class Context {
//委托处理
private ClassA a = new ClassA();
private ClassC c = new ClassC();
//复杂的计算
public void complexMethod(){
this.a.doSomethingA();
this.c.doSomethingC();
}
}
该封装类的作用就是产生一个业务规则complexMethod, 并且它的生存环境是在子系统
内, 仅仅依赖两个相关的对象, 门面对象通过对它的访问完成一个复杂的业务逻辑, 如代码
清单23-13所示。
代码清单23-13 门面类
public class Facade {
//被委托的对象
private ClassA a = new ClassA();
private ClassB b = new ClassB();
private Context context = new Context();
//提供给外部访问的方法
public void methodA(){
this.a.doSomethingA();
} p
ublic void methodB(){
this.b.doSomethingB();
} p
ublic void methodC(){
this.context.complexMethod();
}
}
通过这样一次封装后, 门面对象又不参与业务逻辑了, 在门面模式中, 门面角色应该是
稳定, 它不应该经常变化, 一个系统一旦投入运行它就不应该被改变, 它是一个系统对外的
接口, 你变来变去还怎么保证其他模块的稳定运行呢? 但是, 业务逻辑是会经常变化的, 我
们已经把它的变化封装在子系统内部, 无论你如何变化, 对外界的访问者来说, 都还是同一个门面, 同样的方法——这才是架构师最希望看到的结构。23.5 最佳实践
门面模式是一个很好的封装方法, 一个子系统比较复杂时, 比如算法或者业务比较复
杂, 就可以封装出一个或多个门面出来, 项目的结构简单, 而且扩展性非常好。 还有, 对于
一个较大项目, 为了避免人员带来的风险, 也可以使用门面模式, 技术水平比较差的成员,
尽量安排独立的模块, 然后把他写的程序封装到一个门面里, 尽量让其他项目成员不用看到
这些人的代码, 看也看不懂, 我也遇到过一个“高人”写的代码, private方法、 构造函数、 常
量基本都不用, 你要一个public方法, 好, 一个类里就一个public方法, 所有代码都在里面,
然后你就看吧, 一大坨程序, 看着就能把人逼疯。 使用门面模式后, 对门面进行单元测试,
约束项目成员的代码质量, 对项目整体质量的提升也是一个比较好的帮助。

以上是关于门面模式的主要内容,如果未能解决你的问题,请参考以下文章

门面模式

javaEE设计模式——门面模式

设计模式之门面模式与装饰器模式详解和应用

设计模式之门面模式(外观模式)

设计模式之门面模式(外观模式)

PHP设计模式 - 门面模式