沒錯,這就是面向對象編程(設計模式)須要遵循的 6 個基本原則

本文首發於 沒錯,這就是面向對象編程(設計模式)須要遵循的 6 個基本原則,轉載請註明出處。

在討論面向對象編程和模式(具體一點來講,設計模式)的時候,咱們須要一些標準來對設計的好還進行判斷,或者說應該遵循怎樣的原則和指導方針。php

如今,咱們就來了解下這些原則:數據庫

  • 單一職責原則(S)
  • 開閉原則(O)
  • 里氏替換原則(L)
  • 接口隔離原則(I)
  • 依賴倒置原則(D)
  • 合成複用原則
  • 及迪米特法則(最少知道原則)

本文將涵蓋 SOLID + 合成複用原則的講解及示例,迪米特法則以擴展閱讀形式給出。編程

單一職責原則(Single Responsibility Principle[SRP]) ★★★★

一個類只負責一個功能領域中的相應職責(只作一類事情)。或者說一個類僅能有一個引發它變化的緣由。json

來看看一個功能太重的類示例:設計模式

/**
 * CustomerDataChart 客戶圖片處理
 */
class CustomerDataChart
{
    /**
    * 獲取數據庫鏈接
    */
    public function getConnection()
    {
    }

    /**
    * 查找全部客戶信息
    */
    public function findCustomers()
    {
    }

    /**
    * 建立圖表
    */
    public function createChart()
    {
    }

    /**
    * 顯示圖表
    */
    public function displayChart()
    {
    }
}

咱們發現 CustomerDataChart 類完成多個職能:緩存

  • 創建數據庫鏈接
  • 查找客戶
  • 建立和顯示圖表

此時,其它類若須要使用數據庫鏈接,沒法複用 CustomerDataChart;或者想要查找客戶也沒法實現複用。另外,修改數據庫鏈接或修改圖表顯示方式都須要修改 CustomerDataChart 類。這個問題挺嚴重的,不管修改什麼功能都須要多這個類進行編碼。架構

因此,咱們採用 單一職責原則 對類進行重構,以下:函數

/**
 * DB 類負責完成數據庫鏈接操做
 */
class DB
{
    public function getConnection()
    {
    }
}

/**
 * Customer 類用於從數據庫中查找客戶記錄
 */
class Customer
{
    private $db;

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

    public function findCustomers()
    {
    }
}

class CustomerDataChart
{
    private $customer;

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

    /**
    * 建立圖表
    */
    public function createChart()
    {
    }

    /**
    * 顯示圖表
    */
    public function displayChart()
    {
    }
}

重構完成後:優化

  • DB 類僅處理數據庫鏈接的問題,挺提供 getConnection() 方法獲取數據庫鏈接;
  • Customer 類完成操做 Customers 數據表的任務,這其中包括 CRUD 的方法;
  • CustomerDataChart 實現建立和顯示圖表。

各司其職,配合默契,完美!ui

開閉原則(Open-Closed Principle[OCP]) ★★★★★

開閉原則最重要 的面向對象設計原則,是可複用設計的基石。

「開閉原則」:對擴展開放、對修改關閉,即儘可能在不修改原有代碼的基礎上進行擴展。要想系統知足「開閉原則」,須要對系統進行 抽象

經過 接口抽象類 將系統進行抽象化設計,而後經過實現類對系統進行擴展。當有新需求須要修改系統行爲,簡單的經過增長新的實現類,就能實現擴展業務,達到在不修改已有代碼的基礎上擴展系統功能這一目標。

示例,系統提供多種圖表展示形式,如柱狀圖、餅狀圖,下面是不符合開閉原則的實現:

<?php

/**
 * 顯示圖表
 */
class ChartDisplay
{
    private $chart;

    /**
     * @param string $type 圖標實現類型
     */
    public function __construct(string $type)
    {
        switch ($type) {
            case 'pie':
                $this->chart = new PieChart();
                break;

            case 'bar':
                $this->chart = new BarChart();
                break;

            default:
                $this->chart = new BarChart();
        }

        return $this;
    }

    /**
     * 顯示圖標 
     */
    public function display()
    {
        $this->chart->render();
    }
}

/**
 * 餅圖
 */
class PieChart
{
    public function render()
    {
        echo 'Pie chart.';
    }
}

/**
 * 柱狀圖
 */
class BarChart
{
    public function render()
    {
        echo 'Bar chart.';
    }
}

$pie = new ChartDisplay('pie');
$pie->display(); //Pie chart.

$bar = new ChartDisplay('bar');
$bar->display(); //Bar chart.

