Strategy Pattern 策略模式 [Design Pattern in Java]

Posted by Kubeguts on 2020-07-26

可以在執行期動態更換物件的行為

『策略模式』定義了演算法家族,將他們個別封裝起來,可以讓他們之間互相替換,此模式不會影響到使用此演算法的程式。

沒有思考使用設計模式的時候:模擬鴨子版本

假設我們要開發模擬鴨子遊戲,會有各種不同的鴨子,這時候會們通常都會先設計出一個class叫做 Duck,然後Duck會包含鴨子的共同行為:發出叫聲 quack(), 游泳 swin(), 以及展現外觀 display(),然後有綠頭鴨 MallardDuck class和 紅頭鴨 RedheadDuck class分別繼承 Duck類別

(做UML)

若現在需要讓每隻鴨子都會飛,我們會很直覺的在 Duck class中加入 fly()

但若現在有個橡皮鴨子,會不小心繼承到Duck class的fly(), 但橡皮鴨不會飛!

用繼承的可以改善作法

直接在橡皮鴨 class中的fly() 不定義任何事情

(做UML)

問題

若又遇到誘餌鴨,就會面臨fly()沒定義任何事情,quack()也沒定義任何事情
因為若有很多新型態的鴨子,會導致每一個不同的鴨子都得檢視該鴨子是否可飛或可叫

(做UML)

改用介面做改善

將fly與quack從Duck抽離出來變成 Flyable與Quackable介面,讓有需要飛或叫的鴨子實作之

問題

重複的程式碼會變超多,因為同樣會飛或叫的鴨子都各自實作了Flyable與Quackable介面的程式碼,該程式碼邏輯都是相同的。

設計守則(一)

  • 找出程式中需要更動之處,並將之獨立出來,不要和那些不需要更動的程式碼混在一起

  • 把會變動的部分取出來並將之封裝起來,以便以後可以輕易地擴充此部分,而不影響不需要更動的部分。

將鴨子的行為從Duck類別取出來!

抽離會變動的部分成為獨立類別

將飛行行為 fly,與呱呱叫行為 quack獨立成class

並且在Duck類別設置 “可以設定行為的方法”,可以在“執行期”動態地改變鴨子的飛行或呱呱叫行為

設計守則(二)

寫程式是針對介面而寫,而不是針對實踐

用介面代表每個行為: FlyBehavior, QuackBehavior

用各個鴨子的行為類別去實踐FlyBehavior和QuackBehavior介面
而不是由Duck類別實踐該介面

:::info
寫程式是針對介面去寫:其真正意思是『寫程式是針對超型態(supertype)而寫』

使用超型態的話可以不用理會以後執行時的真正物件型態:為“多型”的實踐

ex:

Animal interface { makeSound() }
Dog implment Animal { makeSound() { bark() }}
Cat implement Animal { makeSound() { meow() }}

Animal animal = new Dog();
animal.makeSound();

有個好處是,可以不用直接在一開始僵化某個變數的宣告型態 ex: Dog x = new dog(); // 僵化了x變數為dog型態

而是可以在執行期,也就是使用的時候指定該物件型態

ex:

1
2
3
4
5
6
7
// 定義getAnimal為回傳Dog();
x = getAnimal(); // 得到 Dog型態
x.makeSound(); // 發出狗叫聲

// 將getAnimal() 中改成傳回Cat();
x = getAnimal(); // 得到 Cat型態
x.makeSound(); // 發出貓叫聲

:::

實踐鴨子的行為

  • FlyBehavior介面,用FlyWithWings(實踐所有有翅膀的鴨子會飛的行為)與FlyNoWay(實踐所有不會飛的鴨子的動作)這兩個類別來實作

  • QuackBehavior介面,用Quack(真的呱呱叫)、Squeak(橡皮吱吱叫)與MuteQuack(叫不出聲音)這三個類別來實作

1
以上設計將飛行與呱呱叫的行為可以被其他物件再三利用,將鴨子的行為抽離出來

問題與思考

  1. 是否該先把系統做出來,在看看哪些地方需要更動,再回頭將需更動的邏輯獨立出來?
    • 答:不儘然,設計系統中可以預先考慮到未來哪些地方可能需要變動
  2. 鴨子是不是也可以設計成一個介面?
    • 答: 不恰當,因為已經將會變動的邏輯(fly與quack)抽離出dock class, 那dock class就可以直接為每隻鴨子都會有同樣邏輯的類別,讓不同類型的鴨子直接繼承使用

整合鴨子的行為

將飛行與呱呱叫的動作,委託其他人處理 (在Dock類別中宣告 FlyBehavior與QuackBehavior,透過Behavior介面取得 有實作該Behavior介面的行為們 ex: FlyBehavior介面會有FlyWithNoWings類別實作)

