博览网: C++设计模式 第一周笔记

1.什么是设计模式

“每一个描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案。这样,你就能一次又一次地使用该方案而不必做重复劳动”。

——Christopher Alexander

设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。

使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

我们在解决问题的时候,常常采取两种方式,一种是分解问题,另一种是抽象。

对于分解来说,其实就是分而治之,把大问题不断的划分为一个又一个的小问题,通过解决分解开的每一个小问题来解决整体的大问题。也就是不断的分工,各司其职来解决问题。

而抽象则属于更高的层次,从我们需要解决的问题出发,在与该问题相关的一组关联对象中提取出主要的或共有的部分――说简单一点,就是用相同的行为来操作不同的对象。

先来看看第一种方式解决问题的伪代码:

classPoint{

public:

intx;

inty;

};

classLine{

public:

Point start;

Point end;

Line(constPoint& start,constPoint& end){

this->start = start;

this->end = end;

}

};

classRect{

public:

Point leftUp;

intwidth;

intheight;

Rect(constPoint& leftUp,intwidth,intheight){

this->leftUp = leftUp;

this->width = width;

this->height = height;

}

};

//增加

classCircle{

};

classMainForm :publicForm {

private:

Point p1;

Point p2;

vector lineVector;

vector rectVector;

//改变

vector circleVector;

public:

MainForm(){

//...

}

protected:

virtualvoidOnMouseDown(constMouseEventArgs& e);

virtualvoidOnMouseUp(constMouseEventArgs& e);

virtualvoidOnPaint(constPaintEventArgs& e);

};

voidMainForm::OnMouseDown(constMouseEventArgs& e){

p1.x = e.X;

p1.y = e.Y;

//...

Form::OnMouseDown(e);

}

voidMainForm::OnMouseUp(constMouseEventArgs& e){

p2.x = e.X;

p2.y = e.Y;

if(rdoLine.Checked){

Line line(p1, p2);

lineVector.push_back(line);

}

elseif(rdoRect.Checked){

intwidth = abs(p2.x - p1.x);

intheight = abs(p2.y - p1.y);

Rect rect(p1, width, height);

rectVector.push_back(rect);

}

//改变

elseif(...){

//...

circleVector.push_back(circle);

}

//...

this->Refresh();

Form::OnMouseUp(e);

}

voidMainForm::OnPaint(constPaintEventArgs& e){

//针对直线

for(inti = 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);

}

//针对矩形

for(inti = 0; i < rectVector.size(); i++){

e.Graphics.DrawRectangle(Pens.Red,

rectVector[i].leftUp,

rectVector[i].width,

rectVector[i].height);

}

//改变

//针对圆形

for(inti = 0; i < circleVector.size(); i++){

e.Graphics.DrawCircle(Pens.Red,

circleVector[i]);

}

//...

Form::OnPaint(e);

}

上面的伪代码可以用来解决点、直线和矩形的绘制,但是遇到绘制其他图形的时候就会遇到问题,必须对原伪代码进行繁琐的修改,具体修改不再进行。

如果使用另外一种方法,抽象的设计思路时,这个问题就会迎刃而解,伪代码如下:

lass Shape{

public:

virtualvoidDraw(constGraphics& g)=0;

virtual~Shape() { }

};

classPoint{

public:

intx;

inty;

};

classLine:publicShape{

public:

Point start;

Point end;

Line(constPoint& start,constPoint& end){

this->start = start;

this->end = end;

}

//实现自己的Draw,负责画自己

virtualvoidDraw(constGraphics& g){

g.DrawLine(Pens.Red,

start.x, start.y,end.x, end.y);

}

};

classRect:publicShape{

public:

Point leftUp;

intwidth;

intheight;

Rect(constPoint& leftUp,intwidth,intheight){

this->leftUp = leftUp;

this->width = width;

this->height = height;

}

//实现自己的Draw,负责画自己

virtualvoidDraw(constGraphics& g){

g.DrawRectangle(Pens.Red,

leftUp,width,height);

}

};

classMainForm :publicForm {

private:

Point p1;

Point p2;

//针对所有形状

vector shapeVector;

public:

MainForm(){

//...

}

protected:

virtualvoidOnMouseDown(constMouseEventArgs& e);

virtualvoidOnMouseUp(constMouseEventArgs& e);

virtualvoidOnPaint(constPaintEventArgs& e);

};

voidMainForm::OnMouseDown(constMouseEventArgs& e){

p1.x = e.X;

p1.y = e.Y;

//...

Form::OnMouseDown(e);

}