在這裏咱們的 ChartDisplay 每增長一種圖表顯示,都須要在構造函數中對代碼進行修改。因此,違反了 開閉原則。咱們能夠經過聲明一個 Chart 抽象類(或接口),再將接口傳入 ChartDisplay 構造函數,實現面向接口編程。

/**
* 圖表接口
*/
interface ChartInterface
{
    /**
    * 繪製圖表
    */
    public function render();
}

class PieChart implements ChartInterface
{
    public function render()
    {
        echo 'Pie chart.';
    }
}

class BarChart implements ChartInterface
{
    public function render()
    {
        echo 'Bar chart.';
    }
}

/**
 * 顯示圖表
 */
class ChartDisplay
{
    private $chart;

    /**
     * @param ChartInterface $chart
     */
    public function __construct(ChartInterface $chart)
    {
        $this->chart = $chart;
    }

    /**
     * 顯示圖標 
     */
    public function display()
    {
        $this->chart->render();
    }
}


$config = ['PieChart', 'BarChart'];

foreach ($config as $key => $chart) {
    $display = new ChartDisplay(new $chart());
    $display->display();
}

修改後的 ChartDisplay 經過接收 ChartInterface 接口做爲構造函數參數,實現了圖表顯示不依賴於具體的實現類即 面向接口編程。在不修改源碼的狀況下,隨時增長一個 LineChart 線狀圖表顯示。具體圖表實現能夠從配置文件中讀取。

里氏替換原則(Liskov Substitution Principle[LSP]) ★★★★★

里氏代換原則:在軟件中將一個基類對象替換成它的子類對象,程序將不會產生任何錯誤和異常,反過來則不成立。若是一個軟件實體使用的是一個子類對象的話,那麼它不必定可以使用基類對象。

示例,咱們的系統用戶類型分爲:普通用戶(CommonCustomer)和 VIP 用戶(VipCustomer),當用戶收到留言時須要給用戶發送郵件通知。原系統設計以下:

<?php

/**
 * 發送郵件
 */
class EmailSender
{
    /**
     * 發送郵件給普通用戶
     *
     * @param CommonCustomer $customer
     * @return void
     */
    public function sendToCommonCustomer(CommonCustomer $customer)
    {
        printf("Send email to %s[%s]", $customer->getName(), $customer->getEmail());
    }
    
    /**
     * 發送郵件給 VIP 用戶
     * 
     * @param VipCustomer $vip
     * @return void
     */
    public function sendToVipCustomer(VipCustomer $vip)
    {
        printf("Send email to %s[%s]", $vip->getName(), $vip->getEmail());
    }    
}

/**
 * 普通用戶
 */
class CommonCustomer
{
    private $name;
    private $email;

    public function __construct(string $name, string $email)
    {
        $this->name = $name;
        $this->email = $email;
    }

    public function getName()
    {
        return $this->name;
    }

    public function getEmail()
    {
        return $this->email;
    }
}

/**
 * Vip 用戶
 */
class VipCustomer
{
    private $name;
    private $email;

    public function __construct(string $name, string $email)
    {
        $this->name = $name;
        $this->email = $email;
    }

    public function getName()
    {
        return $this->name;
    }

    public function getEmail()
    {
        return $this->email;
    }
}

$customer = new CommonCustomer("liugongzi", "liuqing_hu@126.com");
$vip = new VipCustomer("vip", "liuqing_hu@126.com");

$sender = new EmailSender();
$sender->sendToCommonCustomer($customer);// Send email to liugongzi[liuqing_hu@126.com]
$sender->sendToVipCustomer($vip);// Send email to vip[liuqing_hu@126.com]

這裏,爲了演示說明咱們經過在 EmailSender 類中的 send* 方法中使用類型提示功能,對接收參數進行限制。因此若是有多個用戶類型可能就須要實現多個 send 方法才行。

依據 里氏替換原則 咱們知道,可以接收父類的地方 必定 可以接收子類做爲參數。因此咱們僅需定義 send 方法來接收父類便可實現不一樣類型用戶的郵件發送功能:

<?php

/**
 * 發送郵件
 */
class EmailSender
{
    /**
     * 發送郵件給普通用戶
     *
     * @param CommonCustomer $customer
     * @return void
     */
    public function send(Customer $customer)
    {
        printf("Send email to %s[%s]", $customer->getName(), $customer->getEmail());
    }
}

/**
 * 用戶抽象類
 */
abstract class Customer
{
    private $name;
    private $email;

    public function __construct(string $name, string $email)
    {
        $this->name = $name;
        $this->email = $email;
    }

    public function getName()
    {
        return $this->name;
    }

    public function getEmail()
    {
        return $this->email;
    }

}


/**
 * 普通用戶
 */
