在所有的设计模式中,门面模式属于相当简单的一类。如果你已经在其他的文章中学习过门面模式了,那么你一定会觉得相比于其他模式,这个模式的一切理解起来似乎都很简单,但是当你回过头总结的时候,你会发现你变得茫然,你甚至不知道门面模式应该在何处使用,如何使用,这种感觉就像是你手里拿着一把钥匙,但你不知道这钥匙能打开的盒子在哪儿。
接下来,我将按照我的个人理解来聊一聊门面模式。如果你认为我的理解存在偏颇,请在评论区告诉我。

一、问题引入及解决方案

1.1 与内部复杂的系统交互代价

在一个项目的开始的时候,我们拥有很少的几个类,他们往往工作的比较良好。后来,随着新需求的不断被提出,项目中的类开始多了起来,对象与对象的关系开始往复杂的方向偏离。为了让整个项目变得更加便于维护和扩展,我们开始重构这个项目。我们尽量尝试让对象解耦,并且引入了一些设计模式让各个小模块的扩展性更加灵活,但同时也让结构变得更为庞杂,这是不可避免的,天下本没有免费的午餐。所有的小模块组合在一起,构成了我们的子系统。然后,此时有一个外部系统,需要与子系统交互,很可能整个关系就像下图这样:
结构型 - 门面模式(Facade) - 图1

上面图没有实际的意义,只是为了演示两个复杂系统之间对接的情况,图本身没有表达任何现实意义。

我们在惊叹于不知不觉间系统已然具有如此规模的同时,也为两个系统的交互犯了愁:如此多的依赖关系,将来子系统扩展或者接口发生变动时,外部系统的调整将变得艰难,影响很大。

事实上,子系统随着时间的推移演变得越来越复杂,类定义越来越多,是正常、合理且必然的。在一个健壮的项目中,各个小模块职责分明,各个类的行为单一,那么类的数量必然不会少。因为原本可能出现在一个类中表达多个行为的代码在解耦后,就被拆分到不同的类中了,类的数量必然增加。但这并不是指子系统设计得不好,相反,这正说明子系统设计得足够好,因为从某种程度上来说,类的数量就体现了程序的健壮性。

回到这个问题,既然外部系统在使用子系统时这么复杂,要和这么多子系统的类打交道,例如:在A_Class6 中为了创建一个 B_Class9 类的对象,我不得不先创建 B_Class10、B_Class3 的对象,而创建 B_Class3 对象又必须先创建 B_Class5 对象。最终,在 A_Class6 中使用 B_Class9 时,会包含这样的代码:B_Class9 class9 = new B_Class9(new B_Class10(), new B_Class3(new B_Class5()));,如果使用者 A_Class6 根本不关心除 B_Class9 之外的对象,那么这个过程对它来说难以忍受。
那么,是否能简化使用方在使用子系统时的复杂度,让使用方将精力放在自己关心的事情上?

1.2 简化交互方式

解决方案就是:门面模式。既然使用方抱怨交互太复杂,那么我就简化子系统的交互逻辑,把这个简化后的逻辑提取到单独的类中去实现,这就是门面模式所要解决的问题。
结构型 - 门面模式(Facade) - 图2
对比前后两张图,在使用门面模式后:

  • 系统之间的耦合度更低了:外部系统不用再依赖子系统的具体内部实现,仅仅通过门面来处理交互逻辑及传递请求,这使得整个依赖关系变得更加简单;
  • 外部系统与子系统之间的交互变得更加清晰,更方便维护:如果我的子系统内部有修改,我们只需要调整门面内部的逻辑处理,而不必修改外部系统的代码;
  • 简化外部系统的使用:我们可以使用缺省值的方式,简化外部系统的使用,例如,可以在 Facade 中各自维护一个 B_Class9、B_Class10、B_Class3、B_Class5 引用,并且在外部系统请求之前,给他们提供缺省的对象,这能为外部系统省去一大部分麻烦。

OK,到此为止,这就是门面模式所有核心的内容。

二、案例实现

2.1 案例背景

