Sar Yay Club

Do it first. You can do it right later.

06 Jun 2020

Strategy and Template Design Patterns

ဒီနေ့ Gang of Four design pattern တွေထဲက လက်တွေ့မှာ ခဏခဏကြုံရပြီး ခပ်ဆင်ဆင် ဖြစ်နေတဲ့ ဒီဇိုင်းပုံစံနှစ်ခုအကြောင်း နည်းနည်းရေးချင်တယ်။ Strategy pattern နဲ့ Template pattern အကြောင်းပါ။ နှစ်ခုလုံးက code duplication နဲ့ conditional branch တွေအများကြီးခွဲတဲ့ပြသနာကို ဖြေရှင်းတဲ့ နည်းတွေပါ။

အရင်ဆုံး တစ်ခုချင်းဘာဆိုတာကြည့်လိုက်ရအောင်

Strategy pattern

ဒီ pattern ကိုဘယ်ချိန်တွေမှာ သုံးလဲဆိုတော့ ကိုယ်လုပ်ချင်တဲ့ operation တစ်ခုမှာ input data အမျိုးအစားပေါ်မူတည်ပြီးပဲ ဖြစ်ဖြစ်၊ situation တစ်ခုခုအပေါ်မူတည်ပြီးတော့ပဲ ဖြစ်ဖြစ် လုပ်ပုံလုပ်နည်းကွာသွားတာနေရာတစ်နေရာရှိနေရင် သုံးတယ်။ ဥပမာအနေနဲ့ subscription ecommerce application တစ်ခုမှာ order ကို refund လုပ်ရမယ်ဆိုပါတော့။ refund လုပ်တဲ့ အဆင့်တွေက အများစုက တူတူပဲ။

  • Refund amount ကိုတွက်ချက်ရမယ်
  • order ကို cancel လုပ်မယ်
  • customer အပေါ်မူတည်ပြီး refund အမျိုးအစားကို တွက်ချက်ရမယ်။ အပြည့်ပြန်ပေးမှာလား တစ်ဝက်ပဲပြန်ပေးမှာလားပေါ့။
  • နောက်ပြီးတော့ payment gateway ကနေ ပိုက်ဆံပြန်ပေးရမယ်။
  • ပြီးရင် ပိုက်ဆံပြန်ပေးပြီးပါပြီ ဆိုပြီး အီးမေးလ်ပို့မယ်။

အဲ့ဒီမှာ customer ကို ပိုက်ဆံပြန်ပေးတဲ့အချိန်မှာ policy တွေက အမျိုးအစား လေးမျိုး လောက်ရှိတယ်ဆိုပါစို့။ လေးမျိုးလုံးကလည်း လုပ်နည်းလုပ်ဟန်မတူဘူးဆိုရင် အဲ့ဒီကုတ်က ဘယ်ပေါ်လစီနဲ့ကိုက်လဲ အပေါ်မူတည်ပြီး conditional လေးခုစစ်ရတော့မယ်။

class RefundOrder
  def proceed(order)
    amount = calculate_refund_amount(order)

    transaction do
      process_refund(order, amount)
      cancel_order(order)
    end

    email_customer(order.customer)
  end

  private

  #... other code...

  def process_refund(order, amount)
    if order.credit_refundable?
      # refund by credit
    elsif order.replaceable?
      # refund with a different free product
    elsif amount > MAX_REFUNDABLE_AMOUNT
      # direct refund MAX_REFUNDABLE_AMOUNT and refund the rest as credit
    else
      # default direct refund
    end
  end
end

အဲ့လိုအချိန်မှာဆိုရင် အဲ့ဒီ conditional ကို strategy pattern သုံးပြီး replace လုပ်လို့ရတယ်။ conditional branch တစ်ခုချင်းစီက refund ဘယ်လိုလုပ်လဲဆိုတဲ့ strategy တွေပဲ၊ အဲ့ဒီ strategyတွေကို method ထဲမှာ hardcode မလုပ်ပဲနဲ့ order ကိုပဲ ဘယ်refund strategyသုံးမလဲလို့ လှမ်းမေးပြီး အဲဒီ့ strategy ကို execute လုပ်လိုက်တယ်။

class CreditRefundStrategy
  def execute(amount)
  # refunds with credit
  end
end

class ReplaceRefundStrategy
  def execute(amount)
  # find a free product and refund as replacement
  end
end