class CommonCustomer extends Customer
{
}

/**
 * Vip 用戶
 */
class VipCustomer extends Customer
{
}

$customer = new CommonCustomer("liugongzi", "liuqing_hu@126.com");
$vip = new VipCustomer("vip", "liuqing_hu@126.com");

$sender = new EmailSender();
$sender->send($customer);// Send email to liugongzi[liuqing_hu@126.com]
$sender->send($vip);// Send email to vip[liuqing_hu@126.com]

修改後的 send 方法接收 Customer 抽象類做爲參數,到實際運行時傳入具體實現類就能夠輕鬆擴展需求,再多客戶類型也不用擔憂了。

依賴倒置原則(Dependence Inversion Principle[DIP]) ★★★★★

依賴倒轉原則:抽象不該該依賴於細節,細節應當依賴於抽象。換言之,要針對接口編程,而不是針對實現編程。

在里氏替換原則中咱們在未進行優化的代碼中將 CommonCustomer 類實例做爲 sendToCommonCustomer 的參數,來實現發送用戶郵件的業務邏輯,這裏就違反了「依賴倒置原則」。

若是想在模塊中實現符合依賴倒置原則的設計,要將依賴的組件抽象成更高層的抽象類(接口)如前面的 Customer 類,而後經過採用 依賴注入(Dependency Injection) 的方式將具體實現注入到模塊中。另外,就是要確保該原則的正確應用,實現類應當僅實如今抽象類或接口中聲明的方法,不然可能形成沒法調用到實現類新增方法的問題。

這裏提到「依賴注入」設計模式,簡單來講就是將系統的依賴有硬編碼方式,轉換成經過採用 設值注入(setter)構造函數注入接口注入 這三種方式設置到被依賴的系統中,感興趣的朋友能夠閱讀我寫的 深刻淺出依賴注入 一文。

舉例,咱們的用戶在登陸完成後須要經過緩存服務來緩存用戶數據:

<?php

class MemcachedCache
{
    public function set($key, $value)
    {
        printf ("%s for key %s has cached.", $key, json_encode($value));
    }
}

class User
{
    private $cache;

    /**
     * User 依賴於 MemcachedCache 服務(或者說組件)
     */
    public function __construct()
    {
        $this->cache = new MemcachedCache();
    }

    public function login()
    {
        $user = ['id' => 1, 'name' => 'liugongzi'];
        $this->cache->set('dp:uid:' . $user['id'], $user);
    }
}

$user = new User();
$user->login(); // dp:uid:1 for key {"id":1,"name":"liugongzi"} has cached.

這裏,咱們的緩存依賴於 MemcachedCache 緩存服務。然而因爲業務的須要,咱們須要緩存服務有 Memacached 遷移到 Redis 服務。固然,現有代碼中咱們就沒法在不修改 User 類的構造函數的狀況下輕鬆完成緩存服務的遷移工做。

那麼,咱們能夠經過使用 依賴注入 的方式,來實現依賴倒置原則:

<?php

class Cache
{
    public function set($key, $value)
    {
        printf ("%s for key %s has cached.", $key, json_encode($value));
    }
}

class RedisCache extends Cache
{
}

class MemcachedCache extends Cache
{
}

class User
{
    private $cache;

    /**
     * 構造函數注入
     */
    public function __construct(Cache $cache)
    {
        $this->cache = $cache;
    }

    /**
     * 設值注入
     */
    public function setCache(Cache $cache)
    {
        $this->cache = $cache;
    }

    public function login()
    {
        $user = ['id' => 1, 'name' => 'liugongzi'];
        $this->cache->set('dp:uid:' . $user['id'], $user);
    }
}

// use MemcachedCache
$user =  new User(new MemcachedCache());
$user->login(); // dp:uid:1 for key {"id":1,"name":"liugongzi"} has cached.

// use RedisCache
$user->setCache(new RedisCache());
$user->login(); // dp:uid:1 for key {"id":1,"name":"liugongzi"} has cached.

完美!

接口隔離原則(Interface Segregation Principle[ISP]) ★★

接口隔離原則:使用多個專門的接口,而不使用單一的總接口,即客戶端不該該依賴那些它不須要的接口。

簡單來講就是不要讓一個接口來作太多的事情。好比咱們定義了一個 VipDataDisplay 接口來完成以下功能:

  • 經過 readUsers 方法讀取用戶數據;
  • 可使用 transformToXml 方法將用戶記錄轉存爲 XML 文件;
  • 經過 createChart 和 displayChart 方法完成建立圖表及顯示;
  • 還能夠經過 createReport 和 displayReport 建立文字報表及現實。