1
2
3
4
5
6
7
8
9
10
11
12
public class Duck {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;

public void performQuack() {
quackBehavior.quack();
}

public void performFly() {
flyBehavior.fly();
}
}

現在由綠頭鴨(MallarDuck)來使用Duck的quackBehavior所擁有的有實踐自己的子類別 Quack()
用flyBehavior的FlyWithNoWings

且MallarDuck繼承了Duck類別,所以可以使用quackBehavior和flyBehavior取用自己對應的動作

1
2
3
4
5
6
7
8
9
10
public class MallardDuck extends Duck {
// constructor
pubilc MallardDuck() {
quackBehavior = new Quack();
flyBehavior = new FlyWithNoWings();
}

quackBehavior.performQuack();
flyBehavior.performFly();
}

完整測試的code:實作一個MiniDuck

Duck class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class Duck {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public Duck() {}

public abstract void display();

public void performFly() {
flyBehavior.fly();
}

public void performQuack() {
quackBehavior.quack();
}

public void swim() {
System.out.println("All ducks float, even decoys!");
}
}

FlyBehavior and QuackBehavior介面與實踐之的動作

FlyBehavior.inrface

1
2
3
public interface FlyBehavior {
void fly();
}

各種實踐Fly的類別

FlyWithWings.java

1
2
3
4
5
public class FlyWithWings implements FlyBehavior {
public void fly() {
System.out.println("I am flying");
}
}

FlyNoWay.java

1
2
3
4
5
public class FlyNoWay implements FlyBehavior {
public void fly() {
System.out.println("I cannot fly");
}
}

QuackBehavior.java

1
2
3
public interface QuackBehavior {
public void quack();
}

各種實踐Quack的類別

Quack.java

1
2
3
4
5
public class Quack implements QuackBehavior {
public void quack() {
System.out.println("Quack");
}
}

MuteQuack.java

1
2
3
4
5
public class MuteQuack implements QuackBehavior {
public void quack() {
System.out.println("<<Silence>>");
}
}

Squeak.java

1
2
3
4
5
public class Squeak implements QuackBehavior {
public void quack() {
System.out.println("Squeak");
}
}

實踐綠頭鴨的類別:MallardDuck.java

1
2
3
4
5
6
7
8
9
10
11
public class MallardDuck extends Duck {
// constructor
public MallardDuck() {
quackBehavior = new Quack();
flyBehavior = new FlyWithWings();
}

public void display() {
System.out.println("I am a real Mallard duck");
}
}

測試用類別:MiniDuckSimulator.java

1
2
3
4
5
6
7
8
9
10
public class MiniDuckSimulator {
public static void main(String[] args) {
Duck mallard = new MallardDuck();

// 會呼叫 MallardDuck繼承來的performQuack()
// 進而委託 quackBehavior處理quack行為,而非在自己class內處理
mallard.performQuack();
mallard.performFly();
}
}

動態設定行為

在鴨子類別中可以加入設置flyBehavior和quackBehavior的方法

可以隨時呼叫以下方法改變鴨子的行為

1
2
3
4
5
6
7
8
9
10
public abstract class Duck {
...
public void setFlyBehavior(FlyBehavior fb) {
flyBehavior = fb;
}

public void setQuackBehavior(QuackBehavior qb) {
quackBehavior = qb;
}
}

例如有個 “模型鴨“

1
2
3
4
5
6
7
8
9
10
public class ModelDuck extends Duck {
public ModelDuck() {
flyBehavior = new FlyNoWay();
quackBehavior = new Quack();
}

public void display() {
System.out.println("Model duck");
}
}

建立一個新的FlyBehavior型態,具有火箭噴射的功能

1
2
3
4
5
public class FlyRocketPowered implements FlyBehavior {
public void fly() {
System.out.println("I am flying a rocket");
}
}

改變測試類別,加上模型鴨子,使模型鴨具有火箭動力!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MiniDuckSimulator {
public static void main(String[] args) {
Duck mallard = new MallardDuck();
...

/**
* 具有噴射動力的模型鴨子
*/
Duck modelDuck = new ModelDuck();
modelDuck.performFly(); // 不會飛
// 動態地更換飛行的行為
modelDuck.setFlyBehavior(new FlyRocketPowered());
modelDuck.performFly(); // 噴射!
}
}

以上的行為為將兩個類別(FlyBehavior與QuackBehavior)組合起來使用,為"Composition 合成"的精神,與繼承不一樣的是,鴨子Duck的行為不是繼承而來,而是透過適當的行為物件『合成』而來!

設計守則

多用合成,少用繼承

合成可以將演算法封裝成類別,更可以『在執行動態地改變行為』,只要合成的行為物件,符合特定的介面標準即可