class OverMaxRefundStrategy
  def execute
  # direct refund max amount and refund the rest as credit
  end
end

class DeafultRefundStrategy
  def execute(amount)
    # direct refund amount
  end
end

class RefundOrder
  def proceed(order)
    amount = calculate_refund_amount(order)

    transaction do
      process_refund(order, amount)
      cancel_order(order)
    end

    email_customer(order.customer)
  end

  private

  #... other code...

  def process_refund(order, amount)
    order.refund_strategy.execute(amount)
  end
end

အဲ့လိုပြောင်းလိုက်လို့ ရုတ်တစ်ရက်ကြည့်ရင် ဘာမှသိပ်မထူးသွားဘူးလို့ ထင်ရပေမဲ့ တကယ်တော့ ဒီကုတ်က အရင်ကုတ်ထက်ပိုပြီး stable ဖြစ်သွားတယ်။ နောက်ထပ် refund လုပ်မဲ့ policy အသစ် တစ်မျိုးထပ်ထည့်ချင်ရင် ဒီကုတ်ကို modify လုပ်စရာမလိုတော့ဘူး။ Strategy အသစ်ထပ်ထည့်ရုံပဲ။ အရင်ကုတ်အတိုင်းဆိုရင် နောက်ထပ် conditional တစ်ဆင့်ထပ်ထည့်ရမယ်။ Strategy class အသစ်ထပ်ဆောက်ပြီး ထည့်ပေးလိုက်ရုံပဲ။ hardcoded ရေးထားတဲ့ကုတ်ကနေပြီး configuration နဲ့ပြောင်းလို့ရတဲ့ကုတ်ဖြစ်သွားတယ်။

အခုလို hardcode လုပ်ထားတဲ့ ကွဲပြားမှုတွေကို interface တူတဲ့ strategy class အများကြီးအဖြစ်ခွဲထုတ်ပြီးရေးတဲ့ pattern ကို strategy pattern လို့ခေါ်တာပါပဲ။

Template Method pattern

ဒီ pattern ကိုဘယ်ချိန်တွေမှာ သုံးလဲဆိုတော့ ကိုယ်လုပ်ချင်တဲ့ behaviour အမျိုးမျိုးရှိတယ်။ အဲ့ဒီ behaviour တွေက ယေဘုယျအားဖြင့်တူတယ် ဒါပေမဲ့ တစ်ချို့အစိတ်အပိုင်းလေးတွေက လုပ်ပုံလုပ်နည်းကွာသွားတာမျိုးတွေရှိတဲ့ အခါသုံးတယ်။ ဥပမာ business application တစ်ခုမှာ report system တစ်ခုရှိတယ်ဆိုပါတော့။ အဲ့ system မှာ report တွေက အမျိုးမျိုးရှိတယ်။ အဲ့ဒီ Report တွေကို ဒေါင်းလုပ်ချတဲ့ process ကဒီလို

  • Report အတွက်လိုတဲ့ data collection လုပ်ရတယ်
  • Report file ကို generate ထုတ်ရတယ်
  • Generate လုပ်လိုက်တဲ့ report file ကို S3 မှာသွားသိမ်းရတယ်
  • နောက်ဆုံးအနေနဲ့ report file ကို metadata နဲ့ cache လုပ်ရတယ်

ခက်တာက အဲ့ဒီအဆင့်တွေက report အမျိုးအစားပေါ်မူတည်ပြီး မတူတဲ့နေရာတွေမတူကြဘူး။ အကုန်လုံးကတစ်မျိုးနဲ့တစ်မျိုး generate လုပ်တဲ့ကုတ်က မတူကြဘူးဆိုပါတော့။ Pseudo code နဲ့ မြင်သာအောင် ဥပမာ ပြရရင်

  class ReportA
    def download(creator, report_params)
      data =collect_monthly_data(report_params)
      file = generate_excel_file(data)
      s3_bucket_url = save_to_s3(file)
      cache_report(creator, report_params, s3_bucket_url, file)
    end

    private
    # ... implement those methods
  end

  class ReportB
    def download(creator, report_params)
      data = collect_monthly_data(report_params)
      file = generate_pdf_file(data)
      s3_bucket_url = save_to_s3(file)
      cache_report(creator, report_params, s3_bucket_url, file)
    end

    private
    # ... implement those methods
  end

  class ReportC
    def generate(creator, report_params)
      data = collect_annual_data(report_params)
      file = generate_excel_file(data)
      s3_bucket_url = save_to_s3(file)
      cache_report(creator, report_params, s3_bucket_url, file)
    end

    private
    # ... implement those methods
  end

