PHP Abstract - Part II
利用 Abstract 消除重複條件的代碼(Repeated Condition)
這篇我們將會運用 abstract 來簡化不同functions內存有相同的判斷條邏輯。並且會及搭配factory design pattern
的應用,讓business logic
物件和constructor
物件分離,達到分工清楚。也讓獨立或平行測試的實踐更容易。
相關的範例程式碼位於GitHub
Case Study
下面的Update Class程式碼,需要考慮到 i18n字串的情形:
<?php
namespace DemoAbstract;
class Update
{
private $flagI18NEnable;
public function __construct($flagI18NEnable)
{
$this->flagI18NEnable = $flagI18NEnable;
}
public function execute():string
{
if ($this->flagI18NEnable) {
return 'Do A';
} else {
return 'Do B';
}
}
public function render()
{
if ($this->flagI18NEnable) {
return 'render A';
} else {
return 'render B';
}
}
}
在 exectue
和 render
中,我們都出現了 if
判斷式。
if ($this->flagI18NEnable) {
// Do A
} else {
// Do B
}
那測試代碼要怎麼寫??
變成我們需要測試 execute()
寫出兩個測試functions: testExecuteDoI18n()
and testExecuteDoNonI18N()
,並依照FLAG_I18N_ENABLED
條件去測試,結果如下:
<?php
namespace DemoAbstract;
require('./Update.php');
use PHPUnit\Framework\TestCase;
class UpdateTest extends TestCase
{
public function testExecuteDoI18n()
{
// Arrange
// 'FLAG_I18N_ENABLED', true
$update = new Update(true);
// Act
$result = $update->execute();
// Assert
$this->assertTrue($result == 'Do A');
}
public function testExecuteDoNonI18N()
{
// Arrange
// 'FLAG_I18N_ENABLED', false
$update = new Update(false);
// Act
$result = $update->execute();
// Assert
$this->assertTrue($result == 'Do B');
}
}
用 abstract 把 ifs 給拆了
我們可以用abstract的方式將 ifs
從中移除,新版的 update 更改如下:
<?php
namespace DemoAbstract;
abstract class Update
{
abstract public function execute():string;
abstract public function render():string;
}
class I18NUpdate extends Update
{
public function execute():string
{
return 'Do A';
}
public function render():string
{
return 'render A';
}
}
class NonI18NUpdate extends Update
{
public function execute():string
{
return 'Do B';
}
public function render():string
{
return 'render B';
}
}
如此一來,我們的測試就可以針對 I18NUpdate
和 NonI18NUpdate
這兩個物件分別建立,這樣讓測試碼很清楚,也不會出現類似 ifs
的條件判斷式在兩個 methods和測試的代碼內。
<?php
namespace DemoAbstract;
require('./UpdateVersion1.php');
use PHPUnit\Framework\TestCase;
class UpdateVersion1Test extends TestCase
{
public function testExecuteI18nUpdate()
{
$update = new I18NUpdate();
$result = $update->execute();
$this->assertTrue($result == 'Do A');
}
public function testExecuteNonI18NUpdate()
{
$update = new NonI18NUpdate();
$result = $update->execute();
$this->assertTrue($result == 'Do B');
}
}
解決對外使用物件的生成
我們現在有兩種 Update class,那生成時怎麼知道要用哪一個?在何時處理 ifs
的判斷比較好呢?
在Clean Code Talks – Inheritance, Polymorphism, & Testing的talks中Misko Hevery提出了很重要的觀點: piles of objects
vs piles of constructor
Piles of Objects | Piles of Construction |
---|---|
- Business Logics | - factories / builders / Provider<T> |
- Responsible for business logics, domain abstraction | - Responsible for building object graphs |
- Given the collaborators needed | - Creates and provides collaborators (Dependency Injection) |
左邊Pile of Objects主要是從 business logics的觀點來開發,但這會隨著業務邏輯的增加,讓原本的程式邏輯越來越多的 switch cases
or ifs
,這時就需要轉化這些邏輯到右邊的 Pile of Construction
。
用範例說明 Piles of Objects vs Construction
舉例來說,Class Consumer是屬於Piles of Objects
,負責執行UpdateInfo()
的業務邏輯。在 Consumer Class不應該把產生I18NUpdate
或是NonI18NUpdate
的物件邏輯放入代碼內。
class Consumer
{
private $update;
public function __construct($update)
{
$this->update = $update;
}
public function updateInfo():string
{
return $this->update->execute();
}
}
這時,我們就需要利用右側的 Piles of Construction
像是 Factory Design Pattern
去決定生成 I18NUpdate
or NonI18NUpdate
,以保持 Consumer Class的單純。像是下面的代碼中 makeConsumer()
class FactoryConsumer
{
public static function makeConsumer():Consumer
{
$bool = (!isset($_SERVER['FLAG_i18n_ENABLED']) ||
filter_var($_SERVER['FLAG_i18n_ENABLED'], FILTER_VALIDATE_BOOLEAN));
$update = $bool ? new I18NUpdate() : new NonI18NUpdate();
return new Consumer($update);
}
}
$consumer = FactoryConsumer::makeConsumer();
讓有關物件生成的邏輯,物件和物間的關聯,物件和物間的協同合作,交由piles of construction
的 factories, dependency injection等等的手法達成。
請注意 在上面範例程式中,我們運用了 $_SERVER
和 filter_var
兩個小技巧,所以我們可以透過 command line 輸入
FLAG_i18n_ENABLED=false php Consumer.php
設定FLAG_i18n_ENABLED
的參數,並轉換文字false
到正確的boolean
值。
以下為執行的結果:
總結
這樣的改寫帶來了下面的好處:
- 條件式集中在一個地方 (factory)
- 沒有重複的判斷邏輯
- 清楚的分離 Business Logic 物件和 Constructor 物件
- 利用 State 來控制物件行為
- 獨立測試和平行測試變得容易
- Subclasses 分工清楚