面向對象的SOLID例子學習

這些年,月小升同學發現自己不會讀書於是買了一本《如何閱讀一本書》,發現自己不會做筆記就買了一本《如何做筆記》,寫代碼久了,發現自己一直在用的面向對象不是很瞭解,經常把代碼寫成一坨一坨的,於是回頭來學習怎麼面向對象。那些不熟練的基礎,總要還債的。

出來混總是要還的

SOLID 是Michael Feathers推薦的便於記憶的首字母簡寫,它代表了Robert Martin命名的最重要的五個面對對象編碼設計原則

S: 單一職責原則 (SRP) Single Responsibility Principle
O: 開閉原則 (OCP) Open/Closed Principle
L: 里氏替換原則 (LSP) Liskov Substitution Principle
I: 接口隔離原則 (ISP) Interface Segregation Principle
D: 依賴反轉原則 (DIP) Dependency Inversion Principle

  1. 單一責任原則:

當需要修改某個類的時候原因有且只有一個(THERE SHOULD NEVER BE MORE THAN ONE REASON FOR A CLASS TO CHANGE)。換句話說就是讓一個類只做一種類型責任,當這個類需要承當其他類型的責任的時候,就需要分解這個類。

舉例子:不要讓一個類,負責發郵件,還負責修改客戶名稱
不良代碼:

class Email{
public function sendemail(){
//...
}
public function changeUsername(){
//...
}
}
改進版:

class Email{
public function sendemail(){
//...
}
}
 
class User{
public function changeUsername(){
//...
}
}

  1. 開放封閉原則 Open/Closed Principle (OCP)

軟件實體應該是可擴展,而不可修改的。也就是說,對”擴展是開放的,而對修改是封閉的”。這個原則是在說明應該允許用戶在不改變已有代碼的情況下增加新的功能。

實現開閉原則的關鍵就在於“抽象”。把系統的所有可能的行爲抽象成一個抽象底層,這個抽象底層規定出所有的具體實現必須提供的方法的特徵。作爲系統設計的抽象層,要預見所有可能的擴展,從而使得在任何擴展情況下,系統的抽象底層不需修改;同時,由於可以從抽象底層導出一個或多個新的具體實現,可以改變系統的行爲,因此系統設計對擴展是開放的。

看例子

<?php
 
abstract class vehicle
{
protected $name;
public function construct(){
 
}
 
public function getName()
{
return $this->name;
}
}
 
class car extends vehicle
{
public function
construct()
{
parent::construct();
$this->name = 'car';
}
}
 
class bike extends vehicle
{
public function
construct()
{
parent::construct();
 
$this->name = 'bike';
}
}
 
class task
{
private $vehicle;
 
public function
construct($vehicle)
{
$this->vehicle = $vehicle;
}
 
public function dotask($kilometer)
{
$vName = $this->vehicle->getName();
 
if ($vName === 'car') {
return $this->CarRun($kilometer);
} elseif ($vName === 'bike') {
return $this->BikeRun($kilometer);
}
}
 
private function CarRun($kilometer)
{
echo '小汽車跑了'.$kilometer.'公里<hr>';
}
 
private function BikeRun($kilometer)
{
echo '自行車跑了'.$kilometer.'公里<hr>';
}
}
 
echo '<meta charset="utf-8">';
$car = new car;
$bike = new bike;
$task = new task($car);
$task->dotask(10);
$task = new task($bike);
$task->dotask(10);
?>
這個例子,如果增加一個車,bus,那麼就要改動task任務這個類的底層代碼

改良版本

<?php
 
interface vehicle
{
public function run($url);
}
 
class car implements vehicle
{
public function run($kilometer)
{
echo '小汽車跑了'.$kilometer.'公里<hr>';
}
}
 
class bike implements vehicle
{
public function run($kilometer)
{
echo '自行車跑了'.$kilometer.'公里<hr>';
}
}
 
class task
{
private $vehicle;
 
public function __construct($vehicle)
{
$this->vehicle = $vehicle;
}
 
public function dotask($kilometer)
{
$this->vehicle->run($kilometer);
}
 
}
 
echo '<meta charset="utf-8">';
$car = new car;
$bike = new bike;
$task = new task($car);
$task->dotask(10);
$task = new task($bike);
$task->dotask(10);
?>
改良後的車輛,任意增加新車型,都不會改動task的內容

  1. 里氏替換原則

當一個子類的實例應該能夠替換任何其超類的實例時,它們之間才具有is-A關係
可以理解爲:只要有父類出現的地方,都可以使用子類來替代。而且不會出現任何錯誤或者異常。但是反過來卻不行。子類出現的地方,不能使用父類來替代。

定義1:如果對每一個類型爲 T1的對象 o1,都有類型爲 T2 的對象o2,使得以 T1定義的所有程序 P 在所有的對象 o1 都代換成 o2 時,程序 P 的行爲沒有發生變化,那麼類型 T2 是類型 T1 的子類型。
理解爲“只要有父類出現的地方,都可以使用子類來替代”

定義2:所有引用基類的地方必須能透明地使用其子類的對象。
問題由來:有一功能P1,由類A完成。現需要將功能P1進行擴展,擴展後的功能爲P,其中P由原有功能P1與新功能P2組成。新功能P由類A的子類B來完成,則子類B在完成新功能P2的同時,有可能會導致原有功能P1發生故障。

解決方案:當使用繼承時,遵循里氏替換原則。類B繼承類A時,除添加新的方法完成新增功能P2外,儘量不要重寫父類A的方法,也儘量不要重載父類A的方法。