အထက်ကကုတ်မှာ save_to_s3 နဲ့ cache_report လုပ်တဲ့ behaviour တွေက Report အားလုံးအတွက်တူတူပဲ။ ဒါပေမဲ့ report အကုန်လုံးမှာ implement လုပ်ထားတော့ code duplication ဖြစ်နေတယ်။ တကယ်လို့များ s3 မှာ မသိမ်းတော့ပဲ Dropbox မှာပြောင်းသိမ်းမယ်ဆိုရင် သုံးနေရာလုံးမှာပြောင်းရတာ့မယ်။ အဲ့ဒါကို template method pattern သုံးပြီး refactor လုပ်ကြည့်မယ်ဆိုရင်

class Report

  def download(creator, report_params)
    data = collect_data(report_params)
    file = generate_report_file(data)
    s3_bucket_url = save_file(file)
    cache_report(creator, report_params, s3_bucket_url, file)
  end

  private

  def collect_data(report_params)
    #code for collect_monthly_data
  end

  def generate_report_file(data)
    #code for excel file generation
  end

  def save_file(file)
  end

  def cache_report
  end
end

class ReportA < Report
end

class ReportB < Report

  def generate_report_file
    # code for pdf generation
  end

end

class ReportC < Report

  def collect_data
    #code for collect_annual_data
  end

end

ဒီကုတ်မှာ shared လုပ်ထားတဲ့ behaviourတွေက superclass ကိုရောက်သွားပြီတော့ အောက်က subclass တွေကွဲပြားနေတဲ့အပိုင်းတွေကိုပဲ implement လုပ်တော့တယ်။ code duplication ပျောက်သွားပြီးတော့ တိကျတဲ့ abstract structure တစ်ခုထွက်လာတယ်။ အရင်ကုတ်တုံးက Report အသစ်တစ်ခုထပ်တိုးရင် ကုတ်တွေကထပ်ပြီး duplication လုပ်ရမယ်။ အခုကုတ်မှာ subclass အသစ်ထပ်ထည့်ရုံပဲ။ တကယ်လို့ s3 ကနေ Dropbox ကိုပြောင်းသိမ်းချင်တယ်ဆိုရင် superclass က implemenation တစ်နေရာပဲသွားပြောင်းလိုက်ရုံပဲ။ အဲ့လို ခပ်ဆင်ဆင် workflow တွေကို template format ပြောင်းလိုက်တာကို template pattern လို့ခေါ်တာပါပဲ။

ဘယ်နည်းလမ်းကို ဘယ်အချိန်မှာသုံး

ဒီ pattern နှစ်ခုကို သေချာပြန်နှိုင်းယှဉ်ကြည့်မယ်ဆိုရင် တစ်ခုနဲ့တစ်ခုသိပ်မကွာတာကို တွေ့ရတယ်။ ကျွန်တော်လည်း စသိသိချင်းမှာ တူတူပဲလို့တောင်ထင်မိတယ်။ ဥပမာ refund process မှာ strategy pattern အစား template pattern ပြောင်းသုံးလဲ ရသလို report process မှာလဲ collect_data နဲ့ generate_report_file method တွေနေရာမှာ strategy object တွေနဲ့ အစားထိုးလို့ရတယ်။

