範例:收銀機 (策略模式)

Pattern: 策略模式

Class Diagram: 收銀機


需求一:客戶想要一台收銀機

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

namespace App\StrategyPattern\CashRegister;

class Program
{
/**
* @var int
*/
private $originalPrice;

public function __construct($originalPrice)
{
$this->originalPrice = $originalPrice;
}

public function pay()
{
return $this->originalPrice;
}
}


需求二:客戶想要有一個優惠活動 (打8折)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

namespace App\StrategyPattern\CashRegister;

class Program
{
/**
* @var int
*/
private $originalPrice;

/**
* @var string
*/
private $promotion;

public function __construct($originalPrice, $promotion)
{
$this->originalPrice = $originalPrice;
$this->promotion = $promotion;
}

public function pay()
{
if ($this->promotion == '20% off') {
return $this->originalPrice * 0.8;
}

return $this->originalPrice;
}
}

需求三:客戶想要有另一個優惠 (買300回饋100)

  • 身為工程師的我們,二話不說加上了這個功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function pay()
{
$originalPrice = $this->originalPrice;

if ($this->promotion == '20% off') {
return $originalPrice * 0.8;
}

if ($this->promotion == 'spend_300_feedback_100') {
if ($originalPrice >= 300) {
return $originalPrice - floor($originalPrice / 300) * 100;
}
}

return $originalPrice;
}

這時候功能是完成了,但有沒有覺得哪裡怪怪的?
欸嘿,我們想到之前學過的簡單工廠模式

可以實作三個類別,分別是正常付費、8折付費、買300回饋100。
讓我們利用簡單工廠改造它。


  • 首先定義付錢介面
1
2
3
4
5
6
7
8
<?php

namespace App\StrategyPattern\CashRegister\Contracts;

interface Payable
{
public function pay();
}
  • 實作正常付費類別
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php

namespace App\StrategyPattern\CashRegister;

use App\StrategyPattern\CashRegister\Contracts\Payable;

class NormalPay implements Payable
{
/**
* @var int
*/
private $originalPrice;

public function __construct($originalPrice)
{
$this->originalPrice = $originalPrice;
}

public function pay()
{
return $this->originalPrice;
}
}
  • 實作打折付費類別
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php

namespace App\StrategyPattern\CashRegister;

use App\StrategyPattern\CashRegister\Contracts\Payable;

class OffPercentPay implements Payable
{
/**
* @var int
*/
private $originalPrice;

/**
* @var double
*/
private $offPercent;

public function __construct($originalPrice, $offPercent)
{
$this->originalPrice = $originalPrice;
$this->offPercent = $offPercent;
}

public function pay()
{
return $this->originalPrice * (1 - $this->offPercent);
}
}
  • 實作買多少回饋多少類別
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php

namespace App\StrategyPattern\CashRegister;

use App\StrategyPattern\CashRegister\Contracts\Payable;

class FeedbackPay implements Payable
{
/**
* @var int
*/
private $originalPrice;

/**
* @var int
*/
private $priceCondition;

/**
* @var int
*/
private $feedback;

public function __construct($originalPrice, $priceCondition, $feedback)
{
$this->originalPrice = $originalPrice;
$this->priceCondition = $priceCondition;
$this->feedback = $feedback;
}

public function pay()
{
$originalPrice = $this->originalPrice;
$priceCondition = $this->priceCondition;
$feedback = $this->feedback;

if ($originalPrice >= $priceCondition) {
return $originalPrice - floor($originalPrice / $priceCondition) * $feedback;
}

return $originalPrice;
}
}

最後原本程式再搭配工廠即可完成。 (下略)

正當我們洋洋得意的時候,客戶送來第四個需求…


需求四:客戶希望收銀機可以開一般發票或電子發票

不是啊,客戶你要這種發票類型的需求你要先說.. (碎念)

按照簡單工廠模式的思維,
我們必須為這個需求做出6個類別,
分別是 (正常付費、打折付費、買多少回饋多少)x(一般發票、電子發票)的排列組合。

不行不行,假設客戶將來又提出要開統編的需求,我們就要寫8個類別了。

而且這樣也違反開放封閉原則
每次有新需求都會改動所有的程式碼。


在我們研究一下後,發現了一個適合的設計模式:策略模式

  • 我們首先製作一個消費明細類別,它擁有所有的優惠方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php

namespace App\StrategyPattern\CashRegister;

use App\StrategyPattern\CashRegister\OffPercentPay;
use App\StrategyPattern\CashRegister\FeedbackPay;
use App\StrategyPattern\CashRegister\NormalPay;

use App\StrategyPattern\CashRegister\Contracts\Payable;

