設計模式必備知識點---六大設計原則

loserbai 發表於 2021-10-19
設計模式

設計模式,即Design Patterns,是指在軟體設計中,被反覆使用的一種程式碼設計經驗。 使用設計模式的目的是為了可重用程式碼,提高程式碼的可擴充套件性和可維護性。 設計模式這個術語是上個世紀90年代由Erich Gamma、Richard Helm、Raplh Johnson和Jonhn Vlissides四個人總結提煉出來的,並且寫了一本 Design Patterns 的書。
專案地址: https://gitee.com/baichen9187...

一,開閉原則

開閉原則的定義

開閉原則是最基礎的設計原則,它指導我們如何建立一個穩定,靈活的系統。開閉原則定義如下:

Software entities like classes,modules and functions should be open
for extension but closed for modifications.

OOP理論中5個原則之一(分別是S O L I D,單一職責原則、開閉原則、里氏替換、介面隔離、依賴反轉)

一個軟體實體如類,模組和函式應該對擴充套件開放,對修改關閉。

什麼是開閉原則

開閉原則明確的告訴我們:實體實現應該對擴充套件開放,對修改關閉,其含義是說一個實體應該通過擴充套件來實現變化,而不是通過修改已有的程式碼來實現變化的。那什麼是實體呢?軟體實體包括以下幾個部分:

專案或軟體產品中按照一定的邏輯規則劃分的模組
抽象和類
方法

開閉原則的作用

開閉原則是物件導向程式設計的終極目標,它使軟體實體擁有一定的適應性和靈活性的同時具備穩定性和延續性。具體來說,其作用如下。

  1. 對軟體測試的影響軟體遵守開閉原則的話,軟體測試時只需要對擴充套件的程式碼進行測試就可以了,因為原有的測試程式碼仍然能夠正常執行。
  2. 可以提高程式碼的可複用性粒度越小,被複用的可能性就越大;在物件導向的程式設計中,根據原子和抽象程式設計可以提高程式碼的可複用性。
  3. 可以提高軟體的可維護性遵守開閉原則的軟體,其穩定性高和延續性強,從而易於擴充套件和維護。

開閉原則的優點

  • 提高程式碼複用性
  • 提高可維護性
  • 提高靈活性
  • 易於測試
#include <iostream>

class Number {
protected:
    double pi = 3.1415926;
public:
    virtual double getCircularArea(int d) = 0;
    virtual double getRectangularArea(int a, int b) = 0;

};

class NumberArea : public Number {
public:
    double getCircularArea(int r) {
        return ((double)r * r) * this->pi;
    }
    double getRectangularArea(int a, int b) {
        return (double)a * b;
    }
};


int main()
{   
    NumberArea num;
    double cricular = num.getCircularArea(1);
    std::cout << cricular << std::endl;
   
}

二,單一職責原則

單一職責定義

單一職責原則(SRP:Single responsibility principle)又稱單一功能原則,物件導向五個基本原則(SOLID)之一。它規定一個類應該只有一個發生變化的原因。該原則由羅伯特·C·馬丁(Robert C. Martin)於《敏捷軟體開發:原則、模式與實踐》一書中給出的。馬丁表示此原則是基於湯姆·狄馬克(Tom DeMarco)和Meilir Page-Jones的著作中的內聚性原則發展出的。

所謂職責是指類變化的原因。如果一個類有多於一個的動機被改變,那麼這個類就具有多於一個的職責。而單一職責原則就是指一個類或者模組應該有且只有一個改變的原因。

單一職責的作用

就一個類而言,應該僅有一個引起它變化的原因。應該只有一個職責。
每一個職責都是變化的一個軸線,如果一個類有一個以上的職責,這些職責就耦合在了一起。這會導致脆弱的設計。當一個職責發生變化時,可能會影響其它的職責。另外,多個職責耦合在一起,會影響複用性。例如:要實現邏輯和介面的分離。

單一職責的優點

單一職責原則的優點有以下幾個方面:

  • 降低類的複雜性;
  • 提高類的可讀性;
  • 提高程式碼的可維護性和複用性;
  • 降低因變更引起的風險。
#pragma once
#include<string>

using namespace std;
class ProductFactory {
public:
    virtual string product(string material) = 0;
};