အဲ့ဒါဆိုရင် ဘယ်အချိန်မှာ ဘယ် pattern ကိုသုံးသင့်သလဲ မေးခွန်းထုတ်စရာရှိတယ်။ ကျွန်တော့် observation အရတော့ တကယ်တော့ ဒီ pattern နှစ်ခုလုံးရဲ့ နောက်ကွယ်က အနှစ်သာရ princinple တွေကတော့ single responsibility princinple ရယ် dependency inversion ရယ်, open closed princinple ရယ်ပဲ။ ကိုယ်လုပ်ချင်တဲ့ process က အခြေအနေတစ်ခုအပေါ်မူတည်ပြီး ခြားနားနေပြီဆိုရင် single responsibility မဟုတ်တော့ဖို့ chance များနေပြီလို့ပြောလို့ရတယ်။ အဲ့တော့ အဲ့ဒီ မတူတဲ့ responsibilityတွေ ကို သတ်သတ် object တွေအဖြစ်ခွဲထုတ်ပစ်လိုက်တာက ပထမအချက်၊ ဒုတိယတစ်ချက်က higher level abstraction တွေက lower level abstraction တွေကို မှီခိုနေရတဲ့ dependency ကို ပြောင်းပြန်လှန်လိုက်တာပဲ။ ဥပမာ RefundOrder class မှာ refund လုပ်တဲ့ process abstraction တစ်ခုလုံးက အဲ့ဒီအထဲက အစိတ်အပိုင်းတစ်ပိုင်းဖြစ်တဲ့ refund policy တွေဘယ်လိုအလုပ်လုပ်သလဲဆိုတာကို မှီခိုနေရတယ်။ အဲ့ဒါကိုပြောင်းပြန်လှန်ပြီး refund strategy ဆိုတဲ့ high level abstraction တစ်ခုပေါ်ကိုပဲမှီခိုအောင်ပြောင်းလိုက်တယ်။ အဲ့ဒီ abstraction ရဲ့ concrete implementation တွေက သတ်သတ် object တွေဖြစ်သွားတယ်။ အဲ့လို dependency invert လုပ်လိုက်တဲ့ အချိန်မှာ ဒီ class ရဲ့ behaviour ကို ထပ်တိုးချင်ရင် ဒီ class ကို ပြင်စရာမလိုပဲ နောက်ထပ် strategy class တစ်ခုထည့်လိုက်ရုံနဲ့ ထပ်တိုးလို့ရတယ်။ အဲ့ဒါက open for extension closed for modification ပဲ။

Template pattern မှာကျတော့ Object collaboration ထက် internal behavior sharing ကိုပို ဦးစားပေးထားတယ်။ ခြားနားနေတဲ့ အပိုင်းတွေဟာ သူတို့ချည်းသက်သက်သိပ်ပြီးတော့ အဆက်စပ်မရှိပေမဲ့ တူညီတဲ့ higher level abstraction အောက်မှာ အဓိပ္ပါယ်ရှိရှိဆက်စပ်နေတယ်။ အဲ့လို ပီပြင်တဲ့ abstraction တစ်ခုရှိနေတယ်ဆိုရင် inheritance က the right tool လို့ပြောလို့ရတယ်။

အဲဒီတော့ အဲ့နှစ်ခုကို ဘယ်လိုရွေးမလဲလို ့ ကျွန်တော့်အတွက် ကိုယ့်ဘာသာကိုယ် သတ်မှတ်လိုက်တဲ့ rule တွေကတော့

  • ကိုယ် implement လုပ်မဲ့ flow မှာ conditional behaviour တွေပါနေရင် ဒီ pattern တွေနဲ့ အဆင်ပြေတဲ့ ဒီဇိုင်းဖြစ်ဖို့များတယ်။
  • conditional က တစ်နေရာထဲဆိုရင် strategy နဲ့ အဆင်ပြေဖို့များတယ်။
  • conditional တွေကများနေရင် template pattern နဲ့ပို အဆင်ပြေနိုင်တယ်။
  • တစ်ခါတစ်လေ conditional တွေမပါပေမဲ့ duplicate behaviour တွေဖြစ်နေတယ်ဆိုရင်လည်း ဒါဟာ template pattern ဖြစ်နိုင်တယ်။ အဲ့ချိန်ကျရင်တော့ Sandi Metz ရဲ့ rule of thumb for inheritance ကို စဥ်းစားရမယ်။ အဲ့ဒါကတော့ အဲ့ဒီ duplicate behavioru object တွေကို is a relationship သို့မဟုတ် kind of relationship တစ်ခုနဲ့ဖေါ်ပြနိုင်ရင် inheritanceသုံးလို့ သင့်တော်တယ်ဆိုတဲ့ rule ပဲ။ ဥပမာ ReportA, ReportB, ReportC တွေကို a kind of report relationship နဲ့ ဖော်ပြလို့ရတယ်။ အဲ့ဒါဆိုရင် template pattern က the right fit ပဲ။

ဒီသုံးသပ်ချက်ရဲ့ take away ကတော့ ဒီဇိုင်း pattern တွေက တကယ်တော့ SOLID princinple တွေကို အသုံးချထားတဲ့ pattern တွေပဲဆိုတာရယ် polymorphism ဟာ Object Oriented Design ရဲ့ foundation ပါလားဆိုတာရယ်ပါပဲ။