設計模式 - 原則及例項講解

心智極客發表於2020-02-19

關於設計模式

設計模式是一些典型問題的優秀實踐,是自下而上的,設計模式來源於實踐。

在程式設計過程中,相同的問題不斷出現,程式設計師一次次的解決,久而久之,總結出了特定問題的解決方式,並將其看作是優秀的實踐。這些優秀實踐就是設計模式。從這裡可以看出,設計模式是針對場景而言的,並不是使用越多越好,因為設計模式不是科學理論,而是注重實效的。即使是同一個設計模式,在不同的上下文中實現也可能不同。

通常,設計模式由以下幾部分組成

  • 命名。命名直接體現出了問題和解決方案,必須兼顧簡潔性和描述性。
  • 問題。模式是為了解決問題而存在的,因此問題及問題發生的環境是設計模式的基礎。
  • 解決方案。模式的解決方案並非真正的解決方案,更像是一個半成品,編碼人員需要根據具體的場景來完成具體的實現。
  • 效果。設計模式會帶來某種結果,除了收益外,還需要考慮風險。

原則

對於人類來說,存在一些普適原則,適用於不同的人,例如「積極主動」、「要事第一」等。而對於軟體開發而言,同樣也有普適原則的存在,設計模式之所以稱之為優秀實踐,就是因為它們的設計符合一些軟體開發的通用原則:

  • 組合優於繼承
  • 保持元件的松耦合
  • 針對介面程式設計,而非針對實現程式設計。
  • 封裝變化。

案例分析

需求

課程 (Lesson) 分為 固定收費課程 (FixedPriceLesson) 與 計時收費課程 (TimedPriceLesson),前者一次收費 30,後者一小時收費 5塊。

設計

實現

<?php

abstract class Lesson
{

    /**
     * 課程時長
     *
     * @var int
     */
    protected $duration;

    /**
     * Lesson constructor.
     *
     * @param int $duration
     */
    public function __construct(int $duration)
    {
        $this->duration = $duration;
    }

    /**
     * 收費
     *
     * @return int
     */
    abstract function cost(): int;

    /**
     * 收費型別
     *
     * @return string
     */
    abstract function chargeType() : string;
}


class FixedPriceLesson extends Lesson
{
    public function cost(): int
    {
        return 30;
    }

    public function chargeType(): string
    {
        return '固定收費';
    }
}

class TimedPriceLesson extends Lesson
{
    public function cost(): int
    {
        return $this->duration * 5;
    }
    public function chargeType(): string
    {
        return '計時收費';
    }
}

當我們要對課程型別進一步擴充套件時,就會變得很麻煩。例如,要將課程要分成講座(Lecture)和 研討會(Seminar),每種課程又包含了固定收費和即時收費。如果按照繼承的思想來設計的話,就會涉及到大量的改動,並且後期的擴充套件性很差

<?php

abstract class Lesson {}

class Lecture extends Lesson {}
class FixedPriceLesson extends Lecture {}
class TimedPriceLesson extends Lecture {}

我們從設計模式的幾個原則來分析下這個例子

  • 違反了「組合優於繼承」原則 - 所有的功能都透過繼承來實現,導致 Lesson 類承擔過多的職責;
  • 違反了「保持元件的松耦合」原則 - Lesson 類與收費這兩者之間是緊密耦合的,這樣的話,一個元件的變化必然涉及到另外一個元件的變化,當我們引入新的課程時,就同時需要涉及到 Lesson 和收費的變動,導致程式碼複雜度增加;
  • 違反了「針對介面程式設計,而非針對實現程式設計」原則。在該例子中,我們始終是針對具體的實現去程式設計;
  • 沒有「封裝變化」。收費策略是不斷變化的,在該例子中,我們沒有將變化的要素封裝起來。

重構

按照設計原則,將變化的因素封裝起來,這裡變化的因素就是「收費」這一行為,同時,針對介面程式設計,所以需要定義一個抽象的收費策略介面。

設計

定義收費策略介面

<?php

abstract class CostStrategy
{
    abstract public function cost(Lesson $lesson): int;
    abstract public function chargeType(): string;
}

不同的收費策略具體的實現

<?php

class FixedCostStrategy extends CostStrategy
{
    public function cost(Lesson $lesson): int
    {
        return 30;
    }

    public function chargeType(): string
    {
        return "固定收費";
    }
}

class TimedCostStrategy extends CostStrategy
{
    public function cost(Lesson $lesson): int
    {
        return ($lesson->getDuration() * 5);
    }

    public function chargeType(): string
    {
        return "即時收費";
    }
}

父類 Lesson 只需要傳入抽象的收費策略介面即可,保證了職責的單一性

<?php

abstract class Lesson
{
    private $duration;
    private $costStrategy;  

    public function __construct(int $duration, CostStrategy $strategy)
    {
        $this->duration = $duration;
        $this->costStrategy = $strategy; 
    }

    public function cost(): int
    {
        return $this->costStrategy->cost($this);
    }

    public function chargeType(): string
    {
        return $this->costStrategy->chargeType();
    }

    public function getDuration(): int
    {
        return $this->duration;
    }
}

class Lecture extends Lesson {}
class Seminar extends Lesson {}

測試

<?php

$lessons1 = new Seminar(4, new TimedCostStrategy());
$lessons2 = new Lecture(4, new FixedCostStrategy());

$lesson1->cost();
$lesson1->chargeType();

$lesson2->cost();
$lesson2->chargeType();

訊息通知

最後,在現有的基礎上增加訊息通知功能,即課程註冊成功後,傳送通知。對於該功能,可以進行簡單的分析

  • 通知可以分很多種,郵件、簡訊等等,因此,需要將這一不斷變化的行為進行封裝;
  • 需要有一個通知管理類,用於註冊課程和傳送通知;

設計

訊息通知

<?php

abstract class Notifier
{

    public static function getNotifier(): Notifier
    {   

        if (rand(1, 2) === 1) { // 方便測試,使用了隨機返回,實際上可以根據配置檔案來決定返回哪種通知
            return new MailNotifier();
        } else {
            return new TextNotifier();
        }
    }

    abstract public function inform($message);
}

class MailNotifier extends Notifier
{
    public function inform($message)
    {
        print "郵件通知: {$message}\n";
    }
}

class TextNotifier extends Notifier
{
    public function inform($message)
    {
        print "簡訊通知: {$message}\n";
    }
}

通知管理

<?php

/**
 * 客戶端用於註冊課程,併傳送通知
 */
class RegistrationMgr
{
    public function register(Lesson $lesson)
    {
        $notifier = Notifier::getNotifier();
        $notifier->inform("新課程花費 ({$lesson->cost()})");
    }
}

測試

<?php

$mgr = new RegistrationMgr();
$mgr->register($lessons1);
$mgr->register($lessons2);

參考

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章