class ConsumeFactory {

public:
    virtual void consume(string product) = 0;
};
#include<iostream>
#include"./factroy.h"

using namespace std;
class ProductFactoryimpl :ProductFactory{
public:
    string product(string material) {
        return material  ;
    }
};
class ConsumeFactoryimpl : ConsumeFactory {
public :
    void consume(string product) {
        cout << "消費了" << product << endl;
    }
};


int main() {

    ProductFactoryimpl p;
    string data = p.product("goods");
    ConsumeFactoryimpl c;
    c.consume(data);


    return 0;
}

單一職責的違背原則

一個介面實現多種行為方法

#pragma once
#include<string>

using namespace std;
class ConsumeFactory {
public:
    virtual string product(string material) = 0;
     virtual void consume(string product) = 0;
};

class ConsumeFactory :ProductFactory{
public:
    string product(string material) {
        return material  ;
    }
     void consume(string product) {
        cout << "消費了" << product << endl;
    }
};


int main() {

    ConsumeFactoryimpl c;
    string data = c.product("goods");
    
    c.consume(data);


    return 0;
}

三,依賴倒置原則

依賴倒置的定義

在依賴倒置原則中的倒置指的是和一般OO設計的思考方式完全相反。
程式導向的開發,上層呼叫下層,上層依賴於下層,當下層劇烈變動時上層也要跟著變動,這就會導致模組的複用性降低而且大大提高了開發的成本。
依賴倒置原則的核心思想是面向介面程式設計,依賴倒置原則是JavaBean、EJB和COM等元件設計模型背後的基本原則

高層模組不應該依賴低層模組,應該依賴抽象。
抽象不應該依賴細節;細節應該依賴抽象。
即使實現細節不斷變動,只要抽象不變,客戶程式就不需要變化。這大大降低了客戶程式與實現細節的耦合度。

image.png

依賴倒置原則的作用

依賴倒置原則可以有效減少類之間的耦合性,提高系統穩定性沒降低並行開發中的風險,提高程式碼可維護性和可讀性。
依賴倒置原則的本質就是通過抽象(介面)使得各個類或模組之間相互獨立,互不影響,實習模組之間的鬆耦合
image.png

模組間的依賴通過抽象發生,實現類之間不發生直接的依賴關係,其依賴關係是通過介面或抽象類產生;

  1. 介面或抽象類不依賴於實現類;
  2. 實現類依賴於介面或抽象類。
  3. 儘量使用多型
  4. 任何類不應該從具體類派生
  5. 儘量不重寫基類方法

    案例分析

    其中的生物包含依賴了動植物
    場景問題: 類A直接依賴類B,假如要將類A改為依賴類C,則必須通過修改類A的程式碼來達成。這種場景下,類A一般是高層模組,負責複雜的業務邏輯;類B和類C是低層模組,負責基本的原子操作;假如修改類A,會給程式帶來不必要的風險。
    image.png

#pragma once
#include<string>
class AnimalInterface
{
public:
    virtual std::string get() = 0; //食物
};
#include".\Factroy.h"
#include<string>
using std::string;

class AnimalImpl_1 : public AnimalInterface {

public:

    string get() {
        return "草";
    }
};

class AnimalImpl_2 :public AnimalInterface {

public:

    string get() {
        return "水";
    }
};
#include<iostream>
#include<string>

#include"abstraction.cpp"

using namespace std;

class pasture1 {

public:

    void get(AnimalImpl_1 a) {
        vector<string> list{ "牛" ,"羊" ,"豬" ,"馬" };
        for (auto i : list) {
            cout  << i << "被餵食了 " << a.get() << endl;
        }        
    }
};

class pasture2 {
public:
    void get(AnimalImpl_2 a) {
        vector<string> list{ "牛" ,"羊" ,"豬" ,"馬" };
        for (auto i : list) {
            cout << i << "被餵食了 " << a.get() << endl;
        }
    
    }
};


int main() {


    pasture1 p;
    p.get(AnimalImpl_1());
    pasture2 p2;
    p2.get(AnimalImpl_2());
    return 0;
}

如果我還想加一些動物,或者做一些篩選,是不是需要修改程式碼

我們按照原則修改程式碼,原來的抽象層保持不變,修改高層程式碼

