本文摘取自TerryLee(李会军)老师的设计模式系列文章,版权归TerryLee,仅供个人学习参考。转载请标明原作者TerryLee。部分示例代码来自DoFactory。
概述
在软件开发系统中,客户程序经常会与复杂系统的内部子系统之间产生耦合,而导致客户程序随着子系统的变化而变化。那么如何简化客户程序与子系统之间的交互接口?如何将复杂系统的内部子系统与客户程序之间的依赖解耦?这就是要说的外观模式(Façade Pattern)。
意图
为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
结构图
外观模式没有一个一般化的类图描述,下面是一个示意性的对象图:
图1 外观模式结构图
参与者
Façade是一个提供"接口"(一系列方法和属性)来方便客户端使用复杂子系统中类和对象的类。Façade模式是一个简单且看起来作用不大的模式。但是,对于这种在基于3层架构模型设计的系统中常用的设计模式其影响是深远的。在一个3层应用中展现层是一个客户端。对业务层的调用通过一个良好定义的服务层来完成。这个服务层,或称façade,隐藏了业务对象及其间交互的复杂性。
另一个使用Façade模式的领域是在进行重构时。如果你正在处理一系列混乱或麻烦的遗留类并且客户端程序员没有把其隐藏在Façade后。Façade只应将必要的暴露给客户端,并且以良好组织的接口且易于使用方式来呈现。
外观模式通常与其它设计模式合并使用。Façade本身常实现为单例的抽象工厂。然而,你也可以在Façade使用静态方法来得到相同的效果。
这个模式涉及的类或对象:
-
Façade
-
了解哪些子系统的类负责处理一个请求。
-
将客户端的请求委托给适当的子系统对象。
-
Subsystem classes
-
实现子系统功能。
-
处理Façade对象分配的工作。
适用性
-
为一个复杂子系统提供一个简单接口。子系统往往因为不断演化而变得越来越复杂。大多数模式使用时都会产生更多更小的类。这使得子系统更具可重用性,也更容易对子系统进行定制,但这也给那些不需要定制子系统的用户带来一些使用上的困难。Facade可以提供一个简单的缺省视图,这一视图对大多数用户来说已经足够,而那些需要更多的可定制性的用户可以越过 Facade 层。
-
提高子系统的独立性。客户程序与抽象类的实现部分之间存在着很大的依赖性。引入 Facade将这个子系统与客户以及其他的子系统分离,可以提高子系统的独立性和可移植性。
-
当你需要构建一个层次结构的子系统时,可以使用外观模式定义系统中每一层的入口。如果子系统之间是相互依赖的,你可以让它们仅通过Facade进行通讯,从而简化了它们之间的依赖关系。
-
在维护一个遗留的大型系统时,可能这个系统已经非常难以维护和扩展了,为新系统开发一个外观Façade类,来为提供设计粗糙或高度复杂的遗留代码的比较清晰简单的接口,让新系统与Façade对象交互,Façade与遗留代码交互所有复杂的工作。
DoFactory GoF代码
Façade模式为一个大型子系统的类提供了简单且统一的接口。
// Facade pattern // Structural example using System; namespace DoFactory.GangOfFour.Facade.Structural { // Mainapp test application class MainApp { public static void Main() { Facade facade = new Facade(); facade.MethodA(); facade.MethodB(); // Wait for user Console.ReadKey(); } } // "Subsystem ClassA" class SubSystemOne { public void MethodOne() { Console.WriteLine(" SubSystemOne Method"); } } // Subsystem ClassB" class SubSystemTwo { public void MethodTwo() { Console.WriteLine(" SubSystemTwo Method"); } } // Subsystem ClassC" class SubSystemThree { public void MethodThree() { Console.WriteLine(" SubSystemThree Method"); } } // Subsystem ClassD" class SubSystemFour { public void MethodFour() { Console.WriteLine(" SubSystemFour Method"); } } // "Facade" class Facade { private SubSystemOne _one; private SubSystemTwo _two; private SubSystemThree _three; private SubSystemFour _four; public Facade() { _one = new SubSystemOne(); _two = new SubSystemTwo(); _three = new SubSystemThree(); _four = new SubSystemFour(); } public void MethodA() { Console.WriteLine(" MethodA() ---- "); _one.MethodOne(); _two.MethodTwo(); _four.MethodFour(); } public void MethodB() { Console.WriteLine(" MethodB() ---- "); _two.MethodTwo(); _three.MethodThree(); } } }
现实应用的例子中演示了外观模式在一个评估申请者信用评级的抵押应用程序中的应用,Façade为一个大型子系统的类提供了一个简单的接口。
例子中涉及到的类与适配器模式中标准的类对应关系如下:
-
Façade – MortgageApplication
-
Subsystem classes – Bank, Credit, Loan
// Facade pattern // RealWorld example using System; namespace DoFactory.GangOfFour.Facade.RealWorld { // MainApp test application class MainApp { static void Main() { // Facade Mortgage mortgage = new Mortgage(); // Evaluate mortgage eligibility for customer Customer customer = new Customer("Ann McKinsey"); bool eligible = mortgage.IsEligible(customer, 125000); Console.WriteLine(" " + customer.Name + " has been " + (eligible ? "Approved" : "Rejected")); // Wait for user Console.ReadKey(); } } // "Subsystem ClassA" class Bank { public bool HasSufficientSavings(Customer c, int amount) { Console.WriteLine("Check bank for " + c.Name); return true; } } // "Subsystem ClassB" class Credit { public bool HasGoodCredit(Customer c) { Console.WriteLine("Check credit for " + c.Name); return true; } } // "Subsystem ClassC" class Loan { public bool HasNoBadLoans(Customer c) { Console.WriteLine("Check loans for " + c.Name); return true; } } // Customer class class Customer { private string _name; // Constructor public Customer(string name) { this._name = name; } // Gets the name public string Name { get { return _name; } } } // "Facade" class Mortgage { private Bank _bank = new Bank(); private Loan _loan = new Loan(); private Credit _credit = new Credit(); public bool IsEligible(Customer cust, int amount) { Console.WriteLine("{0} applies for {1:C} loan ", cust.Name, amount); bool eligible = true; // Check creditworthyness of applicant if (!_bank.HasSufficientSavings(cust, amount)) { eligible = false; } else if (!_loan.HasNoBadLoans(cust)) { eligible = false; } else if (!_credit.HasGoodCredit(cust)) { eligible = false; } return eligible; } } }
为.NET优化的代码实现了与现实应用代码一致的功能。唯一的不同是在Customer类中使用了.NET 3.0的自动属性和对象初始化器。
// Facade pattern // .NET Optimized example using System; namespace DoFactory.GangOfFour.Facade.NETOptimized { class MainApp { static void Main() { // Facade var mortgage = new Mortgage(); // Evaluate mortgage eligibility for customer var customer = new Customer { Name = "Ann McKinsey" }; bool eligible = mortgage.IsEligible(customer, 125000); Console.WriteLine(" " + customer.Name + " has been " + (eligible ? "Approved" : "Rejected")); // Wait for user Console.ReadKey(); } } // "Subsystem ClassA" class Bank { public bool HasSufficientSavings(Customer c, int amount) { Console.WriteLine("Check bank for " + c.Name); return true; } } // "Subsystem ClassB" class Credit { public bool HasGoodCredit(Customer c) { Console.WriteLine("Check credit for " + c.Name); return true; } } // "Subsystem ClassC" class Loan { public bool HasNoBadLoans(Customer c) { Console.WriteLine("Check loans for " + c.Name); return true; } } // "Facade" class Mortgage { private Bank _bank = new Bank(); private Loan _loan = new Loan(); private Credit _credit = new Credit(); public bool IsEligible(Customer cust, int amount) { Console.WriteLine("{0} applies for {1:C} loan ", cust.Name, amount); bool eligible = true; // Check creditworthyness of applicant if (!_bank.HasSufficientSavings(cust, amount)) { eligible = false; } else if (!_loan.HasNoBadLoans(cust)) { eligible = false; } else if (!_credit.HasGoodCredit(cust)) { eligible = false; } return eligible; } } // "Customer class" class Customer { // Gets or sets the name public string Name { get; set; } } }
来自《大话设计模式》的例子
这个例子中通过购买基金的例子来模拟外观模式,购买基金与购买股票的差异就在于后者需要投资人自己够买并管理多只股票,而前者只需购买管理一只基金,而由基金公司来购买运作多只股票,基金公司正是作为一个外观类存在。
using System; namespace FacadePattern { class Program { static void Main(string[] args) { Fund jijin = new Fund(); jijin.BuyFund(); jijin.SellFund(); Console.Read(); } } class Fund { Stock1 gu1; Stock2 gu2; Stock3 gu3; NationalDebt1 nd1; Realty1 rt1; public Fund() { gu1 = new Stock1(); gu2 = new Stock2(); gu3 = new Stock3(); nd1 = new NationalDebt1(); rt1 = new Realty1(); } public void BuyFund() { gu1.Buy(); gu2.Buy(); gu3.Buy(); nd1.Buy(); rt1.Buy(); } public void SellFund() { gu1.Sell(); gu2.Sell(); gu3.Sell(); nd1.Sell(); rt1.Sell(); } } //股票1 class Stock1 { //卖股票 public void Sell() { Console.WriteLine(" 股票1卖出"); } //买股票 public void Buy() { Console.WriteLine(" 股票1买入"); } } //股票2 class Stock2 { //卖股票 public void Sell() { Console.WriteLine(" 股票2卖出"); } //买股票 public void Buy() { Console.WriteLine(" 股票2买入"); } } //股票3 class Stock3 { //卖股票 public void Sell() { Console.WriteLine(" 股票3卖出"); } //买股票 public void Buy() { Console.WriteLine(" 股票3买入"); } } //国债1 class NationalDebt1 { //卖国债 public void Sell() { Console.WriteLine(" 国债1卖出"); } //买国债 public void Buy() { Console.WriteLine(" 国债1买入"); } } //房地产1 class Realty1 { //卖房地产 public void Sell() { Console.WriteLine(" 房产1卖出"); } //买房地产 public void Buy() { Console.WriteLine(" 房产1买入"); } } }
Facade模式解说
我们平时的开发中其实已经不知不觉的在用外观模式,现在来考虑这样一个抵押系统,当有一个客户来时,有如下几件事情需要确认:到银行子系统查询他是否有足够多的存款,到信用子系统查询他是否有良好的信用,到贷款子系统查询他有无贷款劣迹。只有这三个子系统都通过时才可进行抵押。我们先不考虑Façade模式,那么客户程序就要直接访问这些子系统,分别进行判断。类结构图下:
图2.基本抵押系统的结构图
在这个程序中,我们首先要有一个顾客类,它是一个纯数据类,并无任何操作,示意代码:
//顾客类 public class Customer { private string _name; public Customer(string name) { this._name = name; } public string Name { get { return _name; } } }
下面这三个类均是子系统类,示意代码:
//银行子系统 public class Bank { public bool HasSufficientSavings(Customer c, int amount) { Console.WriteLine("Check bank for " + c.Name); return true; } } //信用子系统 public class Credit { public bool HasGoodCredit(Customer c) { Console.WriteLine("Check credit for " + c.Name); return true; } } //贷款子系统 public class Loan { public bool HasNoBadLoans(Customer c) { Console.WriteLine("Check loans for " + c.Name); return true; } }
来看客户程序的调用:
//客户程序 public class MainApp { private const int _amount = 12000; public static void Main() { Bank bank = new Bank(); Loan loan = new Loan(); Credit credit = new Credit(); Customer customer = new Customer("Ann McKinsey"); bool eligible = true; if (!bank.HasSufficientSavings(customer, _amount)) { eligible = false; } else if (!loan.HasNoBadLoans(customer)) { eligible = false; } else if (!credit.HasGoodCredit(customer)) { eligible = false; } Console.WriteLine(" " + customer.Name + " has been " + (eligible ? "Approved" : "Rejected")); Console.ReadLine(); } }
可以看到,在不用Façade模式的情况下,客户程序与三个子系统都发生了耦合,这种耦合使得客户程序依赖于子系统,当子系统变化时,客户程序也将面临很多变化的挑战。一个合情合理的设计就是为这些子系统创建一个统一的接口,这个接口简化了客户程序的判断操作。看一下引入Façade模式后的类结构图:
图3.增加了Façade的抵押系统的结构图
门面类Mortage的实现如下:
//外观类 public class Mortgage { private Bank bank = new Bank(); private Loan loan = new Loan(); private Credit credit = new Credit(); public bool IsEligible(Customer cust, int amount) { Console.WriteLine("{0} applies for {1:C} loan ", cust.Name, amount); bool eligible = true; if (!bank.HasSufficientSavings(cust, amount)) { eligible = false; } else if (!loan.HasNoBadLoans(cust)) { eligible = false; } else if (!credit.HasGoodCredit(cust)) { eligible = false; } return eligible; } }
顾客类和子系统类的实现仍然如下:
//银行子系统 public class Bank { public bool HasSufficientSavings(Customer c, int amount) { Console.WriteLine("Check bank for " + c.Name); return true; } } //信用证子系统 public class Credit { public bool HasGoodCredit(Customer c) { Console.WriteLine("Check credit for " + c.Name); return true; } } //贷款子系统 public class Loan { public bool HasNoBadLoans(Customer c) { Console.WriteLine("Check loans for " + c.Name); return true; } } //顾客类 public class Customer { private string name; public Customer(string name) { this.name = name; } public string Name { get { return name; } } }
而此时客户程序的实现:
//客户程序类 public class MainApp { public static void Main() { //外观 Mortgage mortgage = new Mortgage(); Customer customer = new Customer("Ann McKinsey"); bool eligable = mortgage.IsEligible(customer, 125000); Console.WriteLine(" " + customer.Name + " has been " + (eligable ? "Approved" : "Rejected")); Console.ReadLine(); } }
可以看到引入Façade模式后,客户程序只与Mortgage发生依赖,也就是Mortgage屏蔽了子系统之间的复杂的操作,达到了解耦内部子系统与客户程序之间的依赖。
.NET架构中的外观模式
在.NET Framework中可以找到许多外观模式的实现。它用于需要将复杂的类型集合以简单的接口对外提供的场景。要恰当的讨论这个问题我们需要区分高层架构中的Façade与较低的组件级的façade类型。Microsoft给出的针对面向组件设计中的较低层次façade类型的术语是-聚合对象。事实上它们是纯正的façade,因为它们为多个较低层次的类型和API提供了一个高层次的接口,以实现支持通用编程场景。
聚合组件的一个例子是System.Disgnostics.EventLog。它向客户端提供了一个简单的API,客户端只关注创建EventLog的实例,设置并获取一些属性,并且将事件写入服务器的事件日志中。打开关闭,读写等复杂操作对客户端完全透明。另一个些聚合组件的例子:用于发送邮件的System.Web.Mail.SmtpMail,用于串口通信的System.IO.SerialPort,用于提供访问消息队列服务器中的队列System.Messaging.MessageQueue类以及用于发送与接收指定位置网络资源的System.Net.WebClient这个提供高层接口的类型。
对组件库中Façade的讨论总是需要仔细考虑。对.NET Framwork的类库也不例外。.NET类库的目标是提供给程序员一个高度可控的平台,然而Façade的目标是简化并限制.NET开发人员可以接触的东西。Facades可能会降低API的可表达性,但同时提供的类型应该易于理解和使用。
外观模式在实际开发中最多的运用当属开发N层架构的应用程序了,一个典型的N层结构如下:
图4.N层架构中外观模式的应用架构图
在这个架构中,总共分为四个逻辑层,分别为:用户层UI,业务外观层Business Façade,业务规则层Business Rule,数据访问层Data Access。其中Business Façade层的职责如下:
-
从"用户"层接收用户输入
-
如果请求需要对数据进行只读访问,则可能使用"数据访问"层
-
将请求传递到"业务规则"层
-
将响应从"业务规则"层返回到"用户"层
-
在对"业务规则"层的调用之间维护临时状态
对这一架构最好的体现就是Duwamish示例了。在该应用程序中,有部分操作只是简单的从数据库根据条件提取数据,不需要经过任何处理,而直接将数据显示到网页上,比如查询某类别的图书列表。而另外一些操作,比如计算定单中图书的总价并根据顾客的级别计算回扣等等,这部分往往有许多不同的功能的类,操作起来也比较复杂。如果采用传统的三层结构,这些商业逻辑一般是会放在中间层,那么对内部的这些大量种类繁多,使用方法也各异的不同的类的调用任务,就完全落到了表示层。这样势必会增加表示层的代码量,将表示层的任务复杂化,和表示层只负责接受用户的输入并返回结果的任务不太相称,并增加了层与层之间的耦合程度。于是就引入了一个Façade层,让这个Facade来负责管理系统内部类的调用,并为表示层提供了一个单一而简单的接口。看一下Duwamish结构图:
图5.Duwamish中外观模式的应用
从图中可以看到,UI层将请求发送给业务外观层,业务外观层对请求进行初步的处理,判断是否需要调用业务规则层,还是直接调用数据访问层获取数据。最后由数据访问层访问数据库并按照来时的步骤返回结果到UI层,来看具体的代码实现。
在获取商品目录的时候,Web UI调用业务外观层:
productSystem = new ProductSystem(); categorySet = productSystem.GetCategories(categoryID);
业务外观层直接调用了数据访问层:
public CategoryData GetCategories(int categoryId) { // Check preconditions ApplicationAssert.CheckCondition(categoryId >= 0,"Invalid Category Id",ApplicationAssert.LineNumber); // Retrieve the data using (Categories accessCategories = new Categories()) { return accessCategories.GetCategories(categoryId); } }
在添加订单时,UI调用业务外观层:
public void AddOrder() { ApplicationAssert.CheckCondition(cartOrderData != null, "Order requires data", ApplicationAssert.LineNumber); //Write trace log. ApplicationLog.WriteTrace("Duwamish7.Web.Cart.AddOrder: CustomerId: " + cartOrderData.Tables[OrderData.CUSTOMER_TABLE].Rows[0][OrderData.PKID_FIELD].ToString()); cartOrderData = (new OrderSystem()).AddOrder(cartOrderData); }
业务外观层调用业务规则层:
public OrderData AddOrder(OrderData order) { // Check preconditions ApplicationAssert.CheckCondition(order != null, "Order is required", ApplicationAssert.LineNumber); (new BusinessRules.Order()).InsertOrder(order); return order; }
业务规则层进行复杂的逻辑处理后,再调用数据访问层:
public bool InsertOrder(OrderData order) { // Assume it's good bool isValid = true; // Validate order summary DataRow summaryRow = order.Tables[OrderData.ORDER_SUMMARY_TABLE].Rows[0]; summaryRow.ClearErrors(); if (CalculateShipping(order) != (Decimal)(summaryRow[OrderData.SHIPPING_HANDLING_FIELD])) { summaryRow.SetColumnError(OrderData.SHIPPING_HANDLING_FIELD, OrderData.INVALID_FIELD); isValid = false; } if (CalculateTax(order) != (Decimal)(summaryRow[OrderData.TAX_FIELD])) { summaryRow.SetColumnError(OrderData.TAX_FIELD, OrderData.INVALID_FIELD); isValid = false; } // Validate shipping info isValid &= IsValidField(order, OrderData.SHIPPING_ADDRESS_TABLE, OrderData.SHIP_TO_NAME_FIELD, 40); // Validate payment info DataRow paymentRow = order.Tables[OrderData.PAYMENT_TABLE].Rows[0]; paymentRow.ClearErrors(); isValid &= IsValidField(paymentRow, OrderData.CREDIT_CARD_TYPE_FIELD, 40); isValid &= IsValidField(paymentRow, OrderData.CREDIT_CARD_NUMBER_FIELD, 32); isValid &= IsValidField(paymentRow, OrderData.EXPIRATION_DATE_FIELD, 30); isValid &= IsValidField(paymentRow, OrderData.NAME_ON_CARD_FIELD, 40); isValid &= IsValidField(paymentRow, OrderData.BILLING_ADDRESS_FIELD, 255); // Validate the order items and recalculate the subtotal DataRowCollection itemRows = order.Tables[OrderData.ORDER_ITEMS_TABLE].Rows; Decimal subTotal = 0; foreach (DataRow itemRow in itemRows) { itemRow.ClearErrors(); subTotal += (Decimal)(itemRow[OrderData.EXTENDED_FIELD]); if ((Decimal)(itemRow[OrderData.PRICE_FIELD]) <= 0) { itemRow.SetColumnError(OrderData.PRICE_FIELD, OrderData.INVALID_FIELD); isValid = false; } if ((short)(itemRow[OrderData.QUANTITY_FIELD]) <= 0) { itemRow.SetColumnError(OrderData.QUANTITY_FIELD, OrderData.INVALID_FIELD); isValid = false; } } // Verify the subtotal if (subTotal != (Decimal)(summaryRow[OrderData.SUB_TOTAL_FIELD])) { summaryRow.SetColumnError(OrderData.SUB_TOTAL_FIELD, OrderData.INVALID_FIELD); isValid = false; } if ( isValid ) { using (DataAccess.Orders ordersDataAccess = new DataAccess.Orders()) { return (ordersDataAccess.InsertOrderDetail(order)) > 0; } } else return false; }
效果及实现要点
-
Façade模式对客户屏蔽了子系统组件,因而减少了客户处理的对象的数目并使得子系统使用起来更加方便。
-
Façade模式实现了子系统与客户之间的松耦合关系,而子系统内部的功能组件往往是紧耦合的。松耦合关系使得子系统的组件变化不会影响到它的客户。
-
如果应用需要,它并不限制它们使用子系统类。因此你可以在系统易用性与通用性之间选择。
总结
Façade模式注重的是简化接口,它更多的时候是从架构的层次去看整个系统,而并非单个类的层次。