设计模式——序言

重点:先言对设计模式进行介绍、介绍关于设计模式的七大原则。
【此条笔记有参考】b站设计模式网课

一、前言

1.什么是设计模式?

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

软件设计的固有复杂性、产品需求不断发生变化。

因此要求我们在设计软件时要使其具有足够的抵御变化的能力。(毕竟没人希望天天删删改改同一份代码,这样就没空去做其他事♂了)

而如何解决着这种复杂性呢?

​ 并不是分而治之。由于不能掌握全部的复杂对象,我们选择忽略它们之间的非本质特征。然后去处理泛化以及理想化后的对象模型。简而言之,抽象。

两者的区别?

​ 假使让你设计一下画图:画一个矩形。你该如何设计?

普通的方法是:

  1. 先写一个point点类
  2. 然后写一个线类。
  3. 调用分别负责画矩形(圆形)或是直线的函数。
  4. 以下是实现。(不想看可以跳)
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...)//长宽
}

更贴合设计模式的写法是:

  1. 先设计一个shape类,类中有画这个图形的成员函数
  2. 让线、矩形类继承自它。
  3. 成功利用多态画图!
//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(...);
} 

假如说:现在要你在此基础上再画圆呢?

  1. 对第一种,你要重写圆类、改变Mainform中的操作函数
  2. 对第二种,你只需要重写圆类即可。

假如说:添加更多的图形呢?

​ 你将发现——对于第二份代码,它的复用性得到了极大提升(相比第一份来说)。不仅仅是代码量降低变得易读,还有更多的好处。

———————————————————————————

二、面向对象设计原则

对于面向对象的理解:

​ 从宏观来看,面向对象更能适应软件的变化,将这种变化所带来的影响减为最小。而从微观来看,则会更强调各个类的“责任”。

七大设计原则:

依赖倒置原则抽象不应该依赖细节,细节应该依赖抽象;高层模块不应该依赖低层模块,二者应该都依赖于抽象。
开放封闭原则对扩展开放、对更改封闭;类模块可扩展但是不可修改。
单一职责原则类的职责要单一,应该仅有一个引起变化的原因。
Liskov替换原则子类必须能够替换它们的基类;继承表达类型抽象。
接口隔离原则接口应该小而完备。
合成复用原则优先使用对象组合,而不是类继承。
迪米特法则一个软件实体对其他实体的引用越少越好。
(一)、依赖倒置原则

​ 针对抽象层编程,而不要针对具体类编程。

​ 对于高层的模块,我们可能更多地要使用抽象(比如抽象类),使其依赖于抽象。(上面的例子1说,如果在Mainform内使用了子类,实际上是让高层的Mainform依赖着不稳定的东西,所以不能隔离变化哩)

​ 依赖倒置原则的主要作用如下。

  1. 依赖倒置原则可以降低类间的耦合性。

  2. 依赖倒置原则可以提高系统的稳定性。

  3. 依赖倒置原则可以减少并行开发引起的风险。

  4. 依赖倒置原则可以提高代码的可读性和可维护性。

(二)、开放封闭原则

​ 软件实体对扩展是开放的,但对修改是关闭的,即在不修改一个软件实体的基础上去扩展其功能。以扩展的方式面对变化。

​ 开闭原则的作用如下。

  1. 对软件测试的影响:软件遵守开闭原则的话,软件测试时只需要对扩展的代码进行测试就可以了,因为原有的测试代码仍然能够正常运行。

  2. 可以提高代码的可复用性:粒度越小,被复用的可能性就越大;在面向对象的程序设计中,根据原子和抽象编程可以提高代码的可复用性。

  3. 可以提高软件的可维护性:遵守开闭原则的软件,其稳定性高和延续性强,从而易于扩展和维护。

(三)、单一职责原则

​ 类的职责要单一,不能将太多的职责放在一个类中。单一职责原则的核心就是控制类的粒度大小、将对象解耦、提高其内聚性。

​ 如果遵循单一职责原则将有以下优点。

  1. 降低类的复杂度。一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多。

  2. 提高类的可读性。复杂性降低,自然其可读性会提高。

  3. 提高系统的可维护性。可读性提高,那自然更容易维护了。

  4. 变更引起的风险降低。变更是必然的,如果单一职责原则遵守得好,当修改一个功能时,可以显著降低对其他功能的影响。

(四)、里氏代换原则

​ 子类必须能够替换它们的基类;继承表达类型抽象。

什么是替换?

​ 替换的前提是面向对象语言所支持的多态特性,同一个行为具有多个不同表现形式或形态的能力。(例子 ):JDK的集合框架,List接口的定义为有序集合,List接口有多个派生类,比如大家耳熟能详的ArrayList, LinkedList。那当某个方法参数或变量是List接口类型时,既可以是ArrayList的实现, 也可以是LinkedList的实现,这就是替换。

什么是与期望行为一致的替换?

​ 在不了解子类的情况下,仅通过接口或基类的方法,即可清楚的知道方法的行为,而不管哪种子类的实现,都与接口或基类方法的期望行为一致。或者说接口或基类的方法是一种契约,使用方按照这个契约来使用,子类也按照这个契约来实现。这就是与期望行为一致的替换。

(五)、接口隔离原则

​ 接口应该小而完备。尽量不要写成public,而是客户程序使用他们,形成依赖。如果该接口发生了变化,那么相对应的客户程序也要进行改变。接口隔离原则是为了约束接口、降低类对接口的依赖性。

​ 遵循接口隔离原则有以下 5 个优点。

  1. 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。

  2. 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。

  3. 如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。

  4. 使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。

  5. 能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。

(六)、合成复用原则

​ 优先使用对象组合,而不是类继承。继承会在某种程度上破坏了封装性,子类父类耦合度较高(父类对子类暴露了很多的东西)。但是对于对象组合,只要求被组合的对象具有良好定义的接口,耦合度低。

​ 通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但它也存在以下缺点

  1. 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
  2. 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
  3. 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。

采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点

  1. 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。

  2. 新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。

  3. 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。

(七)、迪米特法则

一个软件实体对其他实体的引用越少越好,或者说如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,而是通过引入一个第三者发生间接交互。

从迪米特法则的定义和特点可知,它强调以下两点:

  1. 从依赖者的角度来说,只依赖应该依赖的对象。
  2. 从被依赖者的角度说,只暴露应该暴露的方法。

所以,在运用迪米特法则时要注意以下 6 点。

  1. 在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
  2. 在类的结构设计上,尽量降低类成员的访问权限。
  3. 在类的设计上,优先考虑将一个类设置成不变类。
  4. 在对其他类的引用上,将引用其他对象的次数降到最低。
  5. 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
  6. 谨慎使用序列化(Serializable)功能。
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值