abstract class VipDataDisplay
{
    public function readUsers()
    {
        echo 'Read all users.';
    }

    public function transformToXml()
    {
        echo 'save user to xml file.';
    }

    public function createChart()
    {
        echo 'create user chart.';
    }

    public function displayChart()
    {
        echo 'display user chart.';
    }

    public function createReport()
    {
        echo 'create user report.';
    }

    public function displayReport()
    {
        echo 'display user report.';

    }
}

class CommonCustomerDataDisplay extends VipDataDisplay
{

}

如今咱們的普通用戶 CommonCustomerDataDisplay 不須要 Vip 用戶這麼複雜的展示形式,僅須要進行圖表顯示便可,可是若是繼承 VipDataDisplay 類就意味着繼承抽象類中全部方法。

如今咱們將 VipDataDisplay 抽象類進行拆分,封裝進不一樣的接口中:

interface ReaderHandler
{
    public function readUsers();
}

interface XmlTransformer
{
    public function transformToXml();
}

interface ChartHandler
{
    public function createChart();

    public function displayChart();
}

interface ReportHandler
{
    public function createReport();

    public function displayReport();
}

class CommonCustomerDataDisplay implements ReaderHandler, ChartHandler
{
    public function readUsers()
    {
        echo 'Read all users.';
    }

    public function createReport()
    {
        echo 'create user report.';
    }

    public function displayReport()
    {
        echo 'display user report.';

    }
}

重構完成後,僅需在實現類中實現接口中的方法便可。

合成複用原則(Composite Reuse Principle[CRP]) ★★★★

合成複用原則:儘可能使用對象組合,而不是繼承來達到複用的目的。

合成複用原則就是在一個新的對象裏經過關聯關係(包括組合和聚合)來使用一些已有的對象,使之成爲新對象的一部分;新對象經過委派調用已有對象的方法達到複用功能的目的。簡言之:複用時要儘可能使用組合/聚合關係(關聯關係) ,少用繼承。

什麼時候使用繼承,什麼時候使用組合(或聚合)?

當兩個類之間的關係屬於 IS-A 關係時,如 dog is animal,使用 繼承;而若是兩個類之間屬於 HAS-A 關係,如 engineer has a computer,則優先選擇組合(或聚合)設計。

示例,咱們的系統有用日誌(Logger)功能,而後咱們實現了向控制檯輸入日誌(SimpleLogger)和向文件寫入日誌(FileLogger)兩種實現:

<?php

abstract class Logger
{
    abstract public function write($log);
}

class SimpleLogger extends Logger
{
    public function write($log)
    {
        print((string) $log);
    }
}

class FileLogger extends Logger
{
    public function write($log)
    {
        file_put_contents('logger.log', (string) $log);
    }
}

$log = "This is a log.";

$sl = new SimpleLogger();
$sl->write($log);// This is a log.

$fl = new FileLogger();
$fl->write($log);

看起來很好,咱們的簡單日誌和文件日誌可以按照咱們預約的結果輸出和寫入文件。很快,咱們的日誌需求有了寫加強,如今咱們須要將日誌同時向控制檯和文件中寫入。有幾種解決方案吧:

  • 從新定義一個子類去同時寫入控制檯和文件,但這彷佛沒有用上咱們已經定義好的兩個實現類:SimpleLogger 和 FileLogger;
  • 去繼承其中的一個類,而後實現另一個類的方法。好比繼承 SimpleLogger,而後實現寫入文件日誌的方法;嗯,沒辦法 PHP 是單繼承的語言;
  • 使用組合模式,將 SimpleLogger 和 FileLogger 聚合起來使用。

咱們直接看最後一種解決方案吧,前兩種實在是有點。

class AggregateLogger
{
    /**
     * 日誌對象池
     */
    private $loggers = [];

    public function addLogger(Logger $logger)
    {
        $hash = spl_object_hash($logger);
        $this->loggers[$hash] = $logger;
    }

    public function write($log)
    {
        array_map(function ($logger) use ($log) {
            $logger->write($log);
        }, $this->loggers);
    }
}

$log = "This is a log.";

$aggregate = new AggregateLogger();

$aggregate->addLogger(new SimpleLogger());// 加入簡單日誌 SimpleLogger
$aggregate->addLogger(new FileLogger());// 鍵入文件日誌 FileLogger

$aggregate->write($log);

能夠看出,採用聚合的方式咱們能夠很是輕鬆的實現複用。

迪米特法則

設計模式六大原則(5):迪米特法則

感謝 Liuwei-Sunny 大神在「軟件架構、設計模式、重構、UML 和 OOAD」領域的分享,纔有此文。

相關文章
相關標籤/搜索