设计模式分类
从目的来看:
- 创建型模式——如何创建对象
- 结构型模式——如何实现类或对象的组合
- 行为型模式——类或对象怎样交互以及怎样分配职责
从范围来看:
- 类模式处理类与子类的静态关系——偏重于继承方案
- 对象模式处理对象间的动态关系——偏重于组合方案
从封装变化角度对模式分类:
组件协作模式 | 对象性能 | 数据结构 | 单一职责 |
接口隔离 | 对象创建 | 行为变化 | 状态变化 |
领域问题 | |||
重构关键技法
- 静态绑定->动态绑定
- 早绑定->晚绑定
- 继承->组合
- 编译时依赖->运行时依赖
- 紧耦合->松耦合
设计模式的终极目标——高内聚低耦合(高内聚:一个模块中的功能越少越好)
重新认识面相对象
从底层思维理解:
封装、继承、多态
从抽象思维理解:
1、面向对象可以隔离变化——将变化带来的影响减为最小
2、各司其责——面向对象更强调各个类的“责任”
3、对象是什么
- 从语言层面——封装了代码和数据
- 从规格层面——对象是一系列可悲使用的公共接口
- 从概念层面——对象是某种拥有责任的抽象
八大设计原则
单一职责原则
- 一个类应该仅有一个引起他变化的原因
- 变化的方向隐含着类的责任
类的职责单一,对外只提供一种功能,而引起类变化的原因都应该只有一个。
观察下面类,两个方法的返回值相同,后期接手该代码的人可能会因此将main函数中的函数调用全都改成cs.working()
class Clothes{
public:
void working(){
cout<<"working"<<endl;
}
void shopping(){
cout<<"working"<<endl;
}
};
int main(){
Clothes cs;
cs.working();
cs.shopping();
}
这就是不遵循单一职责原则带来的弊端。改进方式是涉及两个类,一个用于实现working,一个实现shopping
开闭原则
- 对扩展开放,对更改封闭
- 类模块应该是可扩展的,但是不可修改
类的改动是通过增加代码进行的,而不是修改源代码。
写一个类,如果写成如下形式,那么想增加其他功能就需要修改类的代码,一旦因为修改出现错误,会导致整个类无法使用
可以通过抽象类,对于每个子类实现一种功能
里氏替换原则
- 子类能够替换他们的基类(is a)
- 继承表达类型抽象
任何抽象类出现的地方都可以用他的实现类进行替换,实际就是虚拟机制,语言级别实现面相对象功能。
依赖倒置原则
依赖于抽象(接口),不要依赖集体的实现(类),也就是针对接口编程。
代码实现
//抽象层
class Car{
public:
virtual void run()=0;
};
class Driver{
public:
virtual void driver()=0;
};
//实现层
calss Benz:public Car{
public:
virtual void run(){
cout<<"benz run..."<<endl;
}
};
class BMW:public Car{
public:
virtual void run(){
cout<<"BMW run..."<<endl;
}
};
class Zhang3:public Driver{
public:
Zhang3(Car *car){
this->car=car;
}
virtual void drive(){
cout<<"Zhang3 run..."<<endl
}
private:
Car *car;
};
//业务逻辑实现,只依赖抽象层
int main(){
Car *benz=new Benz;
Driver *zhang3=new zhang3(benz);
zhang3->drive();
}
总结:
- 高层模块不应该依赖于低层模块,二者都应该依赖于抽象
- 抽象不应该依赖于实现细节,实现细节应该依赖于抽象
观察下列代码,分解的设计方法。这种设计如果有需求变动,会改动很多原本的代码。
类之间的结构如下(mainForm依赖于line和rect类,这里的变化就是line和rect,由于mainform依赖于变化,导致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 MainForm : public Form {
private:
Point p1;
Point p2;
//直接存储的是line对象和rect对象,因为此时我们不需要多态性。
//对比下面的那一块的代码因为需要多态性,所以存储的是指针
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);
};
抽象的设计方法——所有的图形类都继承他,抽象有利于统一处理
补充:析构函数设置成virtual,是为了正确的调用子类的析构函数
类之间的结构如下(mainForm依赖于shape类,line和rect也依赖于shape。这里的变化就是line和rect,我们通过抽象类shape进行隔离变化。下图中的mainform和shape是稳定的):
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);
};
接口隔离原则
- 不应该强迫客户程序依赖他们不用的方法(因为一旦产生依赖,就需要接口保持稳定)
- 接口应该小而完备
不应该强迫用户依赖他们不需要的接口方法。一个接口应该只提供一种对外功能,不应该把所有的操作都封装到一个接口中去。
合成复用原则
对象组合——即class A里面封装了class B,class B可以是对象指针,也可以是对象。
- 类继承通常为“”白箱复用“,对象组合通常为“黑箱复用””。
- 继承在某种程度上破坏了封装性,子类父类的耦合度高
- 对象组合则只要求被组合的对象具有良好定义的接口,耦合度低。
如果使用继承,会导致父类的任何变换都可能影响到子类的行为。如果使用对象组合,就降低了这种依赖关系。对于继承和组合,优先使用组合。
迪米特法则
一个对象应当对其他对象尽可能少的了解,从而降低对象之间的耦合,提高体统的可维护性。例如在一个程序中,各个模块之间互相调用时,通常会提供一个统一的接口来实现。这样其他模块不需要了解一个模块内部的实现细节(黑盒原理)
封装变化点
使用封装来创建对象之间的分界层,让设计者可以在一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合。