voidMainForm::OnMouseUp(constMouseEventArgs& e){

p2.x = e.X;

p2.y = e.Y;

if(rdoLine.Checked){

shapeVector.push_back(newLine(p1,p2));

}

elseif(rdoRect.Checked){

intwidth = abs(p2.x - p1.x);

intheight = abs(p2.y - p1.y);

shapeVector.push_back(newRect(p1, width, height));

}

elseif(...){

//...

shapeVector.push_back(circle);

}

//...

this->Refresh();

Form::OnMouseUp(e);

}

voidMainForm::OnPaint(constPaintEventArgs& e){

//针对所有形状

for(inti = 0; i < shapeVector.size(); i++){

shapeVector[i]->Draw(e.Graphics);//多态调用,各负其责

}

//...

Form::OnPaint(e);

}

从上面的伪代码可以看出,对每个形状增加了一个共同的父类Shape,同时在父类中定义了纯虚函数Draw,使得在多态调用的时候每个子类可以按照自己的方式来实现Draw函数。而界面类MainForm变成只需管理Shape的指针,不再是某个具体的对象,实现了向上抽象的管理。

如果这时候我们增加一个圆形的画法,那么我们只需在class Rect后面增加以下代码:

classCircle :publicShape{

public:

//实现自己的Draw,负责画自己

virtualvoidDraw(constGraphics& g){

g.DrawCircle(Pens.Red,

...);

}

};

通过以上的代码可以看出,当我们使用抽象思维对代码进行设计的时候,使得代码的变更更加容易,代码的复用性得到了提升,它是通过面向对象中的继承和多态性来实现。

作为一名程序员或者说是未来的程序员,抽象思维非常重要,甚至在某种程度上比底层思维更加重要,它在C++程序设计中主要包括以下几个部分:面向对象,组件封装,设计模式,架构模式。

2.面向对象设计原则

软件设计复杂的根本原因是因为它会遇到各种各样的变化:客户需求变化,技术平台变化,开发团队变化,市场环境变化等等。

抽象思维的最大特点是复用性,也就可以抵御这些变化。在1中我们谈到面向对象是抽象思维实现的基础,先让我们重新认识面向对象:



为了确保设计出来地系统具有抽象思维的特性,面向对象设计中提出了一系列的基本原则作为设计的思想。

(1)依赖倒置原则(DIP)

高层模块(稳定)不应该依赖于低层模块(变化),二者都应该依赖于抽象(稳定)。

抽象(稳定)不应该依赖于实现细节(变化),实现细节应该依赖于抽象(稳定)。

(2)开放封闭原则(OCP)

对扩展开放,对更改封闭

模块应该是可扩展的,但是不可修改

(3)单一指责原则(SRP)

一个类应该仅有一个引起它变化的原因

变化的方向隐含着类的责任

(4)Liskov替换原则(LSP)

子类必须能够替换他们的基类(is-a)

集成表达抽血类型

(5)接口隔离原则(ISP)

不应该强迫客户程序依赖他们不用的方法

接口应该小而完备

(6)优先使用对象组合,而不是类继承

类继承通常为“白盒复用”,对象组合通常为“黑箱复用”

继承在某种程度上破坏了封装性,子类父类耦合度高

而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低

(7)封装变化点

使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合

(8)针对接口编程,而不是针对实现编程

不讲变量类型声明为某个特定的具体类,而是声明为某个接口

客户程序无需获知对象的具体类型,只需要知道对象所具有的接口

减少系统中个部分的依赖关系,从而实现“高内聚、松耦合”的类型设计方案

产业强盛的标志:接口的标准化

3. 23个设计模式

GOF-23模式分类

(1)从目的来看

创建型(Creational)模式:将对象,从而对应需求变化为对象创建时具体类型的实现引来的冲击。

结构型(Structural)模式:通过类继承或者对象组合的方式来获得更灵活的结构,从而应对需求变化为对象的结构带来的冲击

行为型(Behavioral)模式:通过类继承或者对象组合的方式,来划分类与对象的指责,从而应对需求变化为多个交互的对象带来的冲击。

(2)从范围来看

类模式处理类与子类的静态关系

对象模式处理对象间的动态关系

从封装变化角度对模式分类



通过重构获得模式,我们来正确使用对应的设计模式。



推荐图书:重构-改善既有代码的设计、重构与模式。

重构关键技法:

静态绑定→动态绑定

早绑定→晚绑定

继承关系→组合关系

编译时依赖→运行时依赖

紧耦合→松耦合

“组件协作”模式:

