1 定义:
外观模式(Façade Pattern)
Provide a unified interface to a set of interfaces in a subsystem. Façade defines a higher-level interface that makes the subsystem easier to use.
(要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。)
简言之,门面对象是外界访问子系统内部的唯一通道,力求“金玉其外,败絮其中”。
1.1 通用类图:
门面角色(Façade):客户端可以调用这个角色的方法。它知晓子系统的所有功能和责任。该角色没有实际的业务逻辑,只是一个委托类,将客户端发来的请求委派到相应的子系统去。
子系统角色(Subsystem):可以同时有一个或多个子系统。每个子系统都是一个类的集合。子系统不知道门面的存在,门面相对于子系统仅是另外一个客户端而已。
1.2 通用代码:
- public class ClassA {
- public void doSomethingA() {
- // 业务逻辑
- }
- }
- public class ClassB {
- public void doSomethingB() {
- // 业务逻辑
- }
- }
- public class ClassC {
- public void doSomethingC() {
- // 业务逻辑
- }
- }
- public class Facade {
- // 被委托的对象
- private ClassA a = new ClassA();
- private ClassB b = new ClassB();
- private ClassC c = new ClassC();
- // 提供给外部访问的方法
- public void methodA() {
- this.a.doSomethingA();
- }
- public void methodB() {
- this.b.doSomethingB();
- }
- public void methodC() {
- this.c.doSomethingC();
- }
- }
2 优点
- 对客户屏蔽子系统组件,减少了客户处理的对象数目并使得子系统使用起来更加容易。通过引入外观模式,客户代码将变得很简单,与之关联的对象也很少。
- 实现了子系统与客户之间的松耦合关系,这使得子系统的组件变化不会影响到调用它的客户类,只需要调整外观类即可。
- 降低了大型软件系统中的编译依赖性,并简化了系统在不同平台之间的移植过程,因为编译一个子系统一般不需要编译所有其他的子系统。一个子系统的修改对其他子系统没有任何影响,而且子系统内部变化也不会影响到外观对象。
- 只是提供了一个访问子系统的统一入口,并不影响用户直接使用子系统类。
3 缺点
- 不能很好地限制客户使用子系统类,如果对客户访问子系统类做太多的限制则减少了可变性和灵活性。
- 在不引入抽象外观类的情况下,增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。
4 应用场景
在遇到以下情况使用facade模式:
- 当你要为一个复杂子系统提供一个简单接口时。子系统往往因为不断演化而变得越来越复杂。大多数模式使用时都会产生更多更小的类。
这一视图对大多数用户来说已经足够,而那些需要更多的可定制性的用户可以越过facade层。
- 客户程序与抽象类的实现部分之间存在着很大的依赖性<客户程序与多个子系统之间存在很大的依赖性>。引入 facade将这个子系统与客户以及其他的子系统分离,可以提高子系统的独立性 和可移植性。
- 当你需要构建一个层次结构的子系统时,使用 facade模式定义子系统中每层的入口点。如果子系统之间是相互依赖的,你可以让它们仅通过facade进行通讯,从而简化了它们之间的依赖关系,同时预防低水平人员开带来的风险扩散(为其限定某一子系统,供其开发),常见的三层架构就是外观模式的体现。
5 注意事项
5.1 一个子系统可以有多个门面
A。门面已经在到不能忍受的程度(比如一个门面对象已经超过了200行代码),虽然都是非常简单的委托操作,也建议拆分成多个门面,否则会给以后的维护和扩展带来不必要的麻烦。如何拆分?可以按功能:如数据库操作的门面可以分为查询门面、删除门面、更新门面等。
B。子系统可以提供不同访问路径:以通用源码为例,ClassA、ClassB、ClassC是一个子系统中的3个对象,现在有两个不同的高层模块来访问该子系统,模块一是子系统的信任模块,可以完整的访问子系统的所有业务逻辑;而模块二属于受限访问对象,只能访问methodB方法。此时,就需要建立两个门面以供不同的高层模块来访问,即在原有代码中增加一个门面即可。代码如下:
- public class Facade2 {
- //引用原有的门面
- private Façade façade = new Façade();
- //对外提供唯一的访问子系统的方法
- public void methodB(){
- This.facade.methodB();
- }
- }
为什么要使用委托而不再编写一个委托到子系统的方法呢?那是因为在面向对象的编程中,尽量保持相同的代码只编写一遍,避免以后到处修改相似代码的悲剧。
5.2 门面不参与子系统内的业务逻辑
以通用代码为例:如果methodC中必须先调用ClassA的doSomethingA,然后再调用 ClassC 的doSomethingC方法呢?如何改。但千万别这么做:
- public void methodC( ) {
- this.a.doSomethingA();
- this.c.doSomethingC();
- }
为什么不要这么做呢?因为你已经让门面对象参与了业务逻辑,而门面对象只提供了个访问子系统的一个路径而已。否则就会产生一个倒依赖的问题:子系统必须依赖门面才能被访问,这是设计上的一个严重错误,不仅违反了单一职责原则,同时也破坏了系统的封装性。
解决之道:建立一个封装类,封装完毕后提供给门面对象。
最终修改如下:
- public class Context {
- // 委托处理
- private ClassA a = new ClassA();
- private ClassC c = new ClassC();
- // 复杂的计算
- public void complexMethod() {
- this.a.doSomethingA();
- this.c.doSomethingC();
- }
- }
- public class Facade {
- // 被委托的对象
- private ClassA a = new ClassA();
- private ClassB b = new ClassB();
- private Context context = new Context();
- // 提供给外部访问的方法
- public void methodA() {
- this.a.doSomethingA();
- }
- public void methodB() {
- this.b.doSomethingB();
- }
- public void methodC() {
- this.context.complexMethod();
- }
- }
5.3外观模式与迪米特法则:
外观模式创造出一个外观对象,将客户端所涉及的属于一个子系统的协作伙伴的数量减到最少,使得客户端与子系统内部的对象的相互作用被外观对象所取代。外观类充当了客户类与子系统类之间的“第三者”,降低了客户类与子系统类之间的耦合度,外观模式就是实现代码重构以便达到“迪米特法则”要求的一个强有力的武器。
5.4 抽象外观类的引入:
外观模式最大的缺点在于违背了“开闭原则”,而抽象外观类可一定程度上解决该问题:
当增加新的子系统或者移除子系统时需要修改外观类,可以通过引入抽象外观类在一定程度上解决该问题,客户端针对抽象外观类进行编程。对于新的业务需求,不修改原有外观类,而对应增加一个新的具体外观类,由新的具体外观类来关联新的子系统对象,同时通过修改配置文件来达到不修改源代码并更换外观类的目的。
6 扩展
暂无
7 范例
7.1睡觉看电视外观类应用
晚上睡觉之前,你总是喜欢看电视,在你进入卧室的时候你需要完成以下几个步骤:打开电灯、打开空调、放
心银幕(假如你家有)、打开电视通过这么些繁琐的步骤后你终于可以看电视了,但是你要睡觉了呢?又要去进行繁琐
的关闭动作。这里你就需要一个外观模式了,通过实现一个更加合理的接口外观类将这些动作都包装起来,实现一
键“看电视”、一键“关电视”。这就是外观模式的动机
实例的UML图
首先是四个组件(电视、电灯、空调、银幕)
然后是比较强大、干净、美观的外观
客户端
运行结果
从上面的使用通过使用外观模式,客户可以非常方便的实现比较复杂的功能。
7.2 用户登陆验证的门面小例
类图
源代码
- package _17_Facade.mixed;
- public interface ILoginCheck {
- public boolean login(String user, String password);
- }
- public class LoginCheck implements ILoginCheck {
- private Database db;
- @Override
- public boolean login(String user, String password) {
- db = new Database();//
- // 检查格式
- if (!FormatCheck.isUserNameValid(user))
- return false;
- if (!FormatCheck.isPasswordValid(password))
- return false;
- // 检查是否存在用户
- if (!isUserExist(user))
- return false;
- // 检查密码是否正确
- if (!password.equals(this.getUserPassword(user)))
- return false;
- return true;
- }
- private boolean isUserExist(String user) {
- return db.isUserExist(user);
- }
- private String getUserPassword(String user) {
- return db.getUserPassword(user);
- }
- }
- public class FormatCheck {
- public static boolean isUserNameValid(String user) {
- return true;
- }
- public static boolean isPasswordValid(String password) {
- return true;
- }
- }
- public class Database {
- public boolean isUserExist(String user) {
- return true;
- }
- public String getUserPassword(String user) {
- return new String("123456");
- }
- }
- public class Login {
- // private LoginCheck check = new LoginCheck();
- private ILoginCheck check = new LogDecorator(new LoginCheck());
- public boolean login(String user, String password) {
- return check.login(user, password);
- }
- }
- public class Client {
- public static void main(String args[]) {
- Login login = new Login();
- boolean result = login.login("Tom", "123456");
- System.out.println("登陆帐号:Tom , 123456 .结果为: " + result);
- }
- }
结果如下:
登陆帐号:Tom , 123456 .结果为 : true
7.3外观装饰类
承上例,业务变化(为加强系统安全),系统需要记录每次登陆的用户名。(在子系统内扩张,这里因为系统已经投产,就小小地结合使用一下装饰模式吧)
类图如下:
源代码(仅需添加如下两类,其它无需作任何变化)
- import java.util.Date;
- import java.text.DateFormat;
- public class Decorator implements ILoginCheck {
- protected ILoginCheck check;
- public Decorator(ILoginCheck check) {
- this.check = check;
- }
- @Override
- public boolean login(String user, String password) {
- return check.login(user, password);
- }
- }
- public class LogDecorator extends Decorator {
- public LogDecorator(ILoginCheck check) {
- super(check);
- }
- public void writeLog(String user) {
- Date now = new Date();
- DateFormat df = DateFormat.getDateTimeInstance(DateFormat.MEDIUM,
- DateFormat.MEDIUM);
- // 显示日期,时间(精确到分)
- System.out.println("---------" + user + " login at " + df.format(now)
- + "----------");
- }
- @Override
- public boolean login(String user, String password) {
- boolean result = super.login(user, password);
- if (result)
- writeLog(user);
- return result;
- }
- }
结果如下:
---------Tom login at 2012-5-17 17:24:03----------
登陆帐号:Tom , 123456 .结果为 : true