
在軟體開發中,有時候為了完成一項較為複雜的功能,一個類需要和多個其他業務類互動,而這些需要互動的業務類經常會作為一個完整的整體出現,由于涉及的類比較多,導緻使用時代碼較為複雜,此時,特别需要一個類似服務員一樣的角色,由他來負責和多個業務類進行互動,而使用這些業務類的類隻需要和該類進行互動即可。外觀模式通過引入一個新的外觀類來實作該功能,外觀類充當了軟體系統中的“服務員”,它為多個業務類的調用提供了一個統一的入口,簡化了類與類之間的互動。
外觀模式(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;
}
}
View Code
最後,運作結果如下圖所示:
此時,如果需要再次修改具體外觀類,隻需要新增一個外觀類,并修改配置檔案即可,原有代碼無須再次修改,符合開閉原則。
五、外觀模式小結
5.1 主要優點
(1)對用戶端屏蔽了子系統元件,減少了用戶端需要處理的對象數量并且使得子系統使用起來更加容易。
(2)實作了子系統與用戶端之間松耦合。
(3)提供了一個通路子系統的統一入口,并不影響用戶端直接使用子系統。
5.2 應用場景
(1)想要為通路一系列複雜的子系統提供一個統一的簡單入口 => 使用外觀模式吧!
(2)用戶端與多個子系統之間存在很大的依賴性,引入外觀類可以将子系統和用戶端解耦
(3)在階層化結構中,可以使用外觀模式定義系統中每一層的入口,層與層之間不直接産生聯系 => 通過外觀類建立聯系,降低層與層之間的耦合度!
參考資料
劉偉,《設計模式的藝術—軟體開發人員内功修煉之道》
作者:周旭龍
出處:http://edisonchou.cnblogs.com
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連結。