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';
        }
    }
}

exectuerender 中,我們都出現了 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';
    }
}

如此一來,我們的測試就可以針對 I18NUpdateNonI18NUpdate 這兩個物件分別建立,這樣讓測試碼很清楚,也不會出現類似 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等等的手法達成。

請注意 在上面範例程式中,我們運用了 $_SERVERfilter_var 兩個小技巧,所以我們可以透過 command line 輸入

FLAG_i18n_ENABLED=false php Consumer.php

設定FLAG_i18n_ENABLED的參數,並轉換文字false到正確的boolean值。

以下為執行的結果:

https://gyazo.com/232f3fe9f15278f881f46400b0603858

總結

這樣的改寫帶來了下面的好處:

  • 條件式集中在一個地方 (factory)
  • 沒有重複的判斷邏輯
  • 清楚的分離 Business Logic 物件和 Constructor 物件
  • 利用 State 來控制物件行為
  • 獨立測試和平行測試變得容易
  • Subclasses 分工清楚

results matching ""

    No results matching ""