Design Pattern (G4) - Strategy Pattern

要解決同一件事情或達到某個目的會有各式各樣的方法,寫程式時往往也要想到之後會不會有更好的處理方法或替代方式。未來和現在是不同的時空,為了加入新的程式碼,通常就是加入條件式來選擇執行方法,但程式碼一多就不好維護了。使用Strategy,就可以讓演算法邏輯獨立出來簡易插拔,讓程式更好維護。


目的

將相關處理機制整合成一個群組,並各自實作封裝,讓彼此之間可以互相替換。

動機


  1. 在遇到各種條件執行對應不同演算法的情況時,往往我們會直接將演算法的實作寫進去,造成程式碼愈來愈肥大。
  2. 當各種演算法實作進去後,但是往後可能某些演算法不會用到,這時候就要去找到這個演算法的程式去刪除。

使用時機

  1. 當相關的類別有相同的動作,但動作內容卻不同
  2. 當複雜的演算法需要封裝時
  3. 當一個類別有多條件性的選擇執行類似操作行為

結構


Strategy將這個演算法Group的共同操作抽到介面中,各演算法在各自實作介面的內部邏輯。Context裡包含了Strategy介面的實體,至於是哪個Strategy就要由Client端來決定。Client可以選擇將指定的Strategy實體傳入Context或指定Strategy讓Context自己產生Strategy實體,對於Context而言只需要執行Strategy實體的介面方法就可以了。

實作



限量以一個通知服務的一個範例來作說明。我們知道一個系統如果有通知的服務,可以想的到的大概有Email通知, SMS簡訊通知, 實體郵件通知, 電話通知...等,在我系統啟動時我要決定要使用哪一種通知機制,我只要再產生通知服務實體的時候指定哪一種通知機制,程式就會自動生成該機制的處理物件,因為各種通知機制的介面方法皆是一樣的,所以對於通知服務來說根本不需要知道我使用的是哪個機制,只要呼叫介面方法就好了。以下為範例程式碼:
using System;

namespace TestSolution
{
    /// <summary>
    /// Client
    /// </summary>
    class Program
    {
        static void Main(string[] args)
        {
            var service = new NotifyService(NotifyMechanism.Email);
            service.Notify("標題", "內文", "作者");

            service = new NotifyService(NotifyMechanism.Mail);
            service.Notify("標題", "內文", "作者");

            service = new NotifyService(NotifyMechanism.Phone);
            service.Notify("標題", "內文", "作者");

            service = new NotifyService(NotifyMechanism.SMS);
            service.Notify("標題", "內文", "作者");

            Console.Read();
        }
    }

    /// <summary>
    /// Context
    /// </summary>
    public class NotifyService
    {
        private INotifyProvider _provider;

        public NotifyService(NotifyMechanism mechanism)
        {
            switch(mechanism)
            {
                case NotifyMechanism.Mail:
                    _provider = new MailNotifyProvider();
                    break;
                case NotifyMechanism.Email:
                    _provider = new EmailNotifyProvider();
                    break;
                case NotifyMechanism.SMS:
                    _provider = new SMSNotifyProvider();
                    break;
                case NotifyMechanism.Phone:
                    _provider = new PhoneNotifyProvider();
                    break;
                default:
                    throw new ArgumentException();
            }
        }

        public void Notify(string title, string content, string author)
        {
            var context = new Context
            {
                Author = author,
                Title = title,
                Content = content,
                Timestamp = DateTime.Now
            };
            _provider.Send(context);
        }
    }

    public class Context
    {
        public string Title { get; set; }

        public string Content { get; set; }

        public string Author { get; set; }

        public DateTime Timestamp { get; set; }
    }

    public enum NotifyMechanism
    {
        Mail,
        Email,
        SMS,
        Phone
    }

    /// <summary>
    /// Strategy
    /// </summary>
    public interface INotifyProvider
    {
        void Send(Context context);
    }

    /// <summary>
    /// ConcreteStrategy
    /// </summary>
    public class MailNotifyProvider : INotifyProvider
    {
        public void Send(Context context)
        {
            // 實作內部邏輯

            Console.WriteLine("Send by mail.");
        }
    }

    /// <summary>
    /// ConcreteStrategy
    /// </summary>
    public class EmailNotifyProvider : INotifyProvider
    {
        public void Send(Context context)
        {
            // 實作內部邏輯

            Console.WriteLine("Send by Email.");
        }
    }

    /// <summary>
    /// ConcreteStrategy
    /// </summary>
    public class SMSNotifyProvider : INotifyProvider
    {
        public void Send(Context context)
        {
            // 實作內部邏輯

            Console.WriteLine("Send by SMS.");
        }
    }

    /// <summary>
    /// ConcreteStrategy
    /// </summary>
    public class PhoneNotifyProvider : INotifyProvider
    {
        public void Send(Context context)
        {
            // 實作內部邏輯

            Console.WriteLine("Send by Phone.");
        }
    }
}


優點


  1. 將條件式內執行的程式碼抽離,增加維護性。
  2. 演算法內容可在不同時間點實作,個演算法也可互相替換。


缺點



  1. Client端必須知道各Strategy之間的差異。
  2. 當有些複雜的Strategy需要與Context更多溝通(EX: 參數數量),這時候介面勢必為它增加調整,造成其他簡單的Strategy也要實作。

相關樣式

  1. Flyweight

使用頻率











參考來源:
Design Patterns - Gang of Four
dofactory - Strategy







留言