面試官:物件導向的三大特性和五大原則是什麼?

boring發表於2019-06-19

物件導向的三大特性

封裝,繼承,多型

什麼是封裝?

把客觀的事物封裝成抽象的類,並且類可以把自己的資料和方法只讓可信的類或者物件操作,對不可信的類進行資訊的隱藏。簡單的說就是:封裝使物件的設計者與物件的使用者分開,使用者只要知道物件可以做什麼就可以了,不需要知道具體是怎麼實現的。封裝可以有助於提高類和系統的安全性。

什麼是繼承?

繼承指的是建立一個新的派生類,從一個或多個先前定義的類中繼承資料和函式,可以重新定義或加進新資料和函式,從而建立了類的層次或等級。

什麼是多型?

多型性指的是: 同一操作作用與不同類的例項,將產生不同的執行結果,即不同類的物件收到相同的訊息時,將得到不同的結果。

程式碼示例


class eat
{
    public function breakfast()
    {
        echo "吃早飯!";
    }
}

class typist
{
    public function type()
    {
        echo "打字!";
    }

}

function printWorking($obj)
{
    if ($obj instanceof eat) {
        echo $obj->breakfast();
    } elseif ($obj instanceof typist) {
        echo $obj->type();
    } else {
        echo "error";
    }
}

printWorking(new eat());
echo PHP_EOL;
printWorking(new typist());

輸出:

吃早飯! 打字!

物件導向的五大原則

單一職責原則,開放封閉原則,里氏替換原則,依賴倒置原則,介面隔離原則

什麼是單一職責原則?

簡單的來說就是: 一個類只做一件事情,不要為類實現過多的功能點,避免相同的職責分散到不同的類中。如果一個類的職責過多可能引起變化的原因也就越多,使程式碼更加的耦合。如果雜交不清的職責將使得程式碼難以維護,牽一髮而動全身。

例如:工廠模式,工廠模式之所以被稱為工廠模式是因為它負責 “生產” 物件。

程式碼示例

工廠模式的好處:多個地方new了一個物件,當此類名發生改變就不需要一個個去修改,只需要修改一處地方。

<?php
class MyObject
{
    public function __construct()
    {
        echo "test code";
    }
}

//工廠類
class MyFactory
{
    public static function factory()
    {
        return new MyObject();
   }
}

$instance = MyFactory::factory();//test code

什麼是開放封閉原則?

開放封閉原則: 一個類是可擴充套件的,而不可修改的。也就是說,對擴充套件開放,對修改封閉。

1.對擴充套件開放,意味著 有新的需求或變化時,可以對現有的程式碼進行擴充套件,以適應新的情況。

2.對修改封閉,在對模組功能進行擴充套件時,不應該影響已有的程式模組。

實現開放封閉的原則的重點:抽象程式設計,而不是具體程式設計,因為抽象相對穩定,讓類依賴於固定的抽象類和介面,所以修改就是封閉的。而物件導向的繼承和多型機制,又可以繼承抽象類或者實現介面,透過重寫其方法來改變固有的行為,實現方法新的擴充,所以就是開放。

例如:裝飾器模式(Decorator),可以動態地新增修改類的功能。一個類提供了一項功能,如果要在修改並新增額外的功能,傳統的程式設計模式,需要寫一個子類去繼承它,並重新實現類的方法,使用裝飾器模式,僅需在執行時新增一個裝飾器物件即可實現,可以實現最大的靈活性。

<?php

/**
 * 輸出一個字串
 * 裝飾器動態新增功能
 * Class EchoText
 */
class EchoText
{
    protected $decorator = [];

    public function Index()
    {
        //呼叫裝飾器前置操作
        $this->beforeEcho();
        echo "你好,我是裝飾器。";
        //呼叫裝飾器後置操作
        $this->afterEcho();
    }

    //增加裝飾器
    public function addDecorator(Decorator $decorator)
    {
        $this->decorator[] = $decorator;
    }

    //執行裝飾器前置操作 先進先出原則
    protected function beforeEcho()
    {
        foreach ($this->decorator as $decorator)
            $decorator->before();
    }

    //執行裝飾器後置操作 先進後出原則
    protected function afterEcho()
    {
        $tmp = array_reverse($this->decorator);
        foreach ($tmp as $decorator)
            $decorator->after();
    }
}