现代软件专业分工之后的第一个结果是“框架与应用程序的划分”,“组件协作”模式通过晚期绑定,来实现框架与应用程序之间的松耦合,是二者之间协作时常用的模式。

典型模式:Template Method、Strategy、 Observer / Event。

3.1 Template Method模式

动机(Motivation)

在软件构建过程中,对于某一项任务,它常常有稳定的整体操作结构,但各个子步骤却有很大改变的需求,或者由于固有的原因(比如框架与应用之间的关系)而无法和任务的整体结构同时实现。

如何在确定稳定操作结构额前提下,来灵活应对各个子步骤的变化或者晚期实现需求?

下面以具体的代码为例进行学习,见代码3.1.1:

代码3.1.1

//程序库开发人员

classLibrary{

public:

voidStep1(){

//...

}

voidStep3(){

//...

}

voidStep5(){

//...

}

};

//应用程序开发人员

classApplication{

public:

boolStep2(){

//...

}

voidStep4(){

//...

}

};

intmain()

{

Library lib();

Application app();

lib.Step1();

if(app.Step2()){

lib.Step3();

}

for(inti = 0; i < 4; i++){

app.Step4();

}

lib.Step5();

}

上述代码

Library开发人员需要开发1、3、5三个步骤,Application开发人员开发2、4两个步骤和程序主流程。这种通过应用端对流程框架进行调用的方式,称之为早绑定。



上述代码如何在确定稳定操作结构额前提下,来灵活应对各个子步骤的变化或者晚期实现需求呢?

请看3.1.2代码

代码3.1.2

//程序库开发人员

classLibrary{

public:

//稳定 template method

voidRun(){

Step1();

if(Step2()) {//支持变化 ==> 虚函数的多态调用

Step3();

}

for(inti = 0; i < 4; i++){

Step4();//支持变化 ==> 虚函数的多态调用

}

Step5();

}

virtual~Library(){ }

protected:

voidStep1() {//稳定

//.....

}

voidStep3() {//稳定

//.....

}

voidStep5() {//稳定

//.....

}

virtualboolStep2() = 0;//变化

virtualvoidStep4() =0;//变化

};

//应用程序开发人员

classApplication :publicLibrary {

protected:

virtualboolStep2(){

//... 子类重写实现

}

virtualvoidStep4() {

//... 子类重写实现

}

};

intmain()

{

Library* pLib=newApplication();

lib->Run();

deletepLib;

}

}

上述代码在3.1.1的基础上做修改后,Library开发人员需要开发1、3、5三个步骤和程序主流程,Application开发人员只需开发2、4两个步骤。这种将流程放入开发库,由库开发者开发,提供给应用程序开发者调用的方法,称之为晚绑定



在代码3.1.1中,存在以下两个问题:

第一,对应用程序开发人员来说,需要自己开发其中的2、4两个步骤,要求比较高,需要对库中的函数情况比较了解,而且重写的两个函数的难度也相对较大。

第二,库开发人员和应用程序开发人员开发的内容的耦合度很高,需要由用开发人员来组织整体调用流程,未来程序的扩展性和可维护性的难度都比较大。

而在代码3.1.2中,库开发人员对开发库进行了重构,增加量两个虚函数,定义了一个run函数,将之前的主流程放入,从而使流程稳定。同时库开发者不知道应用程序开发者会如何设计2、4步骤,因此将其定义为虚函数。

应用程序开发者只需在子类中重新定义这两个虚函数即可,不需要再去考虑整个流程的实现,有效的降低了开发难度。

模式定义

定义一个操作中的算法的骨架(稳定),而将一些步骤延迟(变化)到子类中。Template Method 使得子类可以在不改变(复用)一个算法的结构即可重新定义(Override 重写)该算法的某些特定步骤。

——《设计模式》GoF

结构(Structure)



要点总结

Template Method 是一种非常基础性的设计模式,在面向对象的系统中,有着大量的应用。他用最简洁的机制(虚函数的多态性)为很多应用程序的框架提供了灵活的扩展点,是代码复用方面的基本实现结构。

除了可以灵活对应子步骤的变化外,“不要调用我,让我来调用你”的反向控制结构是 Template Method 的典型应用。

在具体实现方面,被 Template Method 调用的虚方法可以具有实现,也可以没有任何实现(抽象方法、纯虚方法),一般推荐将他们设置为 Protected 方法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,525评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,203评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,862评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,728评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,743评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,590评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,330评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,244评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,693评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,885评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,001评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,723评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,343评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,919评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,042评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,191评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,955评论 2 355

推荐阅读更多精彩内容