class CashContext
{
/**
* @var Payable
*/
private $discountMethod;

/**
* @param int $originalPrice
* @param string $discountType
*/
public function __construct($originalPrice, $discountType)
{
$this->resolveDiscountMethod($originalPrice, $discountType);
}

/**
* @param int $originalPrice
* @param string $discountType
*/
private function resolveDiscountMethod($originalPrice, $discountType)
{
switch ($discountType) {
case '20% off':
$this->discountMethod = new OffPercentPay($originalPrice, 0.2);
break;

case 'spend_300_feedback_100':
$this->discountMethod = new FeedbackPay($originalPrice, 300, 100);
break;

default:
$this->discountMethod = new NormalPay($originalPrice);
break;
}
}

public function pay()
{
return $this->discountMethod->pay();
}

}
  • 再來修改Program,讓它呼叫消費明細物件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php

namespace App\StrategyPattern\CashRegister;

use App\StrategyPattern\CashRegister\CashContext;

class Program
{
/**
* @var CashContext
*/
private $cashContext;

/**
* @param int $originalPrice
* @param string $discountType
*/
public function __construct($originalPrice, $discountType)
{
$this->cashContext = new CashContext($originalPrice, $discountType);
}

public function pay()
{
return $this->cashContext->pay();
}
}

這樣好像還看不出來有什麼好處,我們繼續實作。


  • 定義發票介面
1
2
3
4
5
6
7
8
<?php

namespace App\StrategyPattern\CashRegister\Contracts;

interface Receiptable
{
public function getReceipt();
}
  • 實作一般發票
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

namespace App\StrategyPattern\CashRegister;

use App\StrategyPattern\CashRegister\Contracts\Receiptable;

class NormalReceipt implements Receiptable
{
public function getReceipt()
{
return '一般發票';
}
}
  • 實作電子發票
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

namespace App\StrategyPattern\CashRegister;

use App\StrategyPattern\CashRegister\Contracts\Receiptable;

class ElectronicReceipt implements Receiptable
{
public function getReceipt()
{
return '電子發票';
}
}
  • 修改消費明細類別
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<?php

namespace App\StrategyPattern\CashRegister;

use App\StrategyPattern\CashRegister\NormalPay;
use App\StrategyPattern\CashRegister\NormalReceipt;
use App\StrategyPattern\CashRegister\Contracts\Payable;
use App\StrategyPattern\CashRegister\ElectronicReceipt;
use App\StrategyPattern\CashRegister\Contracts\Receiptable;

class CashContext
{
/**
* @var Payable
*/
private $discountMethod;

/**
* @var Receiptable
*/
private $receipt;

/**
* @param int $originalPrice
* @param string $discountType
* @param string $receiptType
*/
public function __construct($originalPrice, $discountType, $receiptType)
{
$this->resolveDiscountMethod($originalPrice, $discountType);
$this->resolveReceiptType($receiptType);
}

/**
* @param int $originalPrice
* @param string $discountType
*/
private function resolveDiscountMethod($originalPrice, $discountType)
{
switch ($discountType) {
case '20% off':
$this->discountMethod = new OffPercentPay($originalPrice, 0.2);
break;

case 'spend_300_feedback_100':
$this->discountMethod = new FeedbackPay($originalPrice, 300, 100);
break;

default:
$this->discountMethod = new NormalPay($originalPrice);
break;
}
}

/**
* @param string $receiptType
*/
private function resolveReceiptType($receiptType)
{
switch ($receiptType) {
case 'electronicReceipt':
$this->receipt = new ElectronicReceipt();
break;

default:
$this->receipt = new NormalReceipt();
break;
}
}

public function pay()
{
return $this->discountMethod->pay();
}

public function getReceipt()
{
return $this->receipt->getReceipt();
}
}
  • 最後修改原本的Program
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php

namespace App\StrategyPattern\CashRegister;

use App\StrategyPattern\CashRegister\CashContext;

class Program
{
/**
* @var CashContext
*/
private $cashContext;

/**
* @param int $originalPrice
* @param string $discountType
* @param string $receiptType
*/
public function __construct($originalPrice, $discountType, $receiptType)
{
$this->cashContext = new CashContext($originalPrice, $discountType, $receiptType);
}

public function pay()
{
return $this->cashContext->pay();
}

public function getReceipt()
{
return $this->cashContext->getReceipt();
}
}


[單一職責原則]
類別本身職責算法族的職責分離,就是策略模式的精神!

[開放封閉原則]
這下子,我們終於不會在客戶提出一個新需求時,影響到全部的既有程式碼了。

[介面隔離原則]
定義出付錢介面發票介面,讓兩者不會互相影響。
可以交由各自的算法族,分別實現。

[依賴反轉原則]
消費明細類別依賴抽象的付錢介面與發票介面。
不同的算法族,實現對應的抽象介面。

ʕ •ᴥ•ʔ:使用策略模式,我們依然會做出許多小類別(算法族/算法),
但因為切分的更細,也就更能因應需求去做變化。