在软件开发中,有时候为了完成一项较为复杂的功能,一个类需要和多个其他业务类交互,而这些需要交互的业务类经常会作为一个完整的整体出现,由于涉及的类比较多,导致使用时代码较为复杂,此时,特别需要一个类似服务员一样的角色,由他来负责和多个业务类进行交互,而使用这些业务类的类只需要和该类进行交互即可。外观模式通过引入一个新的外观类来实现该功能,外观类充当了软件系统中的“服务员”,它为多个业务类的调用提供了一个统一的入口,简化了类与类之间的交互。
外观模式(Facade) | 学习难度:★☆☆☆☆ | 使用频率:★★★★★ |
一、文件加密模块设计
1.1 需求背景
M公司想要开发一个用于多个软件的文件加密模块,该模块可以对文件中的数据进行加密并将加密后的数据存储在一个新文件中,具体的流程包括3个部分,分别是读取源文件、加密、保存加密之后的文件。其中,读取文件和保存文件使用流来实现,加密操作通过求模运算实现。这3个操作相对独立,为了实现代码地独立重用,让设计更加符合单一职责原则,这3个操作的业务代码封装在3个不同的类中。
1.2 初始设计
M公司开发人员独立实现了这3个具体业务类:FileReader用于读取文件,CipherMachine用于对数据加密,FileWriter用于保存文件。由于该文件加密模块的通用性,它在M公司开发的多款软件中都得以使用,包括财务管理软件、公文审批系统、邮件管理系统等,如下图所示:
从上图中不难发现,在每一次使用这3个类时都需要编写代码与它们逐个进行交互,客户端代码如下:
public static void Main() { FileReader reader = new FileReader(); // 文件读取类 CipherMachine cipher = new CipherMachine(); // 数据加密类 FileWriter writer = new FileWriter(); // 文件保存类 reader = new FileReader(); cipher = new CipherMachine(); writer = new FileWriter(); string plainStr = reader.Read("Facade/src.txt"); // 读取源文件 string encryptStr = cipher.Encrypt(plainStr); // 加密 writer.Write(encryptStr, "Facade/des.txt"); // 将加密结果写入新文件 }
经过分析后发现,该方案虽然能够实现预期功能,但存在以下2个问题:
(1)FileReader、CipherMachie与FileWriter类经常作为一个整体同时出现,但是如果按照上述方案进行设计和实现,在每一次使用这3个类时,客户端代码都需要与它们逐个进行交互,导致客户端代码较为复杂,且在每次使用它们时很多代码都会重复出现。
(2)如果需要更换一个加密类,例如将CipherMachine改为NewCipherMachine,则所有使用该文件加密模块的代码都需要进行修改,系统维护难度增大,灵活性和可扩展性较差。
二、外观模式概述
2.1 外观模式简介
根据单一职责原则,在软件中将一个系统划分为若干个子系统有利于降低整个系统的复杂性,一个常见的设计目标就是使客户类与子系统之间的通信和相互依赖关系达到最小,而达到该目标的途径之一就是引入一个外观(Facade)角色,它为子系统的访问提供了一个简单而单一的入口。
外观(Facade)模式:外部与一个子系统的通信通过一个统一的外观角色进行,为子系统中的一组接口提供一个一致的入口,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
2.2 外观模式结构与角色
外观模式没有一个一般化的类图描述,通常使用示意图来表示外观模式,如下图所示:
当然,下图所示的类图也可以作为外观模式的结构型描述形式之一。
外观模式主要包含两个角色:
(1)Facade(外观角色):在客户端可以调用这个角色的方法,在外观角色中可以知道相关的子系统的功能和责任;在正常情况下,它将所有从客户端发来的请求委派到相应的子系统中去,传递给相应的子系统对象处理。
(2)SubSystem(子系统角色):在软件系统中可以有一个或者多个子系统角色,每一个子系统可以不是一个单独的类,而是一个类的集合,它实现子系统的功能;子系统并不知道外观(又称为门面)的存在,对于子系统而言,外观角色仅仅是另一个客户端而已。
三、重构文件加密模块
3.1 重构后的设计结构
为了降低系统耦合度,封装与多个子系统进行交互的代码,M公司开发人员使用外观模式来重构文件加密模块的设计,重构后的结构如下图所示:
3.2 重构后的代码实现
(1)子系统类:FileReader、CipherMachie和FileWriter
/// <summary> /// 文件读取类:子系统A /// </summary> public class FileReader { public string Read(string fileNameSrc) { Console.WriteLine("读取文件,获取明文:"); string result = string.Empty; using (System.IO.FileStream fsRead = new System.IO.FileStream(fileNameSrc, System.IO.FileMode.Open)) { int fsLen = (int)fsRead.Length; byte[] heByte = new byte[fsLen]; int r = fsRead.Read(heByte, 0, heByte.Length); result = System.Text.Encoding.UTF8.GetString(heByte); } return result; } } /// <summary> /// 数据加密类:子系统B /// </summary> public class CipherMachine { public string Encrypt(string plainText) { Console.WriteLine("数据加密,将明文转换为密文:"); StringBuilder result = new StringBuilder(); for (int i = 0; i < plainText.Length; i++) { string ch = Convert.ToString(plainText[i] % 7); result.Append(ch); } string encryptedResult = result.ToString(); Console.WriteLine(encryptedResult); return encryptedResult; } } /// <summary> /// 文件保存类:子系统C /// </summary> public class FileWriter { public void Write(string encryptedStr, string fileNameDes) { Console.WriteLine("保存密文,写入文件:"); byte[] myByte = System.Text.Encoding.UTF8.GetBytes(encryptedStr); using (System.IO.FileStream fsWrite = new System.IO.FileStream(fileNameDes, System.IO.FileMode.Append)) { fsWrite.Write(myByte, 0, myByte.Length); }; Console.WriteLine("写入文件成功:100%"); } }
(2)外观类:EncrytFacade
public class EncryptFacade { private FileReader reader; private CipherMachine cipher; private FileWriter writer; public EncryptFacade() { reader = new FileReader(); cipher = new CipherMachine(); writer = new FileWriter(); } public void FileEncrypt(string fileNameSrc, string fileNameDes) { string plainStr = reader.Read(fileNameSrc); string encryptedStr = cipher.Encrypt(plainStr); writer.Write(encryptedStr, fileNameDes); } }
(3)客户端调用:
public class Program { public static void Main(string[] args) { EncryptFacade facade = new EncryptFacade(); facade.FileEncrypt("Facade/src.txt", "Facade/des.txt"); Console.ReadKey(); } }
这里,src.txt的内容就是一句:Hello World!
最终运行结果如下图所示:
四、二次重构文件加密模块
4.1 新的加密类
在标准的外观模式实现中,如果需要增加、删除或更换与外观类交互的子系统类,势必会修改外观类或客户端的源代码,这就将违背开闭原则。因此,我们可以引入抽象外观类来对系统进行重构,可以在一定程度上解决该问题。
假设在M公司开发的文件加密模块中需要更换一个加密类,不再使用原有的基于求模运算的加密类CipherMachine,而改为基于移位运算的新加密类NewCipherMachine,其中NewCipherMachine类的代码如下:
/// <summary> /// 新数据加密类:子系统B /// </summary> public class NewCipherMachine { public string Encrypt(string plainText) { Console.WriteLine("数据加密,将明文转换为密文:"); StringBuilder result = new StringBuilder(); int key = 10; // 设置密钥,移位数为10 for (int i = 0; i < plainText.Length; i++) { char c = plainText[i]; // 小写字母位移 if (c >= 'a' && c <= 'z') { c += Convert.ToChar(key % 26); if (c > 'z') { c -= Convert.ToChar(26); } if (c < 'a') { c += Convert.ToChar(26); } } // 大写字母位移 if (c >= 'A' && c <= 'Z') { c += Convert.ToChar(key % 26); if (c > 'Z') { c -= Convert.ToChar(26); } if (c < 'A') { c += Convert.ToChar(26); } } result.Append(c); } string encryptedResult = result.ToString(); Console.WriteLine(encryptedResult); return encryptedResult; } }
4.2 重构后的设计
如何在不修改源代码的基础之上使用新的外观类呢?解决办法是:引入一个新的抽象外观类,客户端只针对抽象编程,而在运行时再确定具体外观类。引入抽象外观类之后的设计结构图如下图所示:
在新的设计中,客户端只针对抽象外观类AbstractEncryptFacade进行编程。
4.3 代码实现
(1)抽象外观类:AbstractEncryptFacade
/// <summary> /// 抽象外观类 /// </summary> public abstract class AbstractEncryptFacade { public abstract void FileEncrypt(string fileNameSrc, string fileNameDes); }
(2)新的外观类:NewEncryptFacade
public class NewEncryptFacade : AbstractEncryptFacade { private FileReader reader; private NewCipherMachine cipher; private FileWriter writer; public NewEncryptFacade() { reader = new FileReader(); cipher = new NewCipherMachine(); writer = new FileWriter(); } public override void FileEncrypt(string fileNameSrc, string fileNameDes) { string plainStr = reader.Read(fileNameSrc); string encryptedStr = cipher.Encrypt(plainStr); writer.Write(encryptedStr, fileNameDes); } }
(3)配置文件将具体外观类进行配置:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <!-- EncryptFacade Setting --> <add key="EncryptFacadeName" value="Manulife.ChengDu.DesignPattern.Facade.NewEncryptFacade, Manulife.ChengDu.DesignPattern.Facade" /> </appSettings> </configuration>
(4)客户端调用
public class Program { public static void Main(string[] args) { AbstractEncryptFacade newFacade = AppConfigHelper.GetFacadeInstance() as AbstractEncryptFacade; if (newFacade != null) { newFacade.FileEncrypt("Facade/src.txt", "Facade/des.txt"); } Console.ReadKey(); } }
其中,AppConfigHelper用于读取配置文件的配置并借助反射动态生成具体外观类实例,其代码如下:
public class AppConfigHelper { public static string GetFacadeName() { string factoryName = null; try { factoryName = System.Configuration.ConfigurationManager.AppSettings["EncryptFacadeName"]; } catch (Exception ex) { Console.WriteLine(ex.Message); } return factoryName; } public static object GetFacadeInstance() { string assemblyName = AppConfigHelper.GetFacadeName(); Type type = Type.GetType(assemblyName); var instance = Activator.CreateInstance(type); return instance; } }
最后,运行结果如下图所示:
此时,如果需要再次修改具体外观类,只需要新增一个外观类,并修改配置文件即可,原有代码无须再次修改,符合开闭原则。
五、外观模式小结
5.1 主要优点
(1)对客户端屏蔽了子系统组件,减少了客户端需要处理的对象数量并且使得子系统使用起来更加容易。
(2)实现了子系统与客户端之间松耦合。
(3)提供了一个访问子系统的统一入口,并不影响客户端直接使用子系统。
5.2 应用场景
(1)想要为访问一系列复杂的子系统提供一个统一的简单入口 => 使用外观模式吧!
(2)客户端与多个子系统之间存在很大的依赖性,引入外观类可以将子系统和客户端解耦
(3)在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系 => 通过外观类建立联系,降低层与层之间的耦合度!
参考资料
刘伟,《设计模式的艺术—软件开发人员内功修炼之道》