前几天和朋友们聚会,大家一时兴起想着去唱 K。时代真的变了,要是 10 年前,大街小巷上都是 KTV 的影子;时过境迁,现在为了找一家 KTV,竟然搜遍了方圆 5 公里,终于在一个小商场的角落里找到了一家。 这家 KTV 只有几个房间,客人也少,不过还是以前的味道,这震耳欲聋的音箱,这目眩神迷的灯光,可不就是当年的模样嘛。房间的门口有一个中控面板,里面有几个按钮,上面分别写着居家模式、Live模式、专业模式等等。在切换模式的时候,房间的灯光和音箱效果会跟着变化。我们就以这个例子来说明门面模式如何使用。这里,为了该例子我们约定,各个模式所对应的效果如下。

  • 居家模式:黄、绿灯光,常亮效果灯光,混响效果音箱;
  • Live模式:黄、绿、红灯光,跑马灯效果灯光,回声效果音箱;
  • 专业模式:绿灯光,频闪效果灯光,原声效果音箱。

在这个案例中,作为用户,我们可以根据喜好选择当前房间的灯光颜色、灯效以及音响效果。我们并未直接控制灯的颜色和效果,也没有控制音箱效果,我们只是在中控面板上选择喜欢的模式,就能切换他们。如果将房间的各种效果比作复杂的子系统,那顾客就是外部系统,中控面板自然就是子系统上层的门面。

2.2 代码实现

(1)设备接口

  1. public interface Equipment {
  2. /**
  3. * 打开设备
  4. */
  5. void on();
  6. /**
  7. * 关闭设备
  8. */
  9. void off();
  10. /**
  11. * 展示设备效果
  12. */
  13. void showEffects();
  14. }

(2)音箱

  1. public abstract class Speaker implements Equipment {
  2. @Override
  3. public void on() {
  4. System.out.println(" 音箱已打开");
  5. }
  6. @Override
  7. public void off() {
  8. System.out.println(" 音箱已关闭");
  9. }
  10. }

(3)音响效果

  1. public class ReverbSoundEffectSpeaker extends Speaker {
  2. @Override
  3. public void showEffects() {
  4. System.out.println(" 音箱使用混响音效");
  5. }
  6. }
  1. public class OriginalSoundEffectSpeaker extends Speaker {
  2. @Override
  3. public void showEffects() {
  4. System.out.println(" 音箱使用原声音效");
  5. }
  6. }
  1. public class EchoSoundEffectSpeaker extends Speaker {
  2. @Override
  3. public void showEffects() {
  4. System.out.println(" 音箱使用回声音效");
  5. }
  6. }

(4)灯光

  1. public abstract class Bulb implements Equipment {
  2. @Override
  3. public void on() {
  4. System.out.println(" " + this.attachEffects() + "已打开");
  5. }
  6. @Override
  7. public void off() {
  8. System.out.println(" " + this.attachEffects() + "已关闭");
  9. }
  10. @Override
  11. public void showEffects() {
  12. System.out.println(" " + this.attachEffects());
  13. }
  14. /**
  15. * 灯光效果
  16. * @return 效果
  17. */
  18. protected abstract String attachEffects();
  19. }
  1. public class GreenBulb extends Bulb {
  2. @Override
  3. public String attachEffects() {
  4. return "绿灯灯光";
  5. }
  6. }
  1. public class RedBulb extends Bulb {
  2. @Override
  3. public String attachEffects() {
  4. return "红色灯光";
  5. }
  6. }
  1. public class YellowBulb extends Bulb {
  2. @Override
  3. public String attachEffects() {
  4. return "黄色灯光";
  5. }
  6. }

(5)灯光效果

  1. public abstract class LightEffectDecorator extends Bulb {
  2. protected final Bulb bulb;
  3. public LightEffectDecorator(Bulb bulb) {
  4. this.bulb = bulb;
  5. }
  6. }
  1. public class BrightEffectDecorator extends LightEffectDecorator {
  2. public BrightEffectDecorator(Bulb bulb) {
  3. super(bulb);
  4. }
  5. @Override
  6. public String attachEffects() {
  7. return "常亮效果的" + super.bulb.attachEffects();
  8. }
  9. }
  1. public class MarqueeEffectDecorator extends LightEffectDecorator {
  2. public MarqueeEffectDecorator(Bulb bulb) {
  3. super(bulb);
  4. }
  5. @Override
  6. public String attachEffects() {
  7. return "跑马灯效果的" + super.bulb.attachEffects();
  8. }
  9. }
  1. public class StrobeEffectDecorator extends LightEffectDecorator {
  2. public StrobeEffectDecorator(Bulb bulb) {
  3. super(bulb);
  4. }
  5. @Override
  6. public String attachEffects() {
  7. return "频闪效果的" + super.bulb.attachEffects();
  8. }
  9. }