#pragma once
#include<string>
class AnimalInterface
{
public:
    virtual std::string get() = 0; //食物
};



#include".\Factroy.h"
#include<string>
using std::string;

class AnimalImpl_1 : public AnimalInterface {

public:

    string get() {
        return "草";
    }
};

class AnimalImpl_2 :public AnimalInterface {

public:

    string get() {
        return "水";
    }
};


class pasture
{
public:
    void get(AnimalInterface *a){
        vector<string> list{ "牛" ,"羊" ,"豬" ,"馬" };
        for (auto i : list) {
            cout << i << "被餵食了 " << a->get() << endl;
        }
            
    }

};


int main() {


    pasture p;
    AnimalInterface* ai = new AnimalImpl_1();
    AnimalInterface* ai2 = new AnimalImpl_2();
    p.get(ai);
    p.get(ai2);
    return 0;
}

四,里氏替換原則

里氏替換原則的重點在不影響原功能,而不是不覆蓋原方法

里氏替換原則譯自Liskov substitution principle。Liskov是一位電腦科學家,也就是Barbara Liskov,麻省理工學院教授,也是美國第一個電腦科學女博士,師從圖靈獎得主John McCarthy教授,人工智慧概念的提出者。

里氏替換原則的定義

里氏替換原則在1994年Barbara Liskov 和 Jeannette Wing發表論文中的描述是:

If S is a declared subtype of T, objects of type S should behave as objects of type T are expected to behave, if they are treated as objects of type T

如果S是T的子型別,對於S型別的任意物件,如果將他們看作是T型別的物件,則物件的行為也理應與期望的行為一致。

問題由來:有一功能P1,由類A完成。現需要將功能P1進行擴充套件,擴充套件後的功能為P,其中P由原有功能P1與新功能P2組成。新功能P由類A的子類B來完成,則子類B在完成新功能P2的同時,有可能會導致原有功能P1發生故障。
簡單來說就是類B繼承A時,除了新的方法外,儘量不對父類的方法進行重寫,否則會對原有功能發生變化

里氏替換原則的強制性

  • 子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法。
    子類中可以增加自己特有的方法。
  • 當子類的方法過載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入引數更寬鬆。
  • 當子類的方法實現父類的抽象方法時,方法的後置條件(即方法的返回值)要比父類更嚴格。

解決方法有多型:新增特有方法

里氏替換原則的優缺點

優點:

  • 程式碼貢獻,減小建立工作
  • 提高程式碼複用性
  • 提高程式碼可擴充套件性
  • 提高專案開放性

相應的缺點也有:

  • 繼承是入侵式的。
  • 降低程式碼的靈活性。
  • 增強了耦合性。

    里氏替換原則的違背原則

class Math {
public:
    virtual int sum(int a, int b) = 0;

    virtual float halve(int a) = 0;
};
#include"Factroy.h"
#include<iostream>
using namespace std;
class math :Math {
public:
    int sum(int a, int b) {
        return a + b;
    }
    float halve(int num) {
        return num >> 1;
    }
};

class math2 :math {
private:
    int add  = 100;

public:
    int sum(int a, int b) {
        return a + b + add;
    }
    float halve(int num) {
        return num >> 1;
    }
};

int main() {
    math m;
    int sum = m.sum(2, 2);
    float halve = m.halve(sum);
    cout << "sum(2,2)" << sum << endl;
    cout << "halve(4)" << halve << endl;

    math2 m2;
    int sum2 = m2.sum(2, 2);
    float halve2 = m2.halve(sum2);
    cout << "sum(2,2)" << sum2 << endl;
    cout << "halve(4)" << halve2 << endl;


    return 0;
 }

案例分析

class Math {

public:
    virtual int sum(int a, int b) = 0;

    virtual float halve(int a) = 0;
};
#include"Factroy.h"
#include<iostream>
using namespace std;
class math :Math {
public:
    int sum(int a, int b) {
        return a + b;
    }
    float halve(int num) {
        return num >> 1;
    }
};

class math3 : public math {
private:
    int add = 100;
public:
    int sum(int a, int b) {
        return a + b + add;
    }
    float halve(int num) {
        return num >> 1;
    }
};

