前言
最近在學習一些基本的設計模式,發現很多博客都是寫了六個原則,但我認為有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/
本文版權歸作者和博客園共有,歡迎轉載,但必須給出原文鏈接,並保留此段聲明,否則保留追究法律責任的權利。