設計模式的征途—3.工廠方法(Factory Method)模式

Edison Chou發表於2017-04-13

上一篇的簡單工廠模式雖然簡單,但是存在一個很嚴重的問題:當系統中需要引入新產品時,由於靜態工廠方法通過所傳入引數的不同來建立不同的產品,這必定要修改工廠類的原始碼,將違背開閉原則。如何實現新增新產品而不影響已有程式碼?工廠方法模式為此應運而生。

工廠方法模式(Factory Method) 學習難度:★★☆☆☆ 使用頻率:★★★★★

一、簡單工廠版的日誌記錄器

1.1 軟體需求說明

Requirement:M公司欲開發一個系統執行日誌記錄器(Logger),該記錄器可以通過多種途徑儲存系統的執行日誌,例如通過檔案記錄或資料庫記錄,使用者可以通過修改配置檔案靈活地更換日誌記錄方式。在設計各類日誌記錄器時,M公司的開發人員發現需要對日誌記錄器進行一些初始化工作,初始化引數的攝製過程比較複雜,而且某些引數的設定有嚴格的先後次序,否則可能會發生記錄失敗。如何封裝記錄器的初始化過程並保證多種記錄器切換的靈活性是M公司開發人員面臨的一個難題。

  M公司開發人員學習了簡單工廠模式對日誌記錄器進行了設計,初始結構如下圖所示。

1.2 基於簡單工廠的程式碼實現

  M公司的程式猿按照結構圖,寫下了核心程式碼LoggerFactory的CreateLogger方法:

    // 簡單工廠方法
    public static ILogger CreateLogger(string args)
    {
        if (args.Equals("db", StringComparison.OrdinalIgnoreCase))
        {
            // 連線資料庫,程式碼省略
            // 建立資料庫日誌記錄器物件
            ILogger logger = new DatabaseLogger();
            // 初始化資料庫日誌記錄器,程式碼省略
            return logger;
        }
        else if(args.Equals("file", StringComparison.OrdinalIgnoreCase))
        {
            // 建立日誌檔案,程式碼省略
            // 建立檔案日誌記錄器物件
            ILogger logger = new FileLogger();
            // 初始化檔案日誌記錄器,程式碼省略
            return logger;
        }
        else
        {
            return null;
        }
    }

  上述程式碼省略了具體日誌記錄器類的初始化程式碼,在LoggerFactory中提供了靜態工廠方法CreateLogger(),用於根據所傳入的引數建立各種不同型別的日誌記錄器。通過使用簡單工廠模式,將日誌記錄器物件的建立和使用分離,客戶端只需要使用由工廠類建立的日誌記錄器物件即可,無須關心物件的建立過程。

  But,雖然簡單工廠模式實現了物件的建立和使用分離,仍然存在以下兩個問題:

  (1)工廠類過於龐大!包含了大量的if-else程式碼,維護和測試的難度增大不少。

  (2)系統擴充套件不靈活,如果新增型別的日誌記錄器,必須修改靜態工廠方法的業務邏輯,違反了開閉原則。

  如何解決這兩個問題,M公司程式猿苦思冥想,想要改進簡單工廠模式,於是開始學習工廠方法模式。

二、工廠方法模式介紹

2.1 工廠方法模式概述

  在簡單工廠模式中只提供一個工廠類,該工廠類需要知道每一個產品物件的建立細節,並決定合適例項化哪一個產品類。其最大的缺點就是當有新產品加入時,必須修改工廠類,需要在其中加入必要的業務邏輯,這違背了開閉原則。此外,在簡單工廠模式中,所有的產品都由同一個工廠建立,工廠類職責較重,業務邏輯較為複雜,具體產品與工廠類之間的耦合度較高,嚴重影響了系統的靈活性和擴充套件性。

  在工廠方法模式中,不再提供一個統一的工廠類來建立所有的產品物件,而是針對不同的產品提供不同的工廠,系統提供一個與產品等級結構對應的工廠等級結構