int main() {
    math* math;
    math3 m3;
    math = &m3;
    int sum3 = math->sum(2, 2);
    float halve3 = math->halve(sum2);
    cout << "sum(2,2)" << sum3 << endl;
    cout << "halve(4)" << halve3 << endl;
    
    return 0;
}

五,介面隔離原則

介面隔離原則的定義

介面隔離原則是對介面的使用進行約束規範的一個原則,具體含義如下

  1. 客戶端不應該依賴它不需要的介面,一個介面對應一個角色,不應該將不同的角色分配給同一個介面,會形成一個龐大的介面,這也導致了介面汙染
  2. 不應該強迫介面依賴與他們不需要的介面
  3. 類間的依賴關係應該建立在最小的介面上

介面隔離原則的優點:

  1. 避免介面汙染,防止無法預料的風險
  2. 提供類內部的邏輯的靈活性,維護性
  3. 提供了系統的內聚性,降低耦合度
  4. 減少程式碼冗餘

程式碼實現

image.png

#pragma once
#include<string>
#include<vector>
using std::string;
using std::vector;

/*封裝了vector*/
template<class T> class Container :public  vector<T> {
};

template<class T> class Crud {
public:
    virtual void insert(T t) = 0;
    virtual T get( int index) = 0;
    virtual void delete_(int index) = 0;
    virtual int size() = 0;
};

//應用
template<class T, class V> class Apply {
public:
    virtual V apply(Crud<T>* c) = 0;
};
#include <iostream>
#include"Interface.h"
using std::cout;
using std::endl;

class Applylmp : public Apply<string,string>, public Crud<string>, public Container<string> {
private:
    Container<string> list;

public:
    void insert(string s) {
        list.insert(list.end(),s);
        cout << "插入了" << s << endl;
    }
    string get(int index) {
        return list.operator[](index);
    }
    void delete_(int index) {
        list.operator[](index) = "";
    }
    int size() {
        return list.size();
    }

    string apply() {
        string s;
        for (int i = 0; i < this->size(); i++) {
            s.append(this->get(i));
        }
        return s;
    }
};

int main()
{
    Applylmp apply;
    string  s[] = { "a","b","c" };
    for (int i = 0; i < (sizeof(s)/ sizeof(string)); i++) {
        apply.insert(s[i]);
    }
    string data =  apply.apply();
    cout << data;
    return 0;
  
}

六,迪米特原則(最小知識原則)

迪米特原則的定義

迪米特法則(Law of Demeter,簡稱LOD),又稱為“最少知識原則”。

迪米特法則最初是用來作為物件導向的系統設計風格的一種法則,在1987年由Ian
Holland在美國東北大學為一個叫迪米特的專案設計提出的,因此叫做迪米特法則。這條法則實際上是很多著名系統,例如火星登陸軟體系統、木星的歐羅巴衛星軌道飛船的軟體系統的指導設計原則。

它的定義為:一個軟體實體應當儘可能少的與其他實體發生相互作用。這樣,當一個模組修改時,就會盡量少的影響其他的模組,擴充套件會相對容易。迪米特法則是對軟體實體之間通訊的限制,它對軟體實體之間通訊的寬度和深度做出了要求。迪米特的其它表述方式為:
只與你直接的朋友們通訊。
不要跟“陌生人”說話。
每一個軟體單位對其他的單位都只有最少的知識,而且侷限於那些與本單位密切相關的軟體單位。
“朋友”的條件為:
當前物件本身(this);
被當做當前物件的方法的引數傳入進來的物件;
當前物件的方法所建立或者例項化的任何物件;
當前物件的任何元件(被當前物件的例項變數引用的任何物件)

迪米特原則的優點

  • 減小了類之間的關係
  • 提高了程式碼的弱耦合
  • 提高了程式碼的複用性

程式碼實現

在這裡插入圖片描述

#include <iostream>
class Stranger {

public:
    void call() {
        std::cout << "我是陌生人" << std::endl;
    }
};


class Friend {
private:
    Stranger* stranger;
public:
    void forward() {
        std::cout << "我給你介紹一下" << std::endl;
        stranger->call();
    }
    void call() {
        std::cout << "我是朋友" << std::endl;
    }

};


class Someone {

public:
    void call(Friend* f) {
        std::cout << "你好!" << std::endl;
        f->forward();
        
    }

};

int main()
{
    Someone someone;
    someone.call(new Friend());
}