(Boolan)C++设计模式 <一> ——设计模式
什么是设计模式
每一个描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案。这样,你就能一次又一次地使用该方案而不必做重复劳动。
——Christopher Alexander
对于我们处理工作中的实际问题的时候,其实绝大多数的问题都是呈一定的模式重复出现的,如果不使用设计模式,那么我们在处理问题的时候,不得不一遍又一遍重复做着类似的工作,不但浪费了大量的精力,同时也更是一种极度让人烦躁的事。
那么面向对象可以帮我们节省大量的精力来不必要每次都重头开始处理那些差不多的问题,在真正的开始面对设计模式之前,那么我们得要先从对象说起。
之前我的文章里面谈到了我关于对象理解,对象其实是一种人类解释现实世界的通用规则。其实用好对象的核心思想就是不断的学着像解释理解这个世界一样,来不断地抽象的看待这个世界,并用代码来讲关于世界的故事。
在此,我不想过多地谈我过去我写过的东西(如果感兴趣可以查看之前的文章),毕竟这就是一种无趣的重复劳动,实在让人感觉提不起精神。但是,既然说道了面向对象,那么,这其中不得不提那么几句,这里我会尽量不炒冷饭。
抽象到底有多厉害?!
其实人类在认识世界的过程中,最大的特点也是现在最火热的机器学习、人工智能、深度学习这些目前应该是最火热的前沿技术希望得到的一种能力,其实就是抽象的能力。
我还是那我这条故事狗最喜爱的举例子的动物,猫咪来说,就像上图的猫咪,对于人类的孩子来说,只需要告诉他其中的某一个猫咪,这个是叫做猫咪,那么当他看到其他的八成是能够认识的(估计遇到上图中的第一个,孩子认不出来也正常)。这些猫在人类看一眼看去全都是一样的,不一样的是细节问题,比如这是一只瘦猫,这是一只肥猫,这是花猫,这是白猫、黑猫等等等。即使仅仅听到一声喵也就知道一定有猫的存在。对于人来说,猫是一个大类,每只猫都是独立的存在。这时候看起来,每一只独立的猫有些类似于计算机的对象,而猫就类似于一个类了。
但是对于计算机来说,对象就是内存中的一块空间而已,在这块内存空间里面保存了一些数据,通过这些数据来解释了这个世界。比如,一只黑色的猫,眼睛是绿色等等,这些数据保存在内存中。也就是其实每个对象他是都是具体的,而不是抽象的,他只能表现着一只猫,如果另外一个猫咪的脚是白色的,那么数据不匹配,就应该是另外一个对象。所以其实对象对于人类世界来说就是不同的个体,每个个体都应该是独立的对象,因此也有学着曾经提出,将object翻译为对象其实并不合理,而应该翻译为物体就是这个道理。
如果我们观察每个对象到底是如何构成,就像我们在讨论一个人是什么性格一样,其实这些就相当于是底层思维模式,而人类更具备的一种向上抽象的能力。比如看到猫的第一反应是猫,而之后才会是他的毛色等等具体的特征。
而面向对象的过程,其实就是希望模拟人类这样一种抽象的思维,通过抽象思维来解释这样的世界。
而为了更好的能够用计算机解释好这个世界,程序员必须要有一套很好的抽象思维。
那么,抽象思维在帮我们更好的解释这个世界的同时,他还会给我们带来其他的帮助吗?对于软件设计的来说,最为头疼的地方就是,他无时无刻不再面对者一个次——变化。不论从客户的需求层面、技术平台层面、开发团队层面还是市场环境层面,都在面对着巨大的变化。而对应着每次变化,那么就需要面对代码的变更,这些变化会摧毁代码的体系结构的设计。
那么为了解决遇到问题的复杂性,我们通常会用两种方式来解决,一种是把分解,另外一种是抽象。
对于分解来说,其实就是分而治之,把大问题不断的划分为一个又一个的小问题,通过解决分解开的每一个小问题来解决整体的大问题。也就是不断的分工,各司其职来解决问题。
对于抽象来说,属于更高的层次,这其实是一种解决问题的通用技术。由于抽象就是不具体,不能掌握全部的复杂对象,所以也就忽视了大量的细节,而抓住一些主要细节,如果我们过于具体,对于中学所学习的物理就是难以进行的,因为我们都知道,中学的物理都建立在一套几乎完全理想化的条件下,比如摩擦力不变,空气没有阻力,温度不影响某一个参数的变化等等。这样抽象的结果,也是为了方便我们来解决主要的问题,而不至于陷入到细节的泥潭里无法自拔。
对于所有的软件思想都必须要落实在具体代码实现上,所以,还是落在一些伪码的描述层面来解释抽象思维的好处和优点。
那么先来看看这样一段伪码描述吧,它希望表达的是一种分而治之的思想,模拟了一套类似图形绘制软件的代码。
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){
//针对直线
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);
}
//针对矩形
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);
}
上面这个伪代码可以用来解决一些图形的绘制,比如矩形,直线和点的形状。在MainForm的类里面对显示界面的每个形状都进行了维护和处理。
但是现在会遇到一个很严重的问题,那就是,我们的伪代码只能解决矩形和线的问题,我们应该如何来绘制一个圆呢,以上的代码并不能直接来解决。也就是这时候我们遇到了一个变化,这个代码会发生什么变化呢?
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){
//针对直线
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);
}
//针对矩形
for (int i = 0; i < rectVector.size(); i++){
e.Graphics.DrawRectangle(Pens.Red,
rectVector[i].leftUp,
rectVector[i].width,
rectVector[i].height);
}
//改变
//针对圆形
for (int i = 0; i < circleVector.size(); i++){
e.Graphics.DrawCircle(Pens.Red,
circleVector[i]);
}
//...
Form::OnPaint(e);
}
假设以上的伪码已经实现了对圆形的功能的扩展,那么在扩展的过程中遇到了什么问题吗?其实问题还是很明显的,就是我们需要改变的地方非常的零散,不但需要添加一个Circle的类,还需要在MainForm中进行业务的修改,通过这些修改才能改完成圆形功能的扩展。而如果站在软件工程的角度上来看,对于修改过的部分都需要在重新测试等一些列的操作,其实对于功能扩充来说其实面对着后期大量的工作。这对于我们管理和维护我们的代码是相当不利的。同时对于多人的协作也是不利的,每个人修改完代码,还需要通知另外的人也需要修改代码,这不但不方便,同时也增加了团队出错的概率。
如果有一种方法,在我们遇到扩展的时候,只需要 增加我们的扩展内容,而不修改之前我们代码就好了。那么我们先来看看关于抽象的设计思路吧。
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;
}
//实现自己的Draw,负责画自己
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;
}
//实现自己的Draw,负责画自己
virtual void Draw(const Graphics& g){
g.DrawRectangle(Pens.Red,
leftUp,width,height);
}
};
class MainForm : public Form {
private:
Point p1;
Point p2;
//针对所有形状
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){
//针对所有形状
for (int i = 0; i < shapeVector.size(); i++){
shapeVector[i]->Draw(e.Graphics); //多态调用,各负其责
}
//...
Form::OnPaint(e);
}
对以上的代码,可以看出,对于每个形状,都增加了一个共同的父类Shape,同时对于图形来说应该如何绘制自己的这样的形状的函数都是由自己来实现的(Draw(...)函数)。对于MainForm来说他管理的容器也发生了改变,只需要管理Shape的指针,而不再具体管理某一个具体的形状,实现了一次向上抽象的管理。同时onPaint的方法来说,也不在直接管理绘制的问题,而是调用了Shape中的虚函数Draw,通过多态调用来实现了MainForm来调用了自己绘制自己的方法。
假设现在在这个架构中需要增加一个圆形的画法。那么代码会发生以下的变化。
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;
}
//实现自己的Draw,负责画自己
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;
}
//实现自己的Draw,负责画自己
virtual void Draw(const Graphics& g){
g.DrawRectangle(Pens.Red,
leftUp,width,height);
}
};
//增加
class Circle : public Shape{
public:
//实现自己的Draw,负责画自己
virtual void Draw(const Graphics& g){
g.DrawCircle(Pens.Red,
...);
}
};
class MainForm : public Form {
private:
Point p1;
Point p2;
//针对所有形状
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){
//针对所有形状
for (int i = 0; i < shapeVector.size(); i++){
shapeVector[i]->Draw(e.Graphics); //多态调用,各负其责
}
//...
Form::OnPaint(e);
}
由以上的代码可以看出,变化的部分,不会有之前的代码变化的部分那么零散,不需要在各种地方做改变,那么也正是由于这样的抽象的思想,使得代码的变更更加容易,重用性得到了提升。
依赖情况什么是好的软件设计呢?软件设计的金科玉律:复用
面向对象的设计到底有没有什么原则呢?
变化是复用最大的天敌!面向对象的最大优势就在于:抵御变化。
面向对象的原则
- 依赖倒置原则(DIP)
-
高层模块(稳定的)不应该依赖于低层模块(容易变化的),二者都应该依赖于抽象(稳定的)
-
抽象(稳定的)不应该依赖于实现细节(容易变化的),实现细节应该依赖于抽象(稳定的)
-
开放封闭原则(OCP)
- 对扩展开放,对更改封闭
- 模块应该是可扩展的,但是不可修改
-
单一指责原则(SRP)
- 一个类应该仅有一个引起它变化的原因
- 变化的方向隐含着类的责任
-
Liskov替换原则(LSP)
- 子类必须能够替换他们的基类(is-a)
- 集成表达抽血类型
-
接口隔离原则(ISP)
- 不应该强迫客户程序依赖他们不用的方法
- 接口应该小而完备
-
优先使用对象组合,而不是类继承
- 类继承通常为“白盒复用”,对象组合通常为“黑箱复用”
- 继承在某种程度上破坏了封装性,子类父类耦合度高
- 而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低
-
封装变化点
- 使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合
-
针对接口编程,而不是针对实现编程
- 不讲变量类型声明为某个特定的具体类,而是声明为某个接口
- 客户程序无需获知对象的具体类型,只需要知道对象所具有的接口
- 减少系统中个部分的依赖关系,从而实现“高内聚、松耦合”的类型设计方案
产业强盛的标志:接口的标准化
GOF-23模式分类
- 从目的分
- 创建型(Creational)模式
将对象,从而对应需求变化为对象创建时具体类型的实现引来的冲击。
- 结构型(Structural)模式
通过类继承或者对象组合的方式来获得更灵活的结构,从而应对需求变化为对象的结构带来的冲击 - 行为型(Behavioral)模式
通过类继承或者对象组合的方式,来划分类与对象的指责,从而应对需求变化为多个交互的对象带来的冲击。
- 创建型(Creational)模式
- 从范围来看
- 类模式处理与子类的静态关系
- 对象模式处理间的动态关系