/**
 * 裝飾器介面
 * Class Decorator
 */
interface Decorator
{
    public function before();

    public function after();
}

/**
 * 顏色裝飾器實現
 * Class ColorDecorator
 */
class ColorDecorator implements Decorator
{
    protected $color;

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

    public function before()
    {
        echo "<dis style='color: {$this->color}'>";
    }

    public function after()
    {
        echo "</div>";
    }
}

/**
 * 字型大小裝飾器實現
 * Class SizeDecorator
 */
class SizeDecorator implements Decorator
{
    protected $size;

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

    public function before()
    {
        echo "<dis style='font-size: {$this->size}px'>**";
    }

    public function after()
    {
        echo "</div>";
    }
}

//例項化輸出類
$echo = new EchoText();
//增加裝飾器
$echo->addDecorator(new ColorDecorator('red'));
//增加裝飾器
$echo->addDecorator(new SizeDecorator('22'));
//輸出
$echo->Index();

什麼是里氏替換原則?

由於物件導向程式設計技術中的繼承在具體程式設計中過於簡單,在許多系統的設計和程式設計實現中,我們並沒有認真地、理性地思考應用系統中各個類之間的繼承好關係是否合適,派生類是否能正確地對基類中的某些方法進行重寫的問題。因此經常出現濫用繼承或者錯誤的繼承現象,給系統後期的維護帶來了不少麻煩。

核心思想:子類必須能夠替換其父類。這一思想體現為對繼承機制的約束規範,只有子類能夠替換父類時才能保證系統在執行期內識別子類,這是保證繼承複用的基礎。

<?php
//例子1
class Bird{
    protect function fly(){

    }
}
//翠鳥
class KingFisher extends Bird{

}

//鴕鳥
class Ostrich extends Bird{
    //鴕鳥不會飛啊
}

//例子2

class A{
    protect function add($a, $b){
        return $a + $b;
    }
}

//過載
class B extends A{

    protected function add($a, $b){
        return $a + $b + 100;
    }
}

里氏替換原則是對類繼承的一種約束。對里氏替換原則有兩種理解:

1.不能隨便去繼承不合適的,有多餘方法或者屬性的類(例子1)。

2.子類可以擴充套件父類的功能,但不能改變父類原有的功能(例子2)。

里氏替換原則包含一下幾個隱藏含義:

1.子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法。

2.子類中可以增加自己特有的方法。

3.當子類的方法過載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入引數更寬鬆。

4.當子類的方法實現父類的抽象方法時,方法的後置條件(即方法的返回值)要比父類更嚴格。

什麼是依賴倒置原則?

簡單的來說就是:一個類,不應該強制依賴另一個類,每個類對於另外一個類都是可替換的。

具體概念:

1.上層模組不應該依賴於下層模組,它們共同依賴於一個抽象(父類不能依賴子類,它們都要依賴抽象類)。

2.抽象不能依賴於具體,具體應該依賴於抽象。

為什麼要依賴介面?因為介面體現對問題的抽象,同時由於抽象一般是相對穩定的或者是相對變化不頻繁的,而具體是易變的。因此,依賴抽象是實現程式碼擴充套件和執行期內繫結(多型)的基礎:只要實現了該抽象類的子類,都可以被類的使用者使用。

<?php

interface employee
{
    public function working();
}

class teacher implements employee//具體應該依賴與抽象
{
    public function working(){
        echo 'teaching...';
    }
}

class coder implements employee
{
    public function working(){
        echo 'coding...';
    }
}

class workA//例子1
{
    public function work(){
        $teacher = new teacher;
        $teacher->working();
    }
}

class workB//例子2
{
    private $e;
    public function set(employee $e){
        $this->e = $e;
    }

    public function work(){
        $this->e->working();
    }
}

$worka = new workA;//workA 依賴於 teacher 類 不符合依賴倒置原則
$worka->work();
$workb = new workB;//workB 不依賴與某個類 既可以注入 teacher 也可以 注入 coder
$workb->set(new teacher());
$workb->work();

