什么是設(shè)計(jì)模式
每一個(gè)描述了一個(gè)在我們周圍不斷重復(fù)發(fā)生的問題,以及該問題的核心解決方案猿妈。這樣别渔,你就能一次又一次地使用該方案而不必做重復(fù)勞動(dòng)。
——Christopher Alexander
對(duì)于我們處理工作中的實(shí)際問題的時(shí)候匾灶,其實(shí)絕大多數(shù)的問題都是呈一定的模式重復(fù)出現(xiàn)的藤违,如果不使用設(shè)計(jì)模式忽你,那么我們?cè)谔幚韱栴}的時(shí)候俯抖,不得不一遍又一遍重復(fù)做著類似的工作,不但浪費(fèi)了大量的精力哩治,同時(shí)也更是一種極度讓人煩躁的事。
那么面向?qū)ο罂梢詭臀覀児?jié)省大量的精力來不必要每次都重頭開始處理那些差不多的問題衬鱼,在真正的開始面對(duì)設(shè)計(jì)模式之前业筏,那么我們得要先從對(duì)象說起。
之前我的文章里面談到了我關(guān)于對(duì)象理解鸟赫,對(duì)象其實(shí)是一種人類解釋現(xiàn)實(shí)世界的通用規(guī)則蒜胖。其實(shí)用好對(duì)象的核心思想就是不斷的學(xué)著像解釋理解這個(gè)世界一樣,來不斷地抽象的看待這個(gè)世界抛蚤,并用代碼來講關(guān)于世界的故事台谢。
在此,我不想過多地談我過去我寫過的東西(如果感興趣可以查看之前的文章)岁经,畢竟這就是一種無趣的重復(fù)勞動(dòng)朋沮,實(shí)在讓人感覺提不起精神。但是缀壤,既然說道了面向?qū)ο蠓兀敲矗@其中不得不提那么幾句塘慕,這里我會(huì)盡量不炒冷飯骑脱。
抽象到底有多厲害?苍糠!
其實(shí)人類在認(rèn)識(shí)世界的過程中,最大的特點(diǎn)也是現(xiàn)在最火熱的機(jī)器學(xué)習(xí)啤誊、人工智能岳瞭、深度學(xué)習(xí)這些目前應(yīng)該是最火熱的前沿技術(shù)希望得到的一種能力,其實(shí)就是抽象的能力蚊锹。
我還是那我這條故事狗最喜愛的舉例子的動(dòng)物瞳筏,貓咪來說,就像上圖的貓咪牡昆,對(duì)于人類的孩子來說姚炕,只需要告訴他其中的某一個(gè)貓咪,這個(gè)是叫做貓咪丢烘,那么當(dāng)他看到其他的八成是能夠認(rèn)識(shí)的(估計(jì)遇到上圖中的第一個(gè)柱宦,孩子認(rèn)不出來也正常)。這些貓?jiān)谌祟惪匆谎劭慈ト际且粯拥牟ネ灰粯拥氖羌?xì)節(jié)問題掸刊,比如這是一只瘦貓,這是一只肥貓赢乓,這是花貓忧侧,這是白貓石窑、黑貓等等等。即使僅僅聽到一聲喵也就知道一定有貓的存在蚓炬。對(duì)于人來說松逊,貓是一個(gè)大類,每只貓都是獨(dú)立的存在肯夏。這時(shí)候看起來经宏,每一只獨(dú)立的貓有些類似于計(jì)算機(jī)的對(duì)象,而貓就類似于一個(gè)類了熄捍。
但是對(duì)于計(jì)算機(jī)來說烛恤,對(duì)象就是內(nèi)存中的一塊空間而已,在這塊內(nèi)存空間里面保存了一些數(shù)據(jù)余耽,通過這些數(shù)據(jù)來解釋了這個(gè)世界缚柏。比如,一只黑色的貓碟贾,眼睛是綠色等等币喧,這些數(shù)據(jù)保存在內(nèi)存中。也就是其實(shí)每個(gè)對(duì)象他是都是具體的袱耽,而不是抽象的杀餐,他只能表現(xiàn)著一只貓,如果另外一個(gè)貓咪的腳是白色的朱巨,那么數(shù)據(jù)不匹配史翘,就應(yīng)該是另外一個(gè)對(duì)象。所以其實(shí)對(duì)象對(duì)于人類世界來說就是不同的個(gè)體冀续,每個(gè)個(gè)體都應(yīng)該是獨(dú)立的對(duì)象琼讽,因此也有學(xué)著曾經(jīng)提出,將object翻譯為對(duì)象其實(shí)并不合理洪唐,而應(yīng)該翻譯為物體就是這個(gè)道理钻蹬。
如果我們觀察每個(gè)對(duì)象到底是如何構(gòu)成,就像我們?cè)谟懻撘粋€(gè)人是什么性格一樣凭需,其實(shí)這些就相當(dāng)于是底層思維模式问欠,而人類更具備的一種向上抽象的能力。比如看到貓的第一反應(yīng)是貓粒蜈,而之后才會(huì)是他的毛色等等具體的特征顺献。
而面向?qū)ο蟮倪^程,其實(shí)就是希望模擬人類這樣一種抽象的思維薪伏,通過抽象思維來解釋這樣的世界滚澜。
而為了更好的能夠用計(jì)算機(jī)解釋好這個(gè)世界,程序員必須要有一套很好的抽象思維。
那么设捐,抽象思維在幫我們更好的解釋這個(gè)世界的同時(shí)借浊,他還會(huì)給我們帶來其他的幫助嗎?對(duì)于軟件設(shè)計(jì)的來說萝招,最為頭疼的地方就是蚂斤,他無時(shí)無刻不再面對(duì)者一個(gè)次——變化。不論從客戶的需求層面槐沼、技術(shù)平臺(tái)層面曙蒸、開發(fā)團(tuán)隊(duì)層面還是市場(chǎng)環(huán)境層面,都在面對(duì)著巨大的變化岗钩。而對(duì)應(yīng)著每次變化纽窟,那么就需要面對(duì)代碼的變更,這些變化會(huì)摧毀代碼的體系結(jié)構(gòu)的設(shè)計(jì)兼吓。
那么為了解決遇到問題的復(fù)雜性臂港,我們通常會(huì)用兩種方式來解決,一種是把分解视搏,另外一種是抽象审孽。
對(duì)于分解來說,其實(shí)就是分而治之浑娜,把大問題不斷的劃分為一個(gè)又一個(gè)的小問題佑力,通過解決分解開的每一個(gè)小問題來解決整體的大問題。也就是不斷的分工筋遭,各司其職來解決問題打颤。
對(duì)于抽象來說,屬于更高的層次漓滔,這其實(shí)是一種解決問題的通用技術(shù)瘸洛。由于抽象就是不具體,不能掌握全部的復(fù)雜對(duì)象次和,所以也就忽視了大量的細(xì)節(jié),而抓住一些主要細(xì)節(jié)那伐,如果我們過于具體踏施,對(duì)于中學(xué)所學(xué)習(xí)的物理就是難以進(jìn)行的,因?yàn)槲覀兌贾篮毖袑W(xué)的物理都建立在一套幾乎完全理想化的條件下畅形,比如摩擦力不變,空氣沒有阻力诉探,溫度不影響某一個(gè)參數(shù)的變化等等日熬。這樣抽象的結(jié)果,也是為了方便我們來解決主要的問題肾胯,而不至于陷入到細(xì)節(jié)的泥潭里無法自拔竖席。
對(duì)于所有的軟件思想都必須要落實(shí)在具體代碼實(shí)現(xiàn)上耘纱,所以,還是落在一些偽碼的描述層面來解釋抽象思維的好處和優(yōu)點(diǎn)毕荐。
那么先來看看這樣一段偽碼描述吧束析,它希望表達(dá)的是一種分而治之的思想,模擬了一套類似圖形繪制軟件的代碼憎亚。
class Point{
public:
int x;
int y;
};
class Line{
public:
Point start;
Point end;
Line(const Point& start, const Point& end){
this->start = start;
this->end = end;
}
};
class Rect{
public:
Point leftUp;
int width;
int height;
Rect(const Point& leftUp, int width, int height){
this->leftUp = leftUp;
this->width = width;
this->height = height;
}
};
class MainForm : public Form {
private:
Point p1;
Point p2;
vector<Line> lineVector;
vector<Rect> rectVector;
public:
MainForm(){
//...
}
protected:
virtual void OnMouseDown(const MouseEventArgs& e);
virtual void OnMouseUp(const MouseEventArgs& e);
virtual void OnPaint(const PaintEventArgs& e);
};
void MainForm::OnMouseDown(const MouseEventArgs& e){
p1.x = e.X;
p1.y = e.Y;
//...
Form::OnMouseDown(e);
}
void MainForm::OnMouseUp(const MouseEventArgs& e){
p2.x = e.X;
p2.y = e.Y;
if (rdoLine.Checked){
Line line(p1, p2);
lineVector.push_back(line);
}
else if (rdoRect.Checked){
int width = abs(p2.x - p1.x);
int height = abs(p2.y - p1.y);
Rect rect(p1, width, height);
rectVector.push_back(rect);
}
//...
this->Refresh();
Form::OnMouseUp(e);
}
void MainForm::OnPaint(const PaintEventArgs& e){
//針對(duì)直線
for (int i = 0; i < lineVector.size(); i++){
e.Graphics.DrawLine(Pens.Red,
lineVector[i].start.x,
lineVector[i].start.y,
lineVector[i].end.x,
lineVector[i].end.y);
}
//針對(duì)矩形
for (int i = 0; i < rectVector.size(); i++){
e.Graphics.DrawRectangle(Pens.Red,
rectVector[i].leftUp,
rectVector[i].width,
rectVector[i].height);
}
//...
Form::OnPaint(e);
}
上面這個(gè)偽代碼可以用來解決一些圖形的繪制员寇,比如矩形,直線和點(diǎn)的形狀第美。在MainForm的類里面對(duì)顯示界面的每個(gè)形狀都進(jìn)行了維護(hù)和處理蝶锋。
但是現(xiàn)在會(huì)遇到一個(gè)很嚴(yán)重的問題,那就是什往,我們的偽代碼只能解決矩形和線的問題扳缕,我們應(yīng)該如何來繪制一個(gè)圓呢,以上的代碼并不能直接來解決恶守。也就是這時(shí)候我們遇到了一個(gè)變化第献,這個(gè)代碼會(huì)發(fā)生什么變化呢?
class Point{
public:
int x;
int y;
};
class Line{
public:
Point start;
Point end;
Line(const Point& start, const Point& end){
this->start = start;
this->end = end;
}
};
class Rect{
public:
Point leftUp;
int width;
int height;
Rect(const Point& leftUp, int width, int height){
this->leftUp = leftUp;
this->width = width;
this->height = height;
}
};
//增加
class Circle{
};
class MainForm : public Form {
private:
Point p1;
Point p2;
vector<Line> lineVector;
vector<Rect> rectVector;
//改變
vector<Circle> circleVector;
public:
MainForm(){
//...
}
protected:
virtual void OnMouseDown(const MouseEventArgs& e);
virtual void OnMouseUp(const MouseEventArgs& e);
virtual void OnPaint(const PaintEventArgs& e);
};
void MainForm::OnMouseDown(const MouseEventArgs& e){
p1.x = e.X;
p1.y = e.Y;
//...
Form::OnMouseDown(e);
}
void MainForm::OnMouseUp(const MouseEventArgs& e){
p2.x = e.X;
p2.y = e.Y;
if (rdoLine.Checked){
Line line(p1, p2);
lineVector.push_back(line);
}
else if (rdoRect.Checked){
int width = abs(p2.x - p1.x);
int height = abs(p2.y - p1.y);
Rect rect(p1, width, height);
rectVector.push_back(rect);
}
//改變
else if (...){
//...
circleVector.push_back(circle);
}
//...
this->Refresh();
Form::OnMouseUp(e);
}
void MainForm::OnPaint(const PaintEventArgs& e){
//針對(duì)直線
for (int i = 0; i < lineVector.size(); i++){
e.Graphics.DrawLine(Pens.Red,
lineVector[i].start.x,
lineVector[i].start.y,
lineVector[i].end.x,
lineVector[i].end.y);
}
//針對(duì)矩形
for (int i = 0; i < rectVector.size(); i++){
e.Graphics.DrawRectangle(Pens.Red,
rectVector[i].leftUp,
rectVector[i].width,
rectVector[i].height);
}
//改變
//針對(duì)圓形
for (int i = 0; i < circleVector.size(); i++){
e.Graphics.DrawCircle(Pens.Red,
circleVector[i]);
}
//...
Form::OnPaint(e);
}
假設(shè)以上的偽碼已經(jīng)實(shí)現(xiàn)了對(duì)圓形的功能的擴(kuò)展兔港,那么在擴(kuò)展的過程中遇到了什么問題嗎庸毫?其實(shí)問題還是很明顯的,就是我們需要改變的地方非常的零散衫樊,不但需要添加一個(gè)Circle的類飒赃,還需要在MainForm中進(jìn)行業(yè)務(wù)的修改,通過這些修改才能改完成圓形功能的擴(kuò)展科侈。而如果站在軟件工程的角度上來看载佳,對(duì)于修改過的部分都需要在重新測(cè)試等一些列的操作,其實(shí)對(duì)于功能擴(kuò)充來說其實(shí)面對(duì)著后期大量的工作臀栈。這對(duì)于我們管理和維護(hù)我們的代碼是相當(dāng)不利的蔫慧。同時(shí)對(duì)于多人的協(xié)作也是不利的,每個(gè)人修改完代碼权薯,還需要通知另外的人也需要修改代碼姑躲,這不但不方便,同時(shí)也增加了團(tuán)隊(duì)出錯(cuò)的概率盟蚣。
如果有一種方法黍析,在我們遇到擴(kuò)展的時(shí)候,只需要 增加我們的擴(kuò)展內(nèi)容屎开,而不修改之前我們代碼就好了阐枣。那么我們先來看看關(guān)于抽象的設(shè)計(jì)思路吧。
class Shape{
public:
virtual void Draw(const Graphics& g)=0;
virtual ~Shape() { }
};
class Point{
public:
int x;
int y;
};
class Line: public Shape{
public:
Point start;
Point end;
Line(const Point& start, const Point& end){
this->start = start;
this->end = end;
}
//實(shí)現(xiàn)自己的Draw,負(fù)責(zé)畫自己
virtual void Draw(const Graphics& g){
g.DrawLine(Pens.Red,
start.x, start.y,end.x, end.y);
}
};
class Rect: public Shape{
public:
Point leftUp;
int width;
int height;
Rect(const Point& leftUp, int width, int height){
this->leftUp = leftUp;
this->width = width;
this->height = height;
}
//實(shí)現(xiàn)自己的Draw蔼两,負(fù)責(zé)畫自己
virtual void Draw(const Graphics& g){
g.DrawRectangle(Pens.Red,
leftUp,width,height);
}
};
class MainForm : public Form {
private:
Point p1;
Point p2;
//針對(duì)所有形狀
vector<Shape*> shapeVector;
public:
MainForm(){
//...
}
protected:
virtual void OnMouseDown(const MouseEventArgs& e);
virtual void OnMouseUp(const MouseEventArgs& e);
virtual void OnPaint(const PaintEventArgs& e);
};
void MainForm::OnMouseDown(const MouseEventArgs& e){
p1.x = e.X;
p1.y = e.Y;
//...
Form::OnMouseDown(e);
}
void MainForm::OnMouseUp(const MouseEventArgs& e){
p2.x = e.X;
p2.y = e.Y;
if (rdoLine.Checked){
shapeVector.push_back(new Line(p1,p2));
}
else if (rdoRect.Checked){
int width = abs(p2.x - p1.x);
int height = abs(p2.y - p1.y);
shapeVector.push_back(new Rect(p1, width, height));
}
else if (...){
//...
shapeVector.push_back(circle);
}
//...
this->Refresh();
Form::OnMouseUp(e);
}
void MainForm::OnPaint(const PaintEventArgs& e){
//針對(duì)所有形狀
for (int i = 0; i < shapeVector.size(); i++){
shapeVector[i]->Draw(e.Graphics); //多態(tài)調(diào)用甩鳄,各負(fù)其責(zé)
}
//...
Form::OnPaint(e);
}
對(duì)以上的代碼,可以看出宪哩,對(duì)于每個(gè)形狀娩贷,都增加了一個(gè)共同的父類Shape,同時(shí)對(duì)于圖形來說應(yīng)該如何繪制自己的這樣的形狀的函數(shù)都是由自己來實(shí)現(xiàn)的(Draw(...)函數(shù))锁孟。對(duì)于MainForm來說他管理的容器也發(fā)生了改變彬祖,只需要管理Shape的指針,而不再具體管理某一個(gè)具體的形狀品抽,實(shí)現(xiàn)了一次向上抽象的管理储笑。同時(shí)onPaint的方法來說,也不在直接管理繪制的問題圆恤,而是調(diào)用了Shape中的虛函數(shù)Draw突倍,通過多態(tài)調(diào)用來實(shí)現(xiàn)了MainForm來調(diào)用了自己繪制自己的方法。
假設(shè)現(xiàn)在在這個(gè)架構(gòu)中需要增加一個(gè)圓形的畫法盆昙。那么代碼會(huì)發(fā)生以下的變化羽历。
class Shape{
public:
virtual void Draw(const Graphics& g)=0;
virtual ~Shape() { }
};
class Point{
public:
int x;
int y;
};
class Line: public Shape{
public:
Point start;
Point end;
Line(const Point& start, const Point& end){
this->start = start;
this->end = end;
}
//實(shí)現(xiàn)自己的Draw,負(fù)責(zé)畫自己
virtual void Draw(const Graphics& g){
g.DrawLine(Pens.Red,
start.x, start.y,end.x, end.y);
}
};
class Rect: public Shape{
public:
Point leftUp;
int width;
int height;
Rect(const Point& leftUp, int width, int height){
this->leftUp = leftUp;
this->width = width;
this->height = height;
}
//實(shí)現(xiàn)自己的Draw淡喜,負(fù)責(zé)畫自己
virtual void Draw(const Graphics& g){
g.DrawRectangle(Pens.Red,
leftUp,width,height);
}
};
//增加
class Circle : public Shape{
public:
//實(shí)現(xiàn)自己的Draw秕磷,負(fù)責(zé)畫自己
virtual void Draw(const Graphics& g){
g.DrawCircle(Pens.Red,
...);
}
};
class MainForm : public Form {
private:
Point p1;
Point p2;
//針對(duì)所有形狀
vector<Shape*> shapeVector;
public:
MainForm(){
//...
}
protected:
virtual void OnMouseDown(const MouseEventArgs& e);
virtual void OnMouseUp(const MouseEventArgs& e);
virtual void OnPaint(const PaintEventArgs& e);
};
void MainForm::OnMouseDown(const MouseEventArgs& e){
p1.x = e.X;
p1.y = e.Y;
//...
Form::OnMouseDown(e);
}
void MainForm::OnMouseUp(const MouseEventArgs& e){
p2.x = e.X;
p2.y = e.Y;
if (rdoLine.Checked){
shapeVector.push_back(new Line(p1,p2));
}
else if (rdoRect.Checked){
int width = abs(p2.x - p1.x);
int height = abs(p2.y - p1.y);
shapeVector.push_back(new Rect(p1, width, height));
}
//改變
else if (...){
//...
shapeVector.push_back(circle);
}
//...
this->Refresh();
Form::OnMouseUp(e);
}
void MainForm::OnPaint(const PaintEventArgs& e){
//針對(duì)所有形狀
for (int i = 0; i < shapeVector.size(); i++){
shapeVector[i]->Draw(e.Graphics); //多態(tài)調(diào)用,各負(fù)其責(zé)
}
//...
Form::OnPaint(e);
}
由以上的代碼可以看出炼团,變化的部分澎嚣,不會(huì)有之前的代碼變化的部分那么零散,不需要在各種地方做改變瘟芝,那么也正是由于這樣的抽象的思想易桃,使得代碼的變更更加容易,重用性得到了提升锌俱。
什么是好的軟件設(shè)計(jì)呢晤郑?軟件設(shè)計(jì)的金科玉律:復(fù)用
面向?qū)ο蟮脑O(shè)計(jì)到底有沒有什么原則呢?
變化是復(fù)用最大的天敵贸宏!面向?qū)ο蟮淖畲髢?yōu)勢(shì)就在于:抵御變化贩汉。
面向?qū)ο蟮脑瓌t
- 依賴倒置原則(DIP)
高層模塊(穩(wěn)定的)不應(yīng)該依賴于低層模塊(容易變化的),二者都應(yīng)該依賴于抽象(穩(wěn)定的)
抽象(穩(wěn)定的)不應(yīng)該依賴于實(shí)現(xiàn)細(xì)節(jié)(容易變化的)锚赤,實(shí)現(xiàn)細(xì)節(jié)應(yīng)該依賴于抽象(穩(wěn)定的)
-
開放封閉原則(OCP)
- 對(duì)擴(kuò)展開放,對(duì)更改封閉
- 模塊應(yīng)該是可擴(kuò)展的褐鸥,但是不可修改
-
單一指責(zé)原則(SRP)
- 一個(gè)類應(yīng)該僅有一個(gè)引起它變化的原因
- 變化的方向隱含著類的責(zé)任
-
Liskov替換原則(LSP)
- 子類必須能夠替換他們的基類(is-a)
- 集成表達(dá)抽血類型
-
接口隔離原則(ISP)
- 不應(yīng)該強(qiáng)迫客戶程序依賴他們不用的方法
- 接口應(yīng)該小而完備
-
優(yōu)先使用對(duì)象組合线脚,而不是類繼承
- 類繼承通常為“白盒復(fù)用”,對(duì)象組合通常為“黑箱復(fù)用”
- 繼承在某種程度上破壞了封裝性,子類父類耦合度高
- 而對(duì)象組合則只要求被組合的對(duì)象具有良好定義的接口浑侥,耦合度低
-
封裝變化點(diǎn)
- 使用封裝來創(chuàng)建對(duì)象之間的分界層姊舵,讓設(shè)計(jì)者可以在分界層的一側(cè)進(jìn)行修改,而不會(huì)對(duì)另一側(cè)產(chǎn)生不良的影響寓落,從而實(shí)現(xiàn)層次間的松耦合
-
針對(duì)接口編程括丁,而不是針對(duì)實(shí)現(xiàn)編程
- 不講變量類型聲明為某個(gè)特定的具體類,而是聲明為某個(gè)接口
- 客戶程序無需獲知對(duì)象的具體類型伶选,只需要知道對(duì)象所具有的接口
- 減少系統(tǒng)中個(gè)部分的依賴關(guān)系史飞,從而實(shí)現(xiàn)“高內(nèi)聚、松耦合”的類型設(shè)計(jì)方案
產(chǎn)業(yè)強(qiáng)盛的標(biāo)志:接口的標(biāo)準(zhǔn)化
GOF-23模式分類
- 從目的分
- 創(chuàng)建型(Creational)模式
將對(duì)象仰税,從而對(duì)應(yīng)需求變化為對(duì)象創(chuàng)建時(shí)具體類型的實(shí)現(xiàn)引來的沖擊构资。
- 結(jié)構(gòu)型(Structural)模式
通過類繼承或者對(duì)象組合的方式來獲得更靈活的結(jié)構(gòu),從而應(yīng)對(duì)需求變化為對(duì)象的結(jié)構(gòu)帶來的沖擊 - 行為型(Behavioral)模式
通過類繼承或者對(duì)象組合的方式陨簇,來劃分類與對(duì)象的指責(zé)吐绵,從而應(yīng)對(duì)需求變化為多個(gè)交互的對(duì)象帶來的沖擊。
- 創(chuàng)建型(Creational)模式
- 從范圍來看
- 類模式處理與子類的靜態(tài)關(guān)系
- 對(duì)象模式處理間的動(dòng)態(tài)關(guān)系