工廠方法(Factory Method)模式:定義一個用於建立物件的介面,讓子類決定將哪一個類例項化。工廠方法模式讓一個類的例項化延遲到其子類。工廠方法模式又簡稱為工廠模式,也可稱為多型工廠模式,它是一種建立型模式。  

2.2 工廠方法模式結構圖

  工廠方法模式提供一個抽象工廠介面來宣告抽象工廠方法,而由其子類來具體實現工廠方法並建立具體的產品物件。

  從圖中可以看出,在工廠方法模式結構圖中包含以下4個角色:

  (1)Product(抽象產品):定義產品的介面,是工廠方法模式所建立的物件的超類,也就是產品物件的公共父類。

  (2)ConcreteProduct(具體產品):它實現了抽象產品介面,某種型別的具體產品由專門的具體工廠建立,具體工廠和具體產品之間一一對應。

  (3)Factory(抽象工廠):抽象工廠類,宣告瞭工廠方法,用於返回一個產品。

  (4)ConcreteFactory(具體工廠):抽象工廠的子類,實現了抽象工廠中定義的工廠方法,並可由客戶端呼叫,返回一個具體產品類的例項。

三、工廠方法版的日誌記錄器

3.1 解決方案

  M公司的程式猿學習了工廠方法之後,決定使用工廠方法模式來重構設計,其基本結構圖如下圖所示:

  其中, Logger介面充當抽象產品角色,而FileLogger和DatabaseLogger則充當具體產品角色。LoggerFactory介面充當抽象工廠角色,而FileLoggerFactory和DatabaseLoggerFactory則充當具體工廠角色。

3.2 重構程式碼

  (1)抽象產品:ILogger介面

    public interface ILogger
    {
        void WriteLog();
    }

  (2)具體產品:FileLogger和DatabaseLogger類

    public class FileLogger : ILogger
    {
        public void WriteLog()
        {
            Console.WriteLine("檔案日誌記錄...");
        }
    }

    public class DatabaseLogger : ILogger
    {
        public void WriteLog()
        {
            Console.WriteLine("資料庫日誌記錄...");
        }
    }

  (3)抽象工廠:ILoggerFactory介面

    public interface ILoggerFactory
    {
        ILogger CreateLogger();
    }

  (4)具體工廠:FileLoggerFactory和DatabaseLoggerFactory類

    public class FileLoggerFactory : ILoggerFactory
    {
        public ILogger CreateLogger()
        {
            // 建立檔案日誌記錄器
            ILogger logger = new FileLogger();
            // 建立檔案,程式碼省略
            return logger;
        }
    }

    public class DatabaseLoggerFactory : ILoggerFactory
    {
        public ILogger CreateLogger()
        {
            // 連線資料庫,程式碼省略
            // 建立資料庫日誌記錄器物件
            ILogger logger = new DatabaseLogger();
            // 初始化資料庫日誌記錄器,程式碼省略
            return logger;
        }
    }

  (5)客戶端呼叫

    public static void Main()
    {
        ILoggerFactory factory = new FileLoggerFactory(); // 可通過引入配置檔案實現
        if (factory == null)
        {
            return;
        }

        ILogger logger = factory.CreateLogger();
        logger.WriteLog();
    }

  執行結果如下圖:

  

四、藉助反射的重構版本

4.1 逃離修改客戶端的折磨

  為了讓系統具有更好的靈活性和可擴充套件性,M公司程式猿決定對日誌記錄器客戶端程式碼進行重構,使得可以在不修改任何客戶端程式碼的基礎之上更換或是增加新的日誌記錄方式。

  在客戶端程式碼中將不再使用new關鍵字來建立工廠物件,而是將具體工廠類的類名存在配置檔案(例如XML檔案)中,通過讀取配置檔案來獲取類名,再借助.NET反射機制來動態地建立物件例項。

