重点:先言对设计模式进行介绍、介绍关于设计模式的七大原则。
【此条笔记有参考】b站设计模式网课
一、前言
1.什么是设计模式?
每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样你就能一次又一次地使用该方案而不必做做重复劳动。
软件设计的固有复杂性、产品需求不断发生变化。
因此要求我们在设计软件时要使其具有足够的抵御变化的能力。(毕竟没人希望天天删删改改同一份代码,这样就没空去做其他事♂了)
而如何解决着这种复杂性呢?
并不是分而治之。由于不能掌握全部的复杂对象,我们选择忽略它们之间的非本质特征。然后去处理泛化以及理想化后的对象模型。简而言之,抽象。
两者的区别?
假使让你设计一下画图:画一个矩形。你该如何设计?
普通的方法是:
- 先写一个point点类
- 然后写一个线类。
- 调用分别负责画矩形(圆形)或是直线的函数。
- 以下是实现。(不想看可以跳)
class Point{// 点类
public:
int x;
int y;
};
class Line{//线类
public:
Point start;//起点
Point end;//终点
//Line类构造函数
Line(const Point& start,const Point& end){this->start=start;this->end=end;}
};
class Rect{//矩形类
public:
Point leftup;
int width;
int height;
//Rect类构造函数
Rect(const Point& leftup,const int width,const int height){
this->leftup=leftup;
this->width;
this->height=height;
}
};
class Mainform{
private:
Point p1;
Point p2;
vector<Line> linevector;
vector<Rect> rectvector;
public:
Mainform(){...};//不重要的细节略去不表
protected:
virtual void OnMouseDown(...);//鼠标落下-取第一个点
virtual void OnMouseUp(...);//鼠标抬起-取第二个点-执行画图操作
virtual void OnPaint(...)//画图
}
void Mainform::OnMouseDown(...){//简化简化!
//取第一个点
p1.x=x;p1.y=y;
}
void Mainform::OnMouseUp(...){
//取第二个点
p2.x=x;p2.y=y;
//如果选择画线
if(rdoLine.checked){
Line Line(p1,p2);//建立线的实例
linevector.push_back(Line);//把它push_back到容器里
}
//如果选择画矩形
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);//把它push_back到容器里
}
//....略一些操作
this->Refresh();//刷新界面,画图<---
}
void Mainform::Onpaint(...){
//画Line
for(int i=0;i<linevector.size();i++)e.Graphics.DrawLine(Pens.red,linevector[i].start.x...);//两点坐标
//画矩形
for(int i=0;i<rectvector.size();i++)e.Graphics.DrawLine(Pens.red,rectvector[i].leftup...);//长宽
}
更贴合设计模式的写法是:
- 先设计一个shape类,类中有画这个图形的成员函数
- 让线、矩形类继承自它。
- 成功利用多态画图!
//Point 类不变,略
class Shape{
public:
virtual void Draw(...)=0;//纯虚函数
virtual ~Shape(){}//析构函数
}
class Line:public shape{
public:
Point start;//起点
Point end;//终点
//Line类构造函数
Line(const Point& start,const Point& end){this->start=start;this->end=end;}
//注意!这里!把画线函数挪到line类,使得自己
virtual void Draw(...){e.Graphics.DrawLine(Pens.red,start.x...);//两点坐标
};
class Mainform{
private:
Point p1;
Point p2;
vector<Shape*> shapevector; //父类指针指向子类,利于多态
public:
Mainform(){...};//不重要的细节略去不表
protected:
virtual void OnMouseDown(...);//鼠标落下-取第一个点
virtual void OnMouseUp(...);//鼠标抬起-取第二个点-执行画图操作
virtual void OnPaint(...)//画图
}
void Mainform::OnMouseDown(...){//简化简化!
//取第一个点
p1.x=x;p1.y=y;
}
void Mainform::OnMouseUp(...){
//取第二个点
p2.x=x;p2.y=y;
//如果选择画线--由于放的是指针,要new一下、把它push_back到容器里(堆对象的指针)
if(rdoLine.checked)shapevector.push_back(new Line(p1,p2));
//如果选择画矩形
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));//把它push_back到容器里
}
//....略一些操作
this->Refresh();//刷新界面,画图<---
}
void Mainform::Onpaint(...){
//画图--多态调用Draw:自行找子类中的Draw
for(int i=0;i<shapevector.size();i++)shapevector[i]->Draw(...);
}
假如说:现在要你在此基础上再画圆呢?
- 对第一种,你要重写圆类、改变Mainform中的操作函数
- 对第二种,你只需要重写圆类即可。
假如说:添加更多的图形呢?
你将发现——对于第二份代码,它的复用性得到了极大提升(相比第一份来说)。不仅仅是代码量降低变得易读,还有更多的好处。
———————————————————————————
二、面向对象设计原则
对于面向对象的理解:
从宏观来看,面向对象更能适应软件的变化,将这种变化所带来的影响减为最小。而从微观来看,则会更强调各个类的“责任”。
七大设计原则:
依赖倒置原则 | 抽象不应该依赖细节,细节应该依赖抽象;高层模块不应该依赖低层模块,二者应该都依赖于抽象。 |
---|---|
开放封闭原则 | 对扩展开放、对更改封闭;类模块可扩展但是不可修改。 |
单一职责原则 | 类的职责要单一,应该仅有一个引起变化的原因。 |
Liskov替换原则 | 子类必须能够替换它们的基类;继承表达类型抽象。 |
接口隔离原则 | 接口应该小而完备。 |
合成复用原则 | 优先使用对象组合,而不是类继承。 |
迪米特法则 | 一个软件实体对其他实体的引用越少越好。 |
(一)、依赖倒置原则
针对抽象层编程,而不要针对具体类编程。
对于高层的模块,我们可能更多地要使用抽象(比如抽象类),使其依赖于抽象。(上面的例子1说,如果在Mainform内使用了子类,实际上是让高层的Mainform依赖着不稳定的东西,所以不能隔离变化哩)
依赖倒置原则的主要作用如下。
-
依赖倒置原则可以降低类间的耦合性。
-
依赖倒置原则可以提高系统的稳定性。
-
依赖倒置原则可以减少并行开发引起的风险。
-
依赖倒置原则可以提高代码的可读性和可维护性。
(二)、开放封闭原则
软件实体对扩展是开放的,但对修改是关闭的,即在不修改一个软件实体的基础上去扩展其功能。以扩展的方式面对变化。
开闭原则的作用如下。
-
对软件测试的影响:软件遵守开闭原则的话,软件测试时只需要对扩展的代码进行测试就可以了,因为原有的测试代码仍然能够正常运行。
-
可以提高代码的可复用性:粒度越小,被复用的可能性就越大;在面向对象的程序设计中,根据原子和抽象编程可以提高代码的可复用性。
-
可以提高软件的可维护性:遵守开闭原则的软件,其稳定性高和延续性强,从而易于扩展和维护。
(三)、单一职责原则
类的职责要单一,不能将太多的职责放在一个类中。单一职责原则的核心就是控制类的粒度大小、将对象解耦、提高其内聚性。
如果遵循单一职责原则将有以下优点。
-
降低类的复杂度。一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多。
-
提高类的可读性。复杂性降低,自然其可读性会提高。
-
提高系统的可维护性。可读性提高,那自然更容易维护了。
-
变更引起的风险降低。变更是必然的,如果单一职责原则遵守得好,当修改一个功能时,可以显著降低对其他功能的影响。
(四)、里氏代换原则
子类必须能够替换它们的基类;继承表达类型抽象。
什么是替换?
替换的前提是面向对象语言所支持的多态特性,同一个行为具有多个不同表现形式或形态的能力。(例子 ):JDK的集合框架,List
接口的定义为有序集合,List
接口有多个派生类,比如大家耳熟能详的ArrayList
, LinkedList
。那当某个方法参数或变量是List
接口类型时,既可以是ArrayList
的实现, 也可以是LinkedList
的实现,这就是替换。
什么是与期望行为一致的替换?
在不了解子类的情况下,仅通过接口或基类的方法,即可清楚的知道方法的行为,而不管哪种子类的实现,都与接口或基类方法的期望行为一致。或者说接口或基类的方法是一种契约,使用方按照这个契约来使用,子类也按照这个契约来实现。这就是与期望行为一致的替换。
(五)、接口隔离原则
接口应该小而完备。尽量不要写成public,而是客户程序使用他们,形成依赖。如果该接口发生了变化,那么相对应的客户程序也要进行改变。接口隔离原则是为了约束接口、降低类对接口的依赖性。
遵循接口隔离原则有以下 5 个优点。
-
将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
-
接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
-
如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
-
使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
-
能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。
(六)、合成复用原则
优先使用对象组合,而不是类继承。继承会在某种程度上破坏了封装性,子类父类耦合度较高(父类对子类暴露了很多的东西)。但是对于对象组合,只要求被组合的对象具有良好定义的接口,耦合度低。
通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但它也存在以下缺点。
- 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
- 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
- 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点。
-
它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
-
新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
-
复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
(七)、迪米特法则
一个软件实体对其他实体的引用越少越好,或者说如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,而是通过引入一个第三者发生间接交互。
从迪米特法则的定义和特点可知,它强调以下两点:
- 从依赖者的角度来说,只依赖应该依赖的对象。
- 从被依赖者的角度说,只暴露应该暴露的方法。
所以,在运用迪米特法则时要注意以下 6 点。
- 在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
- 在类的结构设计上,尽量降低类成员的访问权限。
- 在类的设计上,优先考虑将一个类设置成不变类。
- 在对其他类的引用上,将引用其他对象的次数降到最低。
- 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
- 谨慎使用序列化(Serializable)功能。