如何對工廠模式開放封閉?
將建構物件的邏輯封裝在工廠模式,已經達到 90% 的開放封閉,最少其他 class 都已經開放封閉,將來所有的邏輯修改只剩下工廠模式,若能將工廠模式也開放封閉,那就太好了。
Version
PHP 7.0.0
Laravel 5.2.31
前言
在如何使用 PhpStorm 實現 TDD、重構與偵錯?與深入探討依賴注入中,為了達到工廠模式的開放封閉,我用了一個技巧 : 故意將參數名稱與class名稱取相同,達到工廠模式的開放封閉,但實務上,可能參數來自於下拉式選單的 index,如 0, 1, 2 ….,或者參數與實際 class 名稱並不相同,需要一個 if else
或 switch
轉換,這樣就必須在工廠模式的 create()
或 bind()
寫邏輯,因此無法達成開放封閉原則的要求。1 1本範例為深入探討依賴注入的延伸,若覺得本文的範例看不懂,請先閱讀深入探討依賴注入
實際案例
假設目前有 3 家貨運公司,每家公司的計費方式不同,使用者可以動態選擇不同的貨運公司,將一步步的重構將工廠模式開放封閉。2 2本範例靈感來自於91哥的30天快速上手TDD Day 17:Refactoring - Stagegy Pattern
單元測試
ShippingServiceTest.php3 3GitHub Commit : 新增黑貓單元測試1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20use App\Services\ShippingService;
class ShippingServiceTest extends TestCase
{
/** @test */
public function 黑貓單元測試()
{
/** arrange */
$companyNo = 0;
$weight = 1;
$expected = 110;
/** act */
$target = App::make(ShippingService::class);
$actual = $target->calculateFee($companyNo, $weight);
/** assert */
$this->assertEquals($expected, $actual);
}
}
先建立 ShippingService
的單元測試。
ShippingService
ShippingService.php4 4GitHub Commit : 新增 ShippingService1
2
3
4
5
6
7
8
9
10
11
12
13
14
15namespace App\Services;
class ShippingService
{
/**
* @param int $companyNo
* @param int $weight
* @return int
*/
public function calculateFee(int $companyNo, int $weight) : int
{
$logistics = LogisticsFactory::create($companyNo);
return $logistics->calculateFee($weight);
}
}
因為已經制定了 LogisticsInterface
,3 家貨運公司的計費方式,已經分別被封裝在 BlackCat
、Hsinchu
與 PostOffice
3 個 class 內,所以必須使用工廠模式根據 $companyNo
,回傳適當的貨運公司物件。
工廠模式
LogisticsFactory.php5 5GitHub Commit : 新增 LogisticsFactory1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17namespace App\Services;
class LogisticsFactory
{
public static function create(int $companyNo = 0) : LogisticsInterface
{
if ($companyNo == 0) {
return new BlackCat();
} elseif ($companyNo == 1) {
return new Hsinchu();
} elseif ($companyNo == 2) {
return new PostOffice();
} else {
return new BlackCat();
}
}
}
使用 if else
判斷 $companyNo
,根據不同的 $companyNo
,回傳不同的貨運公司物件。
目前為止完全符合需求,會得到第 1 個 綠燈。
將 if else 重構成 switch
LogisticsFactory.php6 6GitHub Commit : 將 if else 重構成 switch1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18namespace App\Services;
class LogisticsFactory
{
public static function create(int $companyNo = 0) : LogisticsInterface
{
switch ($companyNo) {
case 0:
return new BlackCat();
case 1:
return new Hsinchu();
case 2:
return new PostOffice();
default:
return new BlackCat();
}
}
}
將 if else
重構成 switch
,可稍微改善程式碼的可讀性。7 7將 if else
重構成 switch
,請參考如何在PhpStorm將if else重構成switch case?
重構後趕快執行測試,看看有沒有重構壞掉。
目前為止完全符合需求,會得到第 2 個 綠燈。
將 swtich 重構成 LUT
LogisticsFactory.php8 8GitHub Commit : 將 switch 重構成 LUT1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18namespace App\Services;
use Illuminate\Support\Collection;
class LogisticsFactory
{
public static function create(int $companyNo = 0) : LogisticsInterface
{
$lut = [
0 => BlackCat::class,
1 => Hsinchu::class,
2 => PostOffice::class
];
$className = Collection::make($lut)->get($companyNo, BlackCat::class);
return new $className;
}
}
將 switch
的條件式,改用 LUT (Look Up Table) 的方式表示,其中 case
重構成陣列的 key,要 new
的 class 重構成陣列的 value。
使用 Collection::make()
將陣列轉成 Laravel 的 collection。9 9使用 collect()
helper 亦可。
使用 collection 的 get()
,針對 $lut
做搜尋。
- 第 1 個參數傳入的是 key 的比對值,相當於
switch
的case
。 - 第 2 個參數傳入的示若 key 搜尋不到,所傳回的預設值,相當於
switch
的default
。
重構後趕快執行測試,看看有沒有重構壞掉。
目前為止完全符合需求,會得到第 3 個 綠燈。
將 LUT 重構到 app.php
app.php10 10GitHub Commit : 將 LUT 重構到 config/app.php1
2
3
4
5'logistics' => [
0 => App\Services\BlackCat::class,
1 => App\Services\Hsinchu::class,
2 => App\Services\PostOffice::class
],
將陣列搬到 config/app.php
下,將來若對應邏輯有所修改,只需改 app.php
即可。
LogisticsFactory.php11 11GitHub Commit : 將 LUT 重構到 config/app.php1
2
3
4
5
6
7
8
9
10
11
12
13
14namespace App\Services;
use Illuminate\Support\Collection;
class LogisticsFactory
{
public static function create(int $companyNo = 0) : LogisticsInterface
{
$lut = config('app.logistics');
$className = Collection::make($lut)->get($companyNo, BlackCat::class);
return new $className;
}
}
$lut
改由 config()
讀取 config/app.php
的設定,目前工廠不包含建立物件的邏輯,LUT 已經搬到 app.php
,因此完成工廠模式的開放封閉。
重構後趕快執行測試,看看有沒有重構壞掉。
目前為止完全符合需求,會得到第 4 個 綠燈。
重構成依賴注入
為了達到可測試性的要求,你可能會想將貨運公司物件改用依賴注入的方式,因此我們繼續重構。
ShippingService.php12 12GitHub Commit : ShippingService 改用依賴注入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
27namespace App\Services;
class ShippingService
{
/**
* @var LogisticsInterface
*/
private $logistics;
/**
* ShippingService constructor.
* @param LogisticsInterface $logistics
*/
public function __construct(LogisticsInterface $logistics)
{
$this->logistics = $logistics;
}
/**
* @param int $weight
* @return int
*/
public function calculateFee(int $weight) : int
{
return $this->logistics->calculateFee($weight);
}
}
改用 constructor injection 的方式,將貨運物件注入。
重構單元測試
由於 ShippingService
重構成依賴注入方式,因此單元測試也要跟著重構。
ShippingServiceTest.php13 13GitHub Commit : 重構黑貓單元測試1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22use App\Services\LogisticsFactory;
use App\Services\ShippingService;
class ShippingServiceTest extends TestCase
{
/** @test */
public function 黑貓單元測試()
{
/** arrange */
$companyNo = 0;
$weight = 1;
$expected = 110;
/** act */
LogisticsFactory::bind($companyNo);
$target = App::make(ShippingService::class);
$actual = $target->calculateFee($weight);
/** assert */
$this->assertEquals($expected, $actual);
}
}
因為改成依賴注入,工廠模式的功能不再是建立物件,而是在決定 App::bind()
該與什麼 class 做連結,因此也將工廠模式的 create()
改成 bind()
。
重構工廠模式
LogisticsFactory.php14 14GitHub Commit : 重構 LogisticsFactory1
2
3
4
5
6
7
8
9
10
11
12
13
14
15namespace App\Services;
use App;
use Illuminate\Support\Collection;
class LogisticsFactory
{
public static function bind(int $companyNo = 0)
{
$lut = config('app.logistics');
$className = Collection::make($lut)->get($companyNo, BlackCat::class);
App::bind(LogisticsInterface::class, $className);
}
}
$lut
也是改由 config()
讀取 config/app.php
的設定,目前工廠不包含 App::bind()
的邏輯,LUT 已經搬到 app.php
,因此完成工廠模式的開放封閉。
重構後趕快執行測試,看看有沒有重構壞掉。
目前為止完全符合需求,會得到第 5 個 綠燈。
Conclusion
- 將
if else
或switch
邏輯改用 LUT 表示,可將陣列改放到config/app.php
下,將來若有任何邏輯修改,都是修改在設定檔,而達成工廠模式的開放封閉。 - 透過 collection 的
get()
,可以很方便的搭配 LUT,還可傳入預設參數,配合switch
的default
。
Sample Code
完整的範例可以在我的 GitHub 上找到。