(6)中控面板

  1. public enum ModelFacade {
  2. /**
  3. * 唯一实例
  4. */
  5. INSTANCE;
  6. private Equipment redBulb = new MarqueeEffectDecorator(new RedBulb());
  7. private Equipment greenBulb = new MarqueeEffectDecorator(new GreenBulb());
  8. private Equipment yellowBulb = new MarqueeEffectDecorator(new YellowBulb());
  9. private Equipment speaker = new EchoSoundEffectSpeaker();
  10. public void open() {
  11. System.out.println("|==> 打开设备-------------------------------------------------------------|");
  12. this.redBulb.on();
  13. this.greenBulb.on();
  14. this.yellowBulb.on();
  15. this.speaker.on();
  16. this.liveMode();
  17. }
  18. public void close() {
  19. System.out.println("|==> 关闭设备-------------------------------------------------------------|");
  20. this.redBulb.off();
  21. this.greenBulb.off();
  22. this.yellowBulb.off();
  23. this.speaker.off();
  24. }
  25. public void familyMode() {
  26. System.out.println("|==> 居家模式-------------------------------------------------------------|");
  27. this.greenBulb = new BrightEffectDecorator(new GreenBulb());
  28. this.yellowBulb = new BrightEffectDecorator(new YellowBulb());
  29. this.speaker = new ReverbSoundEffectSpeaker();
  30. System.out.println(" 灯光效果:");
  31. this.greenBulb.showEffects();
  32. this.yellowBulb.showEffects();
  33. System.out.println(" 音响效果:");
  34. this.speaker.showEffects();
  35. }
  36. public void liveMode() {
  37. System.out.println("|==> 现场模式-------------------------------------------------------------|");
  38. this.redBulb = new MarqueeEffectDecorator(new RedBulb());
  39. this.greenBulb = new MarqueeEffectDecorator(new GreenBulb());
  40. this.yellowBulb = new MarqueeEffectDecorator(new YellowBulb());
  41. this.speaker = new EchoSoundEffectSpeaker();
  42. System.out.println(" 灯光效果:");
  43. this.redBulb.showEffects();
  44. this.greenBulb.showEffects();
  45. this.yellowBulb.showEffects();
  46. System.out.println(" 音响效果:");
  47. this.speaker.showEffects();
  48. }
  49. public void professionalMode() {
  50. System.out.println("|==> 专业模式-------------------------------------------------------------|");
  51. this.greenBulb = new StrobeEffectDecorator(new GreenBulb());
  52. this.speaker = new OriginalSoundEffectSpeaker();
  53. System.out.println(" 灯光效果:");
  54. this.greenBulb.showEffects();
  55. System.out.println(" 音响效果:");
  56. this.speaker.showEffects();
  57. }
  58. }