在workA(例子1)中,work方法依賴於teacher實現;在workB(例子2)中,work轉而依賴抽象,這樣可以把需要的物件透過引數傳入。上述程式碼透過介面,實現了一定程度的解耦,但仍然是有限的。不僅是使用介面,使用工廠等也能實現一定程度的解耦和依賴倒置。

在workB中,teacher例項透過set方法傳入,從而實現了工廠模式。由於這樣的實現仍然是硬編碼的,為了實現程式碼的進一步擴充套件,把這個依賴關係寫在配置檔案裡,指明workB需要一個teacher物件,專門由一個程式配置是否正確(如所依賴的類檔案是否存在)以及載入配置中所依賴的實現,這個檢測程式,就稱為IOC容器(這裡不清楚IOC的小夥伴可以自行谷歌)。

什麼是介面隔離原則?

其核心思想是:使用多個小的專門的介面,而不要使用一個大的總介面(只需要關心介面,不需要關心實現)。

介面隔離原則體現在:

1.介面應該是內聚的,應該避免 “胖” 介面。

2.不要強迫依賴不用的方法,這是一種介面汙染。

3.表明客戶端不應該被強迫實現一些不會使用的介面,應該把胖介面分組,用多個介面代替它,每個介面服務於一個子模組。簡單地說,就是使用多個專門的介面比使用單個介面要好很多。

隔離的手段主要有以下兩種:
1、委託分離,透過增加一個新的型別來委託客戶的請求,隔離客戶和介面的直接依賴,但是會增加系統的開銷(設計模式中,如:代理模式,策略模式中都用到了委託的概念,好奇的小夥伴可以自行谷歌,這裡就不貼程式碼了)。

2、多重繼承分離,透過介面多繼承來實現客戶的需求,這種方式是較好的。

胖介面的例項說明

<?php
interface Animal{
  public function walk();
  public function speak();
}

//實現狗的一個介面

class Dog implements Animal{
  public function walk(){
    echo "dogs can walk";
  }
  public function speak(){
    echo "dogs can speak";
  }
}

//ok,現在我們想建立一個魚類,它會游泳,怎麼辦呢?
//我們必須要修改介面,還會影響到dog類的實現,而fish也需要實現walk和speak方法,如下程式碼所示:

interface Animal{
  public function walk();
  public function speak();
  public function swim();
}

//修改後的Gog類
class Dog implements Animal{
  public function walk(){
    echo "dogs can walk";
  }
  public function speak(){
    echo "dogs can speak";
  }
  public function swim(){
  }
}

//魚類
class Fish implements Animal{
  public function walk(){
  }
  public function speak(){
  }
  public function swim(){
    echo "fish can swim";
  }
}

這時Animal介面類就呈現出了”胖“介面的特徵了。所謂胖介面其實就是介面中定義了不是所有實現類都需要的方法,就像Animal介面類,有些動物是不會游泳的,有些動物是不會行走的,還有些動物是不會飛的。如果將這些方法都寫在一個Animal介面類中,那麼後期的擴充套件和維護簡直就是一場災難。

那麼,怎麼解決以上問題呢?

很簡單,介面細化即可,將Animal介面類拆分成三個介面類,然後用多繼承分離介面就ok了。

<?php
interface animalCanSpeak{
  public function speak();
}

interface AnimalCanSwim{
  public function swim();
}

interface animalCanSpeak{
  public function speak();
}

//定義好這幾個介面類之後,dog和fish的實現就容易多了

//狗類
class Dog implements animalCanSpeak,animalCanWalk{
  public function walk(){
    echo "dogs can walk";
  }
  public function speak(){
    echo "dogs can speak";
  }
}

//魚類
class Fish implements AnimalCanSwim{
  public function swim(){
    echo "fish can swim";
  }
}

介面隔離原則(Interface Segregation Principle, ISP)的概念:使用多個專門的介面,而不使用單一的總介面,即客戶端不應該依賴那些它不需要的介面。

在使用介面隔離原則時,我們需要注意控制介面的粒度,介面不能太小,如果太小會導致系統中介面氾濫,不利於維護;介面也不能太大,太大的介面將違背介面隔離原則,靈活性較差,使用起來很不方便。一般而言,介面中僅包含為某一類使用者定製的方法即可,不應該強迫客戶依賴於那些它們不用的方法。

結語

實踐出真理

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

相關文章