Introduction to Design Patterns - Strategy Pattern

Design Pattern တွေထဲကမှ Strategy Pattern အကြောင်းနဲ့ ဘယ်လို အသုံးပြုရသလဲဆိုတာ လေ့လာကြည့်ရအောင်ပါ။

ℹ️
ဒီ article series မှာတော့ Head First Design Patterns စာအုပ်ထဲမှာ ဖော်ပြထားတဲ့ design patterns တွေကို study notes တွေအနေနဲ့ ရေးသားသွားမှာပဲ ဖြစ်ပါတယ်။

Design Pattern တွေဆိုတာ ကျွန်တော်တို့ software တွေရေးသားတဲ့အခါ ကြုံတွေ့လေ့ရှိတဲ့ အခက်အခဲတွေကို ဘယ်လိုဖြေရှင်းမလဲဆိုတာ ကျွန်တော်တို့ရှေ့ကသူတွေ စမ်းသပ်ပြီး တွေ့ရှိလာတဲ့ ဖြေရှင်းပုံ ဖြေရှင်းနည်းတွေပဲ ဖြစ်ပါတယ်။ Design Pattern တွေကို နားလည်ခြင်းအားဖြင့် ကျွန်တော်တို့အနေနဲ့ ဘယ်လိုဖြေရှင်းမလဲဆိုတာမျိုးကို လိုက်ပြီး စဥ်းစားနေစရာ၊ စမ်းသပ်နေစရာမလိုဘဲ ရှိပြီးသား နည်းလမ်းတွေကို အလွယ်တကူ အကျိုးရှိရှိ အသုံးပြုနိုင်မှာပဲ ဖြစ်ပါတယ်။ ဒီ Design Pattern တွေကို Object Oriented language တွေမှာ အသုံးပြုနိုင်မှာပဲ ဖြစ်ပါတယ်။

အရင်ဆုံး ကျွန်တော်တို့ app တခုကို တွေးကြည့်ရအောင်ပါ။ ဒီ app မှာ master class အနေနဲ့ Duck ဆိုတဲ့ class တခုရှိပြီးတော့ သူ့မှာ quack နဲ့ swin လို့ခေါ်တဲ့ method implementation တွေ ပါဝင်ပါတယ်။ display method ကတော့ Duck class မှာ implementation မပါဝင်ဘဲ abstract method အနေနဲ့ပဲ​ပါဝင်ပါတယ်။ သူ့ကို inherit လုပ်မယ့် class တခုချင်းဆီက မတူညီတဲ့ display တွေ ရှိမှာဖြစ်တဲ့အတွက် implementation ကို inherited methods တွေက လုပ်သွားရမှာပဲ ဖြစ်ပါတယ်။

အခု ကျွန်တော်တို့ အပေါ်မှာပြထားတဲ့ duck တွေမှာ fly ဆိုတဲ့ method ကို ထပ်ပြီး ထည့်ဖို့ လိုလာပြီ ဆိုပါစို့။ ကျွန်တော်တို့အနေနဲ့ ဘယ်နေရာမှာ သွားပြီးထည့်မလဲ?

ပထမဆုံး Parent Class ဖြစ်တဲ့ Duck မှာထည့်လိုက်ပြီဆိုပါစို့။ ဒီလိုထည့်ခြင်းအားဖြင့် inherit လုပ်ထားတဲ့ class တွေအားလုံးဟာလဲ fly ဆိုတဲ့ method implementation ပါပြီးသား ဖြစ်သွားမှာ ဖြစ်ပါတယ်။

ဒါပေမယ့် ကျွန်တော်တို့မှာ ပြဿနာဖြစ်လာနိုင်တာက inherit လုပ်မယ့် class တွေထဲကမှ တချို့ class တွေက အသစ်ထည့်လိုက်တဲ့ method ကို သုံးလို့မရဘူးဆိုပါစို့။​ ဥပမာ ကျွန်တော်တို့မှာ RubberDuck ဆိုတဲ့ class ရှိတဲ့အခါ သူ့မှာ fly ဆိုတဲ့ method ကို အသုံးပြုလို့မရပါဘူး။​ အရုပ်ဘဲတွေက ပျံလို့မှ မရတာ။ ဒါပေမယ့် အပေါ်မှာ ထည့်ထားသလိုမျိုး fly method ကို Parent Duck class မှာထည့်လိုက်တဲ့အခါ RubberDuck ကလဲ fly လို့ရနေသလို ဖြစ်သွားပါတယ်။

ဒီမှာတခုလုပ်လို့ရတာက inherited class ဖြစ်တဲ့ RubberDuck မှာ method implementation ကို override လုပ်လိုက်တာမျိုးပါ။ ပျံလို့မရဘူးဆိုရင် implemenation မှာ ဘာမှမထည့်တာမျိုးနဲ့ parent class Duck မှာရှိတဲ့ implementation တွေကို override လုပ်လို့ရပါတယ်။