(7)客户端使用

  1. public class Client {
  2. public static void main(String[] args) {
  3. // 打开所有设备
  4. ModelFacade.INSTANCE.open();
  5. // 切换到居家模式
  6. ModelFacade.INSTANCE.familyMode();
  7. // 切换到专业模式
  8. ModelFacade.INSTANCE.professionalMode();
  9. // 切换到现场模式
  10. ModelFacade.INSTANCE.liveMode();
  11. // 关闭所有设备
  12. ModelFacade.INSTANCE.close();
  13. }
  14. }
  1. |==> 打开设备-------------------------------------------------------------|
  2. 跑马灯效果的红色灯光已打开
  3. 跑马灯效果的绿灯灯光已打开
  4. 跑马灯效果的黄色灯光已打开
  5. 音箱已打开
  6. |==> 现场模式-------------------------------------------------------------|
  7. 灯光效果:
  8. 跑马灯效果的红色灯光
  9. 跑马灯效果的绿灯灯光
  10. 跑马灯效果的黄色灯光
  11. 音响效果:
  12. 音箱使用回声音效
  13. |==> 居家模式-------------------------------------------------------------|
  14. 灯光效果:
  15. 常亮效果的绿灯灯光
  16. 常亮效果的黄色灯光
  17. 音响效果:
  18. 音箱使用混响音效
  19. |==> 专业模式-------------------------------------------------------------|
  20. 灯光效果:
  21. 频闪效果的绿灯灯光
  22. 音响效果:
  23. 音箱使用原声音效
  24. |==> 现场模式-------------------------------------------------------------|
  25. 灯光效果:
  26. 跑马灯效果的红色灯光
  27. 跑马灯效果的绿灯灯光
  28. 跑马灯效果的黄色灯光
  29. 音响效果:
  30. 音箱使用回声音效
  31. |==> 关闭设备-------------------------------------------------------------|
  32. 跑马灯效果的红色灯光已关闭
  33. 跑马灯效果的绿灯灯光已关闭
  34. 跑马灯效果的黄色灯光已关闭
  35. 音箱已关闭

2.3 结构剖析

该例子对应的类图结构如下图所示:
门面模式.png
在该例子中,使用了统一的门面来处理与子系统的负责交互,相比于未使用门面模式时优势主要在于:

  • 交互对象更单一:客户端只与门面交互;
  • 交互更简单:客户端无需再去构造具体子系统的对象,也无需处理在模式切换时,灯光的开关、灯效、音效等逻辑,门面中已经封装好对应的逻辑,使用即可;
  • 维护更方便:如果模式所对应的灯光效果等发生变化,只需修改门面即可,客户端无需任何调整;

    三、门面模式

    3.1 意图

    为子系统中的一组接口提供一个一致的界面,门面模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

门面模式要实现的目的是为了让子系统变得更加容易使用,实现方式是定义一个高层的接口(使用者通过这个高层接口和子系统进行交互),最终的效果就是给子系统的一组接口提供了一个一致的界面。

3.2 使用小技巧

(1)在恰当的地方使用门面模式
在门面模式的定义中,已经指明了使用它的效果——给子系统增加一个门面,对于客户端来说,将变得更加容易使用,这一点尤其重要。如果给子系统加上门面后,仍然不能降低负责度,则不应该使用门面模式(或者打开的方式不对)。
(2)屏蔽那些用户不关心的细节
如在上面的例子中,作为用户的我,不需要知道灯光是如何产生的,灯效是如何切换的,我需要的是打开所有的设备,然后选择一个我喜欢的场景而已。屏蔽掉那些对客户端来说用处不大的细节之后,客户端的使用才能变得简单。
(3)灵活使用门面模式
使用门面模式时应该注意注重其意,而不在于形。拨开现象看本质,门面模式说到底还是为了客户端使用子系统更简单,只要抓住这一点便不会弄巧成拙。至于如何实现,并不会限制于某一种特定的方式。比如为了让门面类具有子系统类的特性,我们可以让门面类继承或持有某些具体的类。再比如,当多个客户端使用子系统,且这些客户端在某些细节上的期望不一致时,我们可以定义抽象的门面,为多个客户端提供差异化的具体门面,以此来满足不同客户端的需求。
(4)不要限制客户越过门面
我们发现:为了让子系统更加方便使用,我们屏蔽了很多细节(使用了缺省值)。但我们无法预知用户每一次的需求,也不可能为所有的用户都提供一个通用并且简单的接口。比如,在上面的例子中,某个用户希望在居家模式下使用的灯光是黄红而不是黄绿,此时子系统应该如何做呢?

答案是:子系统什么也不应该做。既然这种需求属于个例,那么就特殊对待,子系统不限制客户端对底层类的直接使用,那么客户就可以根据底层类实现自己需要的功能。

所以,门面模式从某种意义上来说是属于对子系统使用的一种优化。门面模式希望的是在大多数的情况下,都能为客户端提供良好的服务,但如果有需求超出了门面的能力范围,客户端就应该越过门面,在更底层去寻找答案。

附录

案例代码:…/facade