4.2 擼起袖子開始重構

  (1)建立配置檔案

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="LoggerFactory" value="Manulife.ChengDu.DesignPattern.FactoryMethod.v2.DatabaseLoggerFactory, Manulife.ChengDu.DesignPattern.FactoryMethod" />
  </appSettings>
</configuration>

  (2)封裝一個簡單的AppConfigHelper類

    public class AppConfigHelper
    {
        public static string GetLoggerFactoryName()
        {
            string factoryName = null;
            try
            {
                factoryName = System.Configuration.ConfigurationManager.AppSettings["LoggerFactory"];
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            return factoryName;
        }

        public static object GetLoggerFactoryInstance()
        {
            string assemblyName = AppConfigHelper.GetLoggerFactoryName();
            Type type = Type.GetType(assemblyName);

            var instance = Activator.CreateInstance(type);
            return instance;
        }
    }

  (2)重構客戶端程式碼

    public static void Main()
    {
        ILoggerFactory factory = (ILoggerFactory)AppConfigHelper.GetLoggerFactoryInstance();
        if (factory == null)
        {
            return;
        }

        ILogger logger = factory.CreateLogger();
        logger.WriteLog();
    }

  執行結果如下圖所示:

  

五、工廠方法的隱藏

  有時候,為了進一步簡化客戶端的使用,還可以對客戶端隱藏工廠方法,此時,在工廠類中將直接呼叫產品類的業務方法,客戶端無須呼叫工廠方法建立產品,直接通過工廠即可使用所建立的物件中的業務方法。

  (1)修改抽象工廠

    public abstract class LoggerFactory
    {
        // 在工廠類中直接呼叫日誌記錄器的業務方法WriteLog()
        public void WriteLog()
        {
            ILogger logger = this.CreateLogger();
            logger.WriteLog();
        }

        public abstract ILogger CreateLogger();
    }

  (2)修改具體工廠

    public class DatabaseLoggerFactory : LoggerFactory
    {
        public override ILogger CreateLogger()
        {
            // 連線資料庫,程式碼省略
            // 建立資料庫日誌記錄器物件
            ILogger logger = new DatabaseLogger();
            // 初始化資料庫日誌記錄器,程式碼省略
            return logger;
        }
    }

  (3)簡化的客戶端呼叫

    public static void Main()
    {
        LoggerFactory factory = (LoggerFactory)AppConfigHelper.GetLoggerFactoryInstance();
        if (factory == null)
        {
            return;
        }

        factory.WriteLog();
    }

六、工廠方法模式總結

5.1 主要優點

  • 工廠方法用於建立客戶所需要的產品,還向客戶隱藏了哪種具體產品類將被例項化這一細節。因此,使用者只需要關心所需產品對應的工廠,無須關心建立細節
  • 在系統中加入新產品時,無需修改抽象工廠和抽象產品提供的介面,也無須修改客戶端,還無須修改其他的具體工廠和具體產品,而只要加入一個具體工廠和具體產品就可以了。因此,系統的可擴充套件性得到了保證,符合開閉原則

5.2 主要缺點

  • 在新增新產品時,需要編寫新的具體產品類,還要提供與之對應的具體工廠類,系統中類的個數將成對增加,一定程度上增加了系統的複雜度
  • 由於考慮到系統的可擴充套件性,需要引入抽象層,且在實現時可能需要用到反射等技術,增加了系統的實現難度。

5.3 適用場景

  • 客戶端不知道其所需要的物件的類。在工廠方法模式中,客戶端不需要知道具體產品類的類名,只需要知道所對應的的工廠即可,具體的產品物件由具體工廠建立,可將具體工廠的類名儲存到配置檔案或資料庫中。
  • 抽象工廠類通過其子類來指定建立哪個物件。在工廠方法模式中,抽象工廠類只需要提供一個建立產品的介面,而由其子類來確定具體要建立的物件,利用物件導向的多型性和里氏替換原則,在程式執行時,子類物件將覆蓋父類物件,從而使得系統易於擴充套件。

參考資料

      DesignPattern

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

 

相關文章