不良的設計:違背了里氏替換原則
假設一個父親廚師會個炒雞蛋的手藝,辣椒炒雞蛋,兒子應該會這個手藝,但是兒子不會用辣椒,只會用大蔥,所以炒出來的雞蛋不一樣了。

<?php
class father{
public function cookeEgg(){
echo '辣椒炒雞蛋<br/>';
}
}
 
class son1 extends father{
public function cookeEgg(){
echo '大蔥炒雞蛋<br/>';
}
 
}
echo '<meta charset="utf-8">';
$f = new father;
$f->cookeEgg();
$s1 = new son1;
$s1->cookeEgg();
?>
辣椒炒雞蛋
大蔥炒雞蛋
違背了原則1:只要有父類出現的地方,都可以使用子類來替代。
現在父親會辣椒炒雞蛋,換成兒子來炒,結果兒子因爲不敢碰辣椒,炒成了大蔥燒雞蛋。 這個兒子就不是好兒子。我們假設這個是大兒子。

改良版本:出來一個好的二兒子的樣子

<?php
class father{
public function cookeEgg(){
echo '辣椒炒雞蛋<br/>';
}
}
class son2 extends father{
public function cookeEggWithScallion(){
echo '大蔥炒雞蛋<br/>';
}
}
echo '<meta charset="utf-8">';
$f = new father;
$f->cookeEgg();
$s2 = new son2;
$s2->cookeEgg();
$s2->cookeEggWithScallion();
?>
辣椒炒雞蛋
辣椒炒雞蛋
大蔥炒雞蛋
這個兒子,複合了定義1,父親出來的地方,兒子出來就能提到,父親會辣椒炒雞蛋,兒子也會,所以兒子自動繼承,但是二兒子還會大蔥炒雞蛋,二兒子就會兩個炒蛋了

按解決方案:不要重寫父親的方法
規則1: 不要重寫父親的方案
規則2: 所有孩子都會有父親的技能(父親能辣椒炒雞蛋,兒子就會,父親出現的地方,兒子就可以替代)
規則3: 孩子會額外的技能,自己單獨再寫函數。(子類出現的地方,父親不一定能替代)

  1. 依賴反轉原則

  2. 高層模塊不應該依賴於低層模塊,二者都應該依賴於抽象
  3. 抽象不應該依賴於細節,細節應該依賴於抽象

實際理解爲:高級邏輯層代碼,不要因爲底層的模塊代碼改變而變動。

高層模塊:業務邏輯層,比如羣發郵件,我決定發給購買者和沒有購買者,那麼羣發郵件這個send的工作就是業務邏輯層的高層模塊,而決定哪些用戶時購買者的底層數據庫查詢操作屬於底層模塊。

舉例子:老張開寶馬,這個動作,開車是業務邏輯層,寶馬車跑動是底層。
不良設計

<?php
class Bwm{
public function run(){
echo "開動寶馬汽車";
}
}
 
class Audi{
public function run(){
echo "開動奧迪汽車";
}
}
 
//高層模塊Driver 依賴了底層模塊Bwm , 出來模塊C Audi,我就只好改Driver了。
class Driver{
public function drive(Bwm $car){
$car->run();
}
}
 
echo '<meta charset="utf-8">';
$bwm = new Bwm;
$zhang = new Driver();
$zhang->drive($bwm);
 
$audi = new Audi;
$zhang->drive($audi); //奧迪我沒法開了。 此處代碼會報錯。
?>
有個辦法就是業務層,再寫一個函數funciton driveAudi() 是不是很熟悉,我們因爲要負責處理額外的情況,又寫了個看起來很重複函數。

改良的版本,把車做成接口
寶馬和奧迪都是來實現車的底層邏輯函數。這樣再新車進入的時候,就不用改動邏輯層的代碼了。

<?php
interface Car{
public function run();
}
class Bwm implements Car{
public function run(){
echo "開動寶馬汽車";
}
}
class Audi implements Car{
public function run(){
echo "開動奧迪汽車";
}
}
class Driver{
public function drive(Car $car){
$car->run();
}
}
 
echo '<meta charset="utf-8">';
$bwm = new Bwm;
$zhang = new Driver();
$zhang->drive($bwm);
 
$audi = new Audi;
$zhang->drive($audi);
?>
現在老張可以開寶馬也可以開奧迪,你拿個大衆,我也照樣開。

再次理解這句話:高級邏輯層代碼,不要因爲底層的模塊代碼改變而變動。

  1. 接口分離原則

不能強迫用戶去依賴那些他們不使用的接口。換句話說,使用多個專門的接口比使用單一的總接口總要好。

interface Employee
{
public function work();
 
public function eat();
}
 
class Human implements Employee
{
public function work()
{
// ....working
}
 
public function eat()
{
// ...... eating in lunch break
}
}
機器人僱員不能吃,但是被強迫必須實現吃的接口

class Robot implements Employee
{
public function work()
{
//.... working much more
}
 
public function eat()
{
//.... robot can't eat, but it must implement this method
}
}
改良版本

interface Workable
{
public function work();
}
 
interface Feedable
{
public function eat();
}
 
interface Employee extends Feedable, Workable
{
}
 
class Human implements Employee
{
public function work()
{
// ....working
}
 
public function eat()
{
//.... eating in lunch break
}
}
 
// robot can only work
class Robot implements Workable
{
public function work()
{
// ....working
}
}
對面向對象的領悟,有助於在大型代碼量的工程裏,實現有效分離函數,互不干擾,團隊協作。

https://java-er.com/blog/solid-class-study/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章