ဒါပေမယ့် ဒီနေရာမှာ အခက်အခဲ ဖြစ်လာနိုင်တာက method တခုထည့်ချင်တိုင်း ပြင်ချင်တိုင်း ကျွန်တော်တို့တွေ ရှိသမျှ inherited class တွေအားလုံးကို လိုက်ကြည့်နေရမှာဖြစ်ပြီး အသုံးမလိုတဲ့ class တွေထဲမှာ အပေါ်မှာ ပြထားသလိုမျိုး override တွေ လိုက်လုပ်နေရမှာပဲ ဖြစ်ပါတယ်။ ဒီတော့ ကျွန်တော်တို့ လုပ်လို့ရတာက inherit လုပ်လို့ရတာတွေနဲ့ မရတာတွေကို ခွဲပြီး ထားတာမျိုး လုပ်လို့ရပါတယ်။

💡
Application မှာ ပြောင်းလဲနိုင်တဲ့ အပိုင်းတွေနဲ့ မပြောင်းလဲနိုင်တဲ့ အပိုင်းတွေကို သပ်သပ်ခွဲထားပါ။

ပြောင်းလဲနိုင်တဲ့ အပိုင်းတွေကြည့်မယ်ဆို fly ဆိုတာတွေ့နိုင်ပါတယ်။ တကယ်လို့ quack ဆိုတဲ့ behavior ထပ်ထည့်မယ်ဆိုလဲ ဒါက fly လိုပဲ inherit လုပ်တဲ့ class မတူတာနဲ့ behavior လဲ ကွဲပြားသွားမှာ ဖြစ်ပါတယ်။

ဒီလို behavior တွေကွဲပြားနိုင်တဲ့အခါ parent class ထဲမှာ တန်းပြီးတော့ implementation ကို မထည့်ဘဲ class ကို instantiate လုပ်တဲ့အခါမှ သတ်မှတ်ပေးတာမျိုး လုပ်လို့ရပါတယ်။ အခုလို လုပ်လိုက်ခြင်းအားဖြင့် behavior တွေကို runtime မှာပါ သတ်မှတ်လို့၊ ပြောင်းလဲလို့ ရသွားမှာပဲ ဖြစ်ပါတယ်။

💡
Class member အတွက် Implementation ထက် Interface ကို အသုံးပြုပါ။

ဒီတော့ ကျွန်တော်တို့တွေ FlyBehavior နဲ့ QuackBehavior ဆိုတဲ့ interfaces တွေကို create လုပ်ပါမယ်။ မတူညီတဲ့ behavior implementation တွေကိုလဲ behavior interfaces တွေပေါ် မူတည်ပြီး လုပ်ပါမယ်။

အခုလို ခွဲထုတ်လိုက်ခြင်းအားဖြင့် implemenation တွေက သက်ဆိုင်ရာ class တွေအနေနဲ့ သီးသန့်ရှိနေမှာ ဖြစ်ပါတယ်။ လိုအပ်တဲ့ behavior implemenation ကို runtime မှာ အသုံးပြုလို့ရသွားပါလိမ့်မယ်။ ပြင်ချင်တာပဲ ဖြစ်ဖြစ် အသစ်ထည့်ချင်တာပဲ ဖြစ်ဖြစ် ရှိပြီးသား class တွေကို ထိစရာမလိုဘဲလဲ လုပ်ဆောင်သွားနိုင်မှာ ဖြစ်ပါတယ်။

Original Duck class  ကို အပေါ်မှာ ပြထားသလို ပြင်ဆင်လိုက်တဲ့အခါ ကျွန်တော်တို့တွေ မတူညီတဲ့ behavior တွေကို inherited classes တွေမှာ အလွယ်တကူ အသုံးပြုနိုင်သွားမှာပဲ ဖြစ်ပါတယ်။ တခုသတိပြုရမှာက ကျွန်တော်တို့တွေ FlyBehavior QuackBehavior စသဖြင့် interface တွေကို ထည့်ပေးထားတာပဲ ဖြစ်ပါတယ်။ implementation တွေဖြစ်တဲ့ FlyWithWings, FlyNoWay, Quack စတာတွေကို ကျွန်တော်တို့တွေ Duck class ရဲ့ value type မှာ အသုံးမပြုထားပါဘူး။ အပေါ်မှာ ပြောထားတဲ့ implementation အစား interface ကို အသုံးပြုသင့်တယ်ဆိုတာ ဒါကိုပြောတာပဲ ဖြစ်ပါတယ်။ ဒီတော့မှ behavior  တွေကို အလွယ်တကူ အသုံးပြုနိုင်မှာပဲ ဖြစ်ပါတယ်။

