前言
設計模式作為一個程式設計師,相信大家肯定不會陌生,它是一些成熟的且通用的程式設計解決方案,針對這些肯定會存在一些理論基礎,來為這些這些模式提供理論依據,這裡我們就要先搞明白這些理論到底是什麼,這樣我們對設計模式有事半功倍的效果。
設計模式的基本原則:
1. 單一職責原則
2. 開閉原則
3. 裡式替換原則
4. 依賴倒轉原則
5. 介面隔離原則
6. 合成複用原則
7. 迪米特法則
單一職責原則
定義:規定每個類都應該有一個單一的功能,並且該功能應該由這個類完全封裝起來。
作用:它用於控制類的粒度大小
這個原則很好理解,我們的類在做某些事情的時候只專注與自己領域內的事兒就可以了,譬如我們的模型類就只針對特定模型進行操作,而不會去關心操作類裡面的邏輯,這樣單一的職責隔離,可以方便我們維護。
舉個理想化例子:
現實生活中,我們的攝影師是什麼都乾的,佈景、服裝、燈光、拍照,可以說是累成狗。
但是在程式設計的世界裡面,我們更加希望的是這樣:
- 我們的攝影師主要負責就是控制相機,指揮助手。
- 指導助手佈景,而具體的佈景、服裝和燈光佈置可以交給我們助手。
我們用程式碼模擬下現實生活:
<?php
declare(strict_types=1);
namespace Neilyoz\DesignPatternBase\SingleResponsibility;
class RealPhotographer
{
private string $name;
public function __construct(string $name)
{
$this->name = $name;
}
// 溝通
public function communicate(): void
{
echo $this->name . ' 在溝通' . PHP_EOL;
}
// 佈置場景
public function layout()
{
echo $this->name . ' 在佈置場景' . PHP_EOL;
}
// 搭配服裝
public function matchingClothing()
{
echo $this->name . ' 搭配衣服' . PHP_EOL;
}
// 調整燈光
public function adjustTheLights()
{
echo $this->name . ' 調整燈光' . PHP_EOL;
}
// 控制相機
public function controlCamera()
{
echo $this->name . ' 控制相機' . PHP_EOL;
}
}
我們可以看到一個攝影師負責了方方面面,俗話就是管的太寬了,我們要縮小粒度,讓我們的攝影師只是專注拍照的本質,所以我們可以把 佈置場景
、搭配服裝
、調整燈光
交給助手去完成。我們來修改下這個方法,讓特定的事兒給專業的人去完成。
我們修改下這個類:
<?php
declare(strict_types=1);
namespace Neilyoz\DesignPatternBase\SingleResponsibility;
// 攝影師類
class Photographer
{
private string $name;
public function __construct(string $name)
{
$this->name = $name;
}
// 控制相機
public function controlCamera()
{
echo $this->name . " 操作控制相機拍照" . PHP_EOL;
}
// 指揮助手
public function commandAssistant(Helper $helper)
{
$helper->receivedCommand();
$helper->matchingClothing();
$helper->adjustTheLights();
$helper->layout();
}
}
// 助手類
class Helper
{
private string $name;
public function __construct(string $name)
{
$this->name = $name;
}
public function receivedCommand()
{
echo $this->name . '收到指揮' . PHP_EOL;
}
// 佈置場景
public function layout()
{
echo $this->name . ' 在佈置場景' . PHP_EOL;
}
// 搭配服裝
public function matchingClothing()
{
echo $this->name . ' 搭配衣服' . PHP_EOL;
}
// 調整燈光
public function adjustTheLights()
{
echo $this->name . ' 調整燈光' . PHP_EOL;
}
}
這樣我們就把職責更加明確的分配了,其實程式設計師有時候更像是一個管理者的覺得,我們需要管理具體的類去做具體的事兒。在管理這些類的時候,我們要合理的劃分這些類的職責,否則職責到後面越來越混亂,反而影響我們的管理。所以單一職責讓我們能更好的控制類的粒度。
開閉原則
定義:一個軟體實體應當對擴充套件開放,對修改關閉。
我們的軟體隨著時間推移是會發生一些變化的,但是已有的程式碼已經是穩定執行的,我們不應該去修改這些成熟的程式碼擴充套件他們的功能,除非逼不得已。所以這就考驗到我們的設計水平了。
我們還是看看下面這個場景:
- 攝影師工作時不只是只用一個牌子的相機,不同的廠商的相機,有不同的效果
我們來看看我們通常專注於實現的程式碼是什麼樣子的:
<?php
declare(strict_types=1);
namespace Neilyoz\DesignPatternBase\OpenAndClose;
class Photographer
{
private string $name;
public function __construct(string $name)
{
$this->name = $name;
}
public function photograph(string $camera)
{
switch ($camera) {
case '佳能':
echo $this->name . ' 使用佳能拍' . PHP_EOL;
break;
case '尼康':
echo $this->name . ' 使用尼康拍' . PHP_EOL;
break;
case '索尼':
echo $this->name . ' 使用索尼拍' . PHP_EOL;
break;
default:
echo $this->name . ' 使用手機拍' . PHP_EOL;
break;
}
}
}
這裡程式碼看著是實現了我們的需求,但是如果客戶要求用 哈蘇
、賓得
、富士
拍照呢?你是不是要去修改 photograph
這個方法。這樣就違背了我們的開閉原則。那我們要怎麼才能不修改程式碼的情況下,去完成我們的進擊的需求呢?我們可以這樣改:
<?php
declare(strict_types=1);
namespace Neilyoz\DesignPatternBase\OpenAndClose;
interface Camera
{
function photograph(): string;
}
class CannonCamera implements Camera
{
public function photograph(): string
{
return ' 使用佳能拍' . PHP_EOL;
}
}
class NikonCamera implements Camera
{
public function photograph(): string
{
return ' 使用尼康拍' . PHP_EOL;
}
}
class SonyCamera implements Camera
{
public function photograph(): string
{
return ' 使用索尼拍' . PHP_EOL;
}
}
class FujiCamera implements Camera
{
public function photograph(): string
{
return ' 使用富士拍' . PHP_EOL;
}
}
class Photographer
{
private string $name;
public function __construct(string $name)
{
$this->name = $name;
}
public function photograph(Camera $camera): void
{
echo $this->name . $camera->photograph();
}
}
$photographer = new Photographer("魚不浪");
$photographer->photograph(new CannonCamera());
$photographer->photograph(new NikonCamera());
$photographer->photograph(new SonyCamera());
$photographer->photograph(new FujiCamera());
我們可以使用介面把相機的拍照功能抽象出來,這樣即使是有新的相機進來,我們無非就是實現這個介面就能達到擴充套件的目的,而不需要去修改我們的現有程式碼。
裡式替換原則
定義:所有引用基類的地方必須透明地使用其子類的物件。
使用裡式替換原則時需要注意如下:
- 子類的所有方法必須在父類中宣告,或子類必須實現父類中宣告的所有方法。根據里氏代換原則,為了保證系統的擴充套件性,在程式中通常使用父類來進行定義,如果一個方法只存在子類中,在父類中不提供相應的宣告,則無法在以父類定義的物件中使用該方法。
- 我們在運用里氏代換原則時,儘量把父類設計為抽象類或者介面,讓子類繼承父類或實現父介面,並實現在父類中宣告的方法,執行時,子類例項替換父類例項,我們可以很方便地擴充套件系統的功能,同時無須修改原有子類的程式碼,增加新的功能可以透過增加一個新的子類來實現。里氏代換原則是開閉原則的具體實現手段之一。
其實還是一個抽象的概念,我們還是拿攝影師來說:
- 攝影師拿相機,至於什麼牌子的相機我們不管,我們只是抽象相機這個概念
<?php
declare(strict_types=1);
namespace Neilyoz\DesignPatternBase\Liskv;
abstract class Camera
{
public function open()
{
echo "相機開機" . PHP_EOL;
}
public abstract function screen(): void;
}
class CannonCamera extends Camera
{
public function screen(): void
{
echo "佳能拍照" . PHP_EOL;
}
}
class NikonCamera extends Camera
{
public function screen(): void
{
echo "尼康拍照" . PHP_EOL;
}
}
class Photographer
{
/**
* 這裡引數是父類,我們可以傳入子類
* @param Camera $camera
*/
public function screen(Camera $camera)
{
$camera->open();
$camera->screen();
}
}
$photographer = new Photographer();
$photographer->screen(new CannonCamera());
$photographer->screen(new NikonCamera());
依賴倒轉原則
定義:抽象不應該依賴於細節,細節應當依賴於抽象。換言之,要針對介面程式設計,而不是針對實現程式設計。
這個我們在開閉原則中已經給出了例項,我們就是針對上層抽象進行的程式設計。
我們來看看常用的三種注入方式:
- 構造注入
- 設值注入
- 介面傳遞注入
我們挨個看,為了省事兒我就只用一個類來演示:
<?php
declare(strict_types=1);
namespace Neilyoz\DesignPatternBase\Dependency;
interface Camera
{
public function open(): void;
public function screen(): void;
}
// 實現相機介面
class CannonCamera implements Camera
{
public function open(): void
{
echo "開啟佳能相機" . PHP_EOL;
}
public function screen(): void
{
echo "佳能相機拍照" . PHP_EOL;
}
}
// 實現相機介面
class NikonCamera implements Camera
{
public function open(): void
{
echo "開啟尼康相機" . PHP_EOL;
}
public function screen(): void
{
echo "尼康相機拍照" . PHP_EOL;
}
}
class Photographer
{
private Camera $camera;
// 使用構造注入
public function __construct(Camera $camera)
{
$this->camera = $camera;
}
/**
* 使用設值注入
* @param Camera $camera
*/
public function setCamera(Camera $camera): void
{
$this->camera = $camera;
}
/**
* 使用介面傳遞注入
* @param Camera $camera
*/
public function open(Camera $camera): void
{
$camera->open();
}
public function screen()
{
$this->open($this->camera);
$this->camera->screen();
}
}
// 我們攝影師本來有自己佳能相機
$photographer = new Photographer(new CannonCamera());
// 並用它進行拍照
$photographer->screen();
// 朋友帶著尼康相機來了,他拿朋友的尼康相機來玩兒
$photographer->setCamera(new NikonCamera());
$photographer->screen();
介面隔離原則
定義:
- 客戶端不應該依賴它不需要的介面。
- 類間的依賴關係應該建立在最小的介面上。
我們透過上面的例子,發現介面真的是一個好東西,可以讓我們解耦很多我們的程式。
還是攝影師的例子,不同攝影師有不同的行為,我們不排除有的攝影師啥都 OK,有的攝影師也缺乏一些能力。
- 攝影師可以拍攝表達作品想法
- 攝影師也要和客戶溝通,但有的攝影師只是工具人,不需要溝通
- 有的攝影師自己重洗膠片,有的直接用數碼
對於這些介面我們的攝影師不用都實現,實現自己需要的介面就好了。
<?php
declare(strict_types=1);
namespace Neilyoz\DesignPatternBase\InterfaceSegregation;
interface Screen
{
public function screen();
}
interface Communicate
{
public function communicate();
}
interface RinseTheFilm
{
public function rinseTheFilm();
}
class PhotographerOne implements Screen, Communicate
{
public function screen()
{
echo "PhotographerOne 拍照" . PHP_EOL;
}
public function communicate()
{
echo "PhotographerOne 溝通" . PHP_EOL;
}
}
class PhotographerTwo implements Screen, Communicate, RinseTheFilm
{
public function screen()
{
echo "PhotographerTwo 拍照" . PHP_EOL;
}
public function communicate()
{
echo "PhotographerTwo 溝通" . PHP_EOL;
}
public function rinseTheFilm()
{
echo "PhotographerTwo 沖洗照片了" . PHP_EOL;
}
}
合成複用原則
定義:儘量使用合成、聚合的方式,而不是使用繼承
這裡有很多種情況,還是用程式碼來舉例說明,我們在有時候在開發中,有時候類可能會用到別的類的方法,我們可以有繼承,依賴,聚合,組合的關係。繼承會導致我們的程式耦合度過高,所以我們會選擇另外的三種方式:
- 依賴
<?php
declare(strict_types=1);
namespace Neilyoz\DesignPatternBase\Composite;
class CompositeParent
{
public function method1()
{
echo "方法1" . PHP_EOL;
}
public function method2()
{
echo "方法2" . PHP_EOL;
}
public function method3()
{
echo "方法3" . PHP_EOL;
}
}
class CompositeChild
{
public function method1(CompositeParent $compositeParent)
{
$compositeParent->method1();
}
}
- 聚合
<?php
declare(strict_types=1);
namespace Neilyoz\DesignPatternBase\Composite;
class CompositeParent
{
public function method1()
{
echo "方法1" . PHP_EOL;
}
public function method2()
{
echo "方法2" . PHP_EOL;
}
public function method3()
{
echo "方法3" . PHP_EOL;
}
}
class CompositeChild
{
private CompositeParent $compositeParent;
public function setCompositeParent(CompositeParent $compositeParent): void
{
$this->compositeParent = $compositeParent;
}
}
- 組合
<?php
declare(strict_types=1);
namespace Neilyoz\DesignPatternBase\Composite;
class CompositeParent
{
public function method1()
{
echo "方法1" . PHP_EOL;
}
public function method2()
{
echo "方法2" . PHP_EOL;
}
public function method3()
{
echo "方法3" . PHP_EOL;
}
}
class CompositeChild
{
private CompositeParent $compositeParent;
public function __construct()
{
$this->compositeParent = new CompositeParent();
}
}
迪米特法則
定義:
- 一個物件應該對其他物件保持最少的瞭解
- 類與類關係越密切,耦合度越大
- 一個類對自己依賴的類知道的越少越好,對於被依賴的類不管多複雜,都儘量將邏輯封裝在內部。對外部除了提供 public 方法,不要透露任何資訊
- 只與直接的朋友通訊
- 直接朋友:每個物件都會與其他物件有耦合關係,只要兩個物件之間有耦合關係,我們就說這兩個物件之間是朋友關係。耦合的方式很多,依賴、關聯、聚合等。其中我們稱出現成員變數,方法引數,方法返回值中的類為直接朋友,而區域性變數中的類不是直接的朋友。也就是說,陌生的類最好不要以區域性變數的形式出現在類的內部。
<?php
declare(strict_types=1);
namespace Neilyoz\DesignPatternBase\LawOfDemeter;
class Student
{
}
class Police
{
}
class LawOfDemeter
{
// 直接朋友
private Student $student;
public function getStudent(): Student
{
return $this->student;
}
public function setStudent(Student $student): void
{
$this->student = $student;
}
public function doSomething()
{
// 非直接朋友
$police = new Police();
}
}
總結
根據這些基本的原則去理解設計模式,感覺不會太困難,當然會 UML 類圖就最好了。
本作品採用《CC 協議》,轉載必須註明作者和本文連結