設計模式的征途—11.外觀(Facade)模式

Edison Chou發表於2017-07-06

在軟體開發中,有時候為了完成一項較為複雜的功能,一個類需要和多個其他業務類互動,而這些需要互動的業務類經常會作為一個完整的整體出現,由於涉及的類比較多,導致使用時程式碼較為複雜,此時,特別需要一個類似服務員一樣的角色,由他來負責和多個業務類進行互動,而使用這些業務類的類只需要和該類進行互動即可。外觀模式通過引入一個新的外觀類來實現該功能,外觀類充當了軟體系統中的“服務員”,它為多個業務類的呼叫提供了一個統一的入口,簡化了類與類之間的互動。

外觀模式(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)在層次化結構中,可以使用外觀模式定義系統中每一層的入口,層與層之間不直接產生聯絡 => 通過外觀類建立聯絡,降低層與層之間的耦合度!

參考資料

  DesignPattern

  劉偉,《設計模式的藝術—軟體開發人員內功修煉之道》

 

相關文章