public interface FlyBehavior {
  public void fly();
}
public interface QuackBehavior {
  public void quack();
}
public abstract class Duck {
  FlyBehavior flyBehavior;
  QuackBehavior quackBehavior;
  ...
  public void performFly() {
    flyBehavior.fly();
  }
  
  public void performQuack() {
    quackBehavior.quack();
  }
  ...
}
Mallard - Wildlife Garden Web Shop
Mallard Duck
public class MallardDuck extends Duck {
  FlyBehavior flyBehavior;
  QuackBehavior quackBehavior;
  
  public MallardDuck() {
    flyBehavior = new FlyWithWings();
    quackBehavior = new Quack();
  }
  ...
}
public class Quack implements QuackBahavior {
  public void quack() {
    System.out.println("Quack");
  }
}
public class FlyWithWings implements FlyBehavior {
  public void fly() {
    System.out.println("Flying with wings");
  }
}

အပေါ်မှာပြထားတဲ့ Mallard Duck ဆိုတာ သက်ရှိဘဲ ဖြစ်တဲ့အတွက် FlyWithWings နဲ့ Quack behavior classes တွေကို အသုံးပြုသွားတာ ဖြစ်ပါတယ်။ Class constructor ထဲမှာ ထည့်ထားတာ ဖြစ်တဲ့အတွက် MallardDuck class ကို instantiate လုပ်တာနဲ့ behavior classes တွေလဲ instantiate လုပ်ပြီးသား ဖြစ်သွားမှာ ဖြစ်ပါတယ်။

အခုလို constructor ထဲမှာ instantiate လုပ်တာ class dependency ကိုတော့ ဖြစ်သွားစေပါတယ်။ ဒါကို ဘယ်လိုဖြေရှင်းမလဲဆိုတာ ကျွန်တော်တို့ နောက်လာမယ့် အပိုင်းတွေမှာ ကြည့်သွားမှာပဲ ဖြစ်ပါတယ်။ အခုလောလောဆယ်တော့ ကျွန်တော်တို့ Duck class နဲ့ သူကနေ inherit လုပ်ထားတဲ့ class တွေမှာ behavior တွေကို အလွယ်တကူ ပြင်လွယ် ပြောင်းလွယ်နေပြီပဲ ဖြစ်ပါတယ်။

ကျွန်တော်တို့ အခုလုပ်လို့ရသွားတာ တခုက class instantiation အချိန်တင် မကဘဲ runtime မှာပါ behavior တွေကို ပြောင်းလဲ​လို့ရသွားတာပဲ ဖြစ်ပါတယ်။

public class FlyLightSpeed implements FlyBehavior {
  public void fly() {
    System.out.println("Flying with light speed!");
  }
}
public abstract class Duck {
  ...
  public void setFlyBehavior(fb: FlyBehavior) {
    flyBehavior = fb;
  }

  public void setQuackBehavior(qb: QuackBehavior) {
    quackBehavior = fb;
  }
  ...
}
public static void main(String[] args) {
  Duck mallard = new MallardDuck();
  mallard.performFly();

  mallard.setFlyBehavior(new FlyLightSpeed());
  mallard.performFly();
}

အပေါ်မှာ ပြထားတာကတော့ Duck class နဲ့ သူ့ရဲ့ behavior တွေ ဘယ်လိုဆက်နွယ်နေသလဲဆိုတာပဲ ဖြစ်ပါတယ်။ FlyBehavior နဲ့ QuackBehavior တို့ကို တည်ဆောက်ထားတဲ့ပုံက အစမှာပြောခဲ့သလို parent class မှာ implement လုပ်ပြီး တိုက်ရိုက် inherit လုပ်တာမျိုးမဟုတ်ဘဲ သက်ဆိုင်ရာ duck class တွေ တည်ဆောက်တဲ့အခါမှ compose လုပ်သွားတာမျိုး ဖြစ်ပါတယ်။ ဒီလိုမျိုး တည်ဆောက်တာကို composition လို့ခေါ်ပါတယ်။

💡
Inheritance ထက် composition လိုမျိုးကို ပိုအသုံးပြုပါ။

ကျွန်တော်တို့အပေါ်မှာ အသုံးပြုသွားတဲ့ pattern ကို strategy pattern လို့ခေါ်ပါတယ်။ မတူညီတဲ့ behaviors (algorithms) တွေကို သပ်သပ်ခွဲထားပြီး အလွယ်တကူ ပြောင်းလဲအသုံးပြုလို့ရအောင် လုပ်ပေးထားတာပဲ ဖြစ်ပါတယ်။

💡
The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.