文章目录
参考网站: 菜鸟教程 C语言中文网
设计模式简介
设计模式
- 设计模式代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。
- 设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。
- 这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。
使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。 - 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。
- 项目中合理地运用设计模式可以完美地解决很多问题,每种模式在现实中都有相应的原理来与之对应,每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是设计模式能被广泛应用的原因。
分类
模式 | & 描述 | 包括 |
---|---|---|
创建型模式 | 这些设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。 | 工厂模式 抽象工厂模式 单例模式 建造者模式 原型模式 |
结构型模式 | 这些设计模式关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式。 | 适配器模式 桥接模式 过滤器模式 组合模式 装饰器模式 外观模式 享元模式 代理模式 |
行为型模式 | 这些设计模式特别关注对象之间的通信。 | 责任链模式 命令模式 解释器模式 迭代器模式 中介者模式 备忘录模式 观察者模式 状态模式 空对象模式 策略模式 模板模式 访问者模式 |
J2EE 模式 | 这些设计模式特别关注表示层。这些模式是由 Sun Java Center 鉴定的。 | MVC 模式业务代表模式 组合实体模式 数据访问对象模式 前端控制器模式 拦截过滤器模式 服务定位器模式 传输对象模式 |
设计模式之间的关系
面向对象设计原则
为什么要用面向对象设计?
抵御变化!(将变化降成最小)
例子
对于一套画图程序:
头文件:
// 点类
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);
}
当我们需要增加一个需求的时候:画圆形
我们需要修改或者添加的代码:
- 头文件增加圆的类
class Circle { };
- 数据成员需要添加
vector<Circle> circleVector;
- OnMouseUp需要添加对圆的处理函数
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); }
- OnPaint增加对圆的绘制
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); }
换一种方法:
头文件:
// 绘制函数
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));
}
// ...
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);
}
当我们需要增加一个需求的时候:画圆形
我们需要修改或者添加的代码:
- 头文件增加圆的类 需要实现自己的绘制函数
class Circle : public Shape { public: // 实现自己的Draw,负责画自己 virtual void Draw(const Graphics& g) { g.DrawCircle(Pens.Red, ...); } };
- OnMouseUp增加对圆的处理
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); }
虽然本质上需要写的核心代码只是换了个位置,但是需要改变的位置少了,代码出错的机率就变小了!
重新认识面向对象
- 从宏观来看,面向对象的构建方式更能适应软件的变化,能将变化所带来的影响减为最小。
- 从微观层面来看,面向对象的方式更强调各个类的“责任”。(增加新类型不应该影响原来类型的实现)
设计原则
1、开放封闭原则(OCP)
- 对扩展开放,对更改封闭。
- 类模板应该是可扩展的,但是不可修改。
在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。
可以通过“抽象约束、封装变化”来实现开闭原则,即通过接口或者抽象类为软件实体定义一个相对稳定的抽象层,而将相同的可变因素封装在相同的具体实现类中。
2、Liskov替换原则(LSP)
- 子类必须能够替换它们的基类(IS-A)
- 继承表达类型抽象
如果不满足这个原则表明两者不是子类父类的关系。
也就是说:
- 任何基类可以出现的地方,子类一定可以出现。
- 子类可以扩展父类的功能,但不能改变父类原有的功能。
正确的做法是:取消几维鸟原来的继承关系,定义鸟和几维鸟的更一般的父类,如动物类,它们都有奔跑的能力。几维鸟的飞行速度虽然为 0,但奔跑速度不为 0,可以计算出其奔跑 300 千米所要花费的时间。
3、依赖倒置原则(DIP)
- 高层模块(稳定)不应该依赖于低层模块(变化),二者都应该依赖于抽象(稳定)。
- 抽象不应该依赖实现细节(变化),实现细节应该依赖于抽象(稳定)。
上面的画图例子:
4、单一职责原则(SRP)
- 一个类应该仅仅只有一个引起它变化的原因。
- 变化的方向隐含着类的责任。
该原则提出对象不应该承担太多职责,如果一个对象承担了太多的职责,至少存在以下两个缺点:
- 一个职责的变化可能会削弱或者抑制这个类实现其他职责的能力;
- 当客户端需要该对象的某一个职责时,不得不将其他不需要的职责全都包含进来,从而造成冗余代码或代码的浪费。
5、接口隔离原则(ISP)
- 不应该强迫客户程序依赖它们不同的方法。
- 接口应该小而完备。
要为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:
- 单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
- 单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。
6、迪米特原则(LOD)
- 一个软件实体应当尽可能少地与其他实体发生作用。
也叫最少知识原则。如果两个类不彼此通信,那么这两个类就不应当直接地发生相互作用。如果其中一个类需要另一个类的某一个方法的话,可以通过第三者转发这个调用。
对于一个对象,其朋友包括如下几类:
- 当前对象 this
- 以参数形式传入到当前对象方法中的对象
- 当前对象的成员对象
- 若当前对象的成员你对象是一个集合,那么集合中的对象也都是朋友
- 当前对象所创建的对象
注意点
优先使用对象组合,而不是类继承
- 类继承通常为“白箱复用”,对象组合通常为“黑箱复用”。
- 继承在某种程度上破坏了封装性,子类父类耦合度高。
- 对象组合则只要求被组合的对象具有良好定义的接口,耦合度低。
继承应该是某某某继承于人类,人类继承于动物。
而不是某某某继承于他父母。
封装变化点
一侧变化一侧稳定。
- 使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良影响,从而实现层次间的松耦合。
针对接口编程
主要指的是业务类型
- 不将变量类型声明为某个特定的具体类,而是声明为某个某个接口。
- 客户程序无需获知对象的具体类型,只需要知道对象所具有的接口。
- 减少系统中各部分的依赖关系,从而实现“高内聚、松耦合”的类型实际方案。
总结
- 静态 -> 动态
- 早绑定 -> 晚绑定
- 继承 -> 组合
- 编译时依赖 -> 运行时依赖
- 紧耦合 -> 松耦合