設計模式七大原則(C++描述)


前言

最近在學習一些基本的設計模式,發現很多博客都是寫了六個原則,但我認為有7個原則,並且我認為在編碼中思想還是挺重要,所以寫下一篇博客來總結下
之后有機會會寫下一些設計模式的博客(咕咕咕........

設計模式的七大原則

1.單一職責原則
2.開放-封閉原則
3.依賴倒置原則
4.里氏替換原則(LSP)
5.接口隔離原則
6.迪米特原則(最少知道原則)
7.合成復用原則

1.單一職責原則

准確解析:就一個類而言,應該僅有一個引起它變化的原因

當一個類職責變化時不會導致另一個類職責的變化.

優點:可以降低類的復雜度,提高可讀性

2.開放-封閉原則

准確解析:軟件實體(類,模板,函數等等)應該可以擴展,但不可修改

開閉原則是面對對象設計的核心所在;開放人員應該僅對程序中呈現出頻繁變化
的那些部分做出抽象.

3.依賴倒置原則

准確解析:A.高層模板(穩定)不應該依賴底層模板(變化).兩個都應該依賴抽象(穩定)
B.抽象(穩定)不應該依賴實現細節(變化).細節(變化)應該依賴抽象(穩定).

不論變化還是穩定都應該依賴於穩定

說白了:要面對接口編程,不要對實現編程.

#include<iostream>
class Book
{
    public:void look()
    {
        ....
    }
    .....
}
class Man
{
   puclic:void Action(Book book)
   {
      book.look();
   }
   ....
}

int main()
{
    Man man=new Man();
    Book book=new book();
    Man->Action(book);
    ....
}

上面顯示的是人看書的行為

那么假設現有我想要人進行看視頻行為,視頻類的代碼如下:

class Video
{
    public:void Video()
    {
        ....
    }
    .....
}

那么我不僅要對人這個類中修改,還有對主函數的代碼進行修改;如果有大量的需要的話,這個修改過程將會變得非常痛苦,因為書和人的耦合度太高.

接下來使用依賴倒置原則來會解決當前的痛苦,能夠降低書和人的耦合度

書和視頻我們當作一個可以看的東西ILOOK作為接口類,然后書和視頻繼承這個類

class ILOOK
{
    public:virtual void look()=0; 
}

class Bookpublic ILOOk
{
    public:void look()
    {
        ....
    }
    .....
}

class Video:public ILOOk
{
    public:void look()
    {
        ....
    }
    .....
}
class Man
{
   puclic:void Action(ILOOK ilook)
   {
      ilook.look();
   }
   ....
}

int main()
{
    Man man=new Man();
    ILOOK ilook=new book();
    Man->Action(ilook);
    ILOOK ilook2=new video();
    Man->Action(ilook2);
    ....
}

這樣就實現了簡單的依賴倒置,人依賴於ILOOK這個類,並且書和視頻也都依賴於ILook(即高層和底層都應該依賴抽象

這便是一個簡單的面對接口編程.

這個依賴倒置原則將會貫串於所有設計模式,所以對於這個原則一定要有清晰的認識

4.里氏替換原則(LSP)

准確解析:子類型必須能夠替換掉它們的父類型
說白了就是一種IS-A的另一種表達

比如說:鳥是一個父類,有 fly()這個虛函數,燕子是一個鳥,因為它能夠飛,所以它可以繼承鳥類;

企鵝不能飛,所以它不能繼承鳥類,即使他在生物學上是鳥類,但它在編程世界中不能夠繼承鳥類

這里說出LSP的一個特點:只有當子類可以替換掉父類,軟件單位的功能不受影響時,父類才能夠被復用,而子類也能夠在父類的基礎上增加新的行為

通俗來說:子類可以擴展父類的功能,但不能改變父類原來的功能。

包括4層含義:1.子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法。
2.子類中可以增加自己特有的方法。
3.當子類的方法重載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬松。
4.當子類的方法實現父類的抽象方法時,方法的后置條件(即方法的返回值)要比父類更嚴格。

4種含義不展開講,但用下面的一個例子來簡單說明

#include<iostream>

class A
{
public:
	int fun1(int a, int b) {
		return a - b;
	}
};

class B :public A
{
public:
	int fun1(int a, int b) {
		return a + b;
	}
	int fun2(int a, int b)
	{
	     return fun1(a, b)-100;   //想要a-b-100,但現實是a+b-100
	}
};
int main()
{
	int a = 100, b = 20;
	B* m_b=new B();
	std::cout << m_b->fun2(a, b) << std::endl;
}

上面顯示的結果會是20,因為B類中的fun1()覆蓋到了A類中的fun1();所以fun2()中調用的是B類的fun1(),這便違反了里氏替換原則

不遵循里氏替換原則的后果是:出問題的概率會大大提高

5.接口隔離原則

准確解釋:不應該強迫客戶程序依賴他們不用的方法;接口應該小而完備

class I
{
    public:
    void method1()=0;
    void method2()=0;
    void method3()=0;
    void method4()=0;
    void method5()=0;
}

class A
{
    public:
    void depend1(I i)
    {
        i.method1();
    }
     void depend2(I i)
    {
        i.method2();
    }
     void depend3(I i)
    {
        i.method3();
    }
}
class B:public I
{
    public:
    void method1()
    {
        std::cout<<"B實現方法1"<<std::endl;
    }
    void method2()
    {
        std::cout<<"B實現方法2"<<std::endl;
    }
    void method3()
    {
        std::cout<<"B實現方法3"<<std::endl;
    }
    //B類種方法4和5不是必須的
    //但方法4和5因為繼承的原因仍讓需要空實現
    void method4(){}
    void method5(){}
}

class C
{
    public:
    void depend1(I i)
    {
        i.method1();
    }
     void depend2(I i)
    {
        i.method4();
    }
     void depend3(I i)
    {
        i.method5();
    }
}
class D:public I
{
    public:
    void method1()
    {
        std::cout<<"B實現方法1"<<std::endl;
    }
    void method4()
    {
        std::cout<<"B實現方法4"<<std::endl;
    }
    void method5()
    {
        std::cout<<"B實現方法4"<<std::endl;
    }
    //B類種方法2和3不是必須的
    //但方法2和3因為繼承的原因仍讓需要空實現
    void method2(){}
    void method3(){}
}
上面便沒有使用接口隔離原則
下面便使用了接口隔離,所以一些無關的方法就可以不用去實現

class I1
{
    public:
    void method1()=0;
}
class I2
{
    public:
    void method2()=0;
    void method3()=0;
}
class I3
{
    public:
    void method4()=0;
    void method5()=0;
}
class A
{
    public:
    void depend1(I1 i)
    {
        i1.method1();
    }
     void depend2(I2 i)
    {
        i2.method2();
    }
     void depend3(I2 i)
    {
        i2.method3();
    }
}
class B:public I1,public I2
{
    public:
    void method1()
    {
        std::cout<<"B實現I1方法1"<<std::endl;
    }
    void method2()
    {
        std::cout<<"B實現I2方法2"<<std::endl;
    }
    void method3()
    {
        std::cout<<"B實現I2方法3"<<std::endl;
    }
}

class C
{
    public:
    void depend1(I1 i)
    {
        i1.method1();
    }
     void depend2(I2 i)
    {
        i3.method4();
    }
     void depend3(I2 i)
    {
        i3.method5();
    }
}
class D:public I1,public I3
{
    public:
    void method1()
    {
        std::cout<<"B實現I1方法1"<<std::endl;
    }
    void method4()
    {
        std::cout<<"B實現I3方法4"<<std::endl;
    }
    void method5()
    {
        std::cout<<"B實現I3方法4"<<std::endl;
    }
    
}
使用接口隔離原則時應注意:
1.接口盡量小,但是要有限度。如果過小,則會造成接口數量過多,使設計復雜化。所以一定要適度。
2.為依賴接口的類定制服務,只暴露給調用的類它需要的方法,它不需要的方法則隱藏起來。
3.提高內聚,減少對外交互。使接口用最少的方法去完成最多的事情。
這個原則可以在實踐多花時間思考,才可以准確地使用它

6.迪米特原則(最少知道原則)

准確解釋:一個對象應該對其他對象保持最少的了解

因為類之間的關系最緊密,耦合度越高,一個類變化時對另一個類的影響也大

我們使用迪米特原則就是要降低類之間的耦合度

C++中一個重要的特性:高內聚,低耦合.高內聚,低耦合.高內聚,低耦合.(重要的事情說三遍)
#include<iostream>
#include<list>
#include<string>

class Employee
{
private:
	std::string m_id;
public:
	Employee(){}
	Employee(std::string id) :m_id(id) {}
	std::string get_id()
	{
		return m_id;
	}
};
class SubEmployee
{
private:
	std::string m_id;
public:
	SubEmployee() {

	}
	SubEmployee(std::string id) :m_id(id) {}
	std::string get_id()
	{
		return m_id;
	}
};
class SubCompanyManager
{
public:
	std::list<SubEmployee> getAllEmployee()
	{
		std::list<SubEmployee> list(100);
		for (int i = 0; i < 100; i++)
		{
			SubEmployee emp("分公司" + std::to_string(i));
			list.push_back(emp);
		}
		return list;
	}
};
class CompanyManager
{
public:
	std::list<Employee> getAllEmployee()
	{
		std::list<Employee> list(30);
		for (int i = 0; i < 30; i++)
		{
			Employee emp("總公司"+std::to_string(i));
			list.push_back(emp);
		}
		return list;
	}
	void printALLEmployee(SubCompanyManager sub)
	{
		std::list<SubEmployee> list1(100);
		list1 = sub.getAllEmployee();
		std::list<SubEmployee>::iterator itor= list1.begin();
		for (; itor != list1.end(); itor++)
		{
			std::cout << itor->get_id();
		}
		std::list<Employee> list2(30);
		list2= getAllEmployee();
		std::list<Employee>::iterator itor2 = list2.begin();
		for (; itor2 != list2.end(); itor2++)
		{
			std::cout << itor2->get_id();
		}
	}
};

int main()
{
	CompanyManager* e = new CompanyManager();
	SubCompanyManager s;
	e->printALLEmployee(s);
	system("pause");
	return 0;
}
上面的代碼違反了迪米特原則

根據迪米特法則,只與直接的朋友發生通信,而SubEmployee類並不是CompanyManager類的直接朋友(以局部變量出現的耦合不屬於直接朋友),從邏輯上講總公司只與他的分公司耦合就行了,與分公司的員工並沒有任何聯系,這樣設計顯然是增加了不必要的耦合。

class SubCompanyManager
{
public:
	std::list<SubEmployee> getAllEmployee()
	{
		std::list<SubEmployee> list(100);
		for (int i = 0; i < 100; i++)
		{
			SubEmployee emp("分公司" + std::to_string(i));
			list.push_back(emp);
		}
		return list;
	}
	void printALLEmployee()
	{
		std::list<SubEmployee> list = getAllEmployee();
		std::list<SubEmployee>::iterator itor = list.begin();
		for (; itor != list.end(); itor++)
		{
			std::cout << itor->get_id();
		}
	}
};
class CompanyManager
{
public:
	std::list<Employee> getAllEmployee()
	{
		std::list<Employee> list(30);
		for (int i = 0; i < 30; i++)
		{
			Employee emp("總公司" + std::to_string(i));
			list.push_back(emp);
		}
		return list;
	}
	void printALLEmployee(SubCompanyManager sub)
	{
		sub.printALLEmployee();
		std::list<Employee> list2(30);
		list2 = getAllEmployee();
		std::list<Employee>::iterator itor2 = list2.begin();
		for (; itor2 != list2.end(); itor2++)
		{
			std::cout << itor2->get_id();
		}
	}
};

為分公司增加了打印人員ID的方法,總公司直接調用來打印,從而避免了與分公司的員工發生耦合。

另外切記不要過分使用迪米特原則,否則會產生大量的這樣的中介和傳遞類,

7.合成復用原則

准確解析:盡量先使用組合后聚合等關聯關系來實現,其次才考慮使用繼承關系來實現

繼承復用:又稱"白箱""復用,耦合度搞,不利於類的擴展和維護

組合或聚合復用:又稱"黑箱"復用,耦合度低,靈活度高

上面的圖使用繼承復合產生了大量的子類,如何需要增加新的"動力源"或者"顏色"
都要修改源代碼,因為耦合度高,這違背了開閉原則

如果改為組合或聚合復用就可以很好的解決上述問題,如下圖所示

七點原則總結

單一職責原則告訴我們實現類要職責單一;里氏替換原則告訴我們不要破壞繼承體系;依賴倒置原則告訴我們要面向接口編程;接口隔離原則告訴我們在設計接口的時候要精簡單一;迪米特法則告訴我們要降低耦合。而開閉原則是總綱,他告訴我們要對擴展開放,對修改關閉。合成復用原則告訴我們要優先使用組合或者聚合關系復用,少用繼承關系復用。
我們在實踐時應該根據實際情況靈活使運用,才能達到良好的設計

參考博客:
http://www.uml.org.cn/sjms/201211023.asp#2

參考視頻:
https://www.bilibili.com/video/av22292899

參考書籍:<<大話設計模式>>

作者:Ligo丶

出處:https://www.cnblogs.com/Ligo-Z/

本文版權歸作者和博客園共有,歡迎轉載,但必須給出原文鏈接,並保留此段聲明,否則保留追究法律責任的權利。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM