March 17, 2011

可抽換元件設計模式 - Plugin Pattern

Plugin Pattern源於Martin Fowler的經典名著Patterns of Enterprise Application Architecture,以下為該書對Plugin Pattern的定義
Links classes during configuration rather than compilation.
中文的意思是透過反射(reflection)機制於執行期(run time)由設定檔(configuration)取得實際要執行的物件,它在實作上有幾項特色
  • 會有一個實作Factory Method Pattern的類別,用於生成實際要執行之物件(plugin object)
  • 設定檔內會有實際生成物件的組件名稱及型別,如放置在App.config或Web.config的appSettings中
  • 實際生成之物件必定實作一通用介面

什麼情況下我們會需要用到Plugin Pattern呢?當系統需要提供同一機制但不同實作時就會用到。

以下以一範例說明如何實作Plugin Pattern,範例為實作一簡易的log機制,系統可允許一般文字log及XML格式log

1.建立一通用介面
public interface ILogger
{
    void Write(Dictionary<string, string> messages);
}

2.建立TextLogger類別(plugin object),並實作ILogger介面
public class TextLogger : ILogger
    {
        public void Write(Dictionary<string, string> messages)
        {
            StringBuilder sb = new StringBuilder();
            messages.ToList().ForEach(c => sb.AppendLine(string.Format("{0}:{1}", c.Key, c.Value)));
            sb.AppendLine();
            File.AppendAllText(string.Format("{0}.txt", DateTime.Now.ToString("yyyyMMdd")), sb.ToString());
        }
    }

3.建立XmlLogger類別(plugin object) ,並實作ILogger介面
public class XmlLogger : ILogger
    {
        public void Write(Dictionary<string, string> messages)
        {
            XElement element = new XElement(
                new XElement("Record",
                    new XElement("Action", messages["Action"]),
                    new XElement("Detail", messages["Detail"]),
                    new XElement("LogTime", messages["LogTime"])
                )
            );

            File.AppendAllText(string.Format("{0}_{1}.xml", DateTime.Now.ToString("yyyyMMdd"), Guid.NewGuid()), element.ToString());
        }
    }

4.建立一實作Factory Method Pattern的類別
public class LoggerFactory
    {
        private static ILogger _logger;

        public static ILogger CreateLogger()
        {
            if (_logger == null)
            {
                string assemblyName = ConfigurationManager.AppSettings["AssemblyName"];
                string classType = ConfigurationManager.AppSettings["ClassType"];

                Assembly assembly = Assembly.Load(assemblyName);
                _logger = assembly.CreateInstance(classType) as ILogger;
            }

            return _logger;
        }
    }

5.建立設定檔
<xml version="1.0" encoding="utf-8">
<configuration>
  <appsettings>
    <add key="AssemblyName" value="LoggerSample">
    <add key="ClassType" value="LoggerSample.PluginPattern.TextLogger">
  </appsettings>
</configuration>

6.使用方式
ILogger log = LoggerFactory.CreateLogger();
Dictionary<string, string> messages = new Dictionary<string, string>();
messages.Add("Action", "建立使用者");
messages.Add("Detail", string.Format("UserId: {0}, UserName: {1}", user.UserId, user.UserName));
messages.Add("LogTime", DateTime.Now.ToString());
log.Write(messages);

7.若要切換log機制,僅須修改設定檔
<xml version="1.0" encoding="utf-8">
<configuration>
  <appsettings>
    <add key="AssemblyName" value="LoggerSample">
    <add key="ClassType" value="LoggerSample.PluginPattern.XmlLogger">
  </appsettings>
</configuration>

以上為Plugin Pattern的實作步驟。事實上,在.NET Framework中已內建Plugin Pattern,稱為Provider Pattern,實作時plugin object需額外繼承ProviderBase類別,並自訂Provider用的設定檔區段(繼承ConfigurationSection),詳細的實作細節可參考Clark兄的文章Provider Pattern

雖然Plugin Pattern在實作上很簡單,但在系統中可能不只像範例中只有log機制需要作抽換的動作,所以在實務上會以IoC(Inversion of Control) Pattern再搭配IoC Framework如StructureMap, Unity, Spring.NET等實作抽換機制。

範例程式下載

備註
原文修改自筆者於公司內部分享Plugin Pattern實作的簡報檔,可按此下載

No comments: