设计模式之设计原则

一、设计模式原则

1. 开闭原则

  1. 开闭原则的意思是:对扩展开放,对修改关闭。开闭原则是编程中最基础、最重要的设计原则。
  2. 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而 不是通过修改 已有的代码来实现变化。
  3. C++实现扩展的方式
    • 在类中组合基类指针
    • 继承虚函数覆盖

2. 单一职责

  1. 一个类应该仅有一个引起它变化的原因;(一个类或者一个方法只负责一项职责,尽量做到类的只有一个行为原因引起变化)。
  2. 如类 A 负责两个不同职责:职责 1 ,职责 2 。如果当职责 1 需求变更而改变 A 时,可能造成职责 2 执行错误,所以需要将类 A 的粒度分解为A1 , A2。
  3. 单一职责原则的根本在于控制类的粒度大小 。

3. 里氏替换

  1. 里氏代换原则的根本,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常。(只要父类能出现的地方,其子类就可以出现,而且将父类替换为子类也不会产生任何错误或异常)。
  2. 子类型必须能够替换掉它的父类型;主要出现在子类覆盖父类实现,原来使用父类型的程序可能出现错误,因为子类覆盖了父类方法却没有实现父类方法的职责(子类覆盖了父类的方法并且改变了父类方法的原有功能逻辑);
    • 比如,父类原来传递来两个参数进行加法运算,子类覆盖后,进行减法运算,改变了父类方法的职责,导致子类替换父类时,出现错误。
  3. 如何满足里氏替换原则
    • 需要注意应该尽可能的将父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,这样可以做到满足开闭原则;
    • 子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法,也就是父类定义,子类实现
    • 子类不应该破坏父类的契约,也就是不能更改原有的方法的逻辑含义
  4. 里氏替换是继承复用的基石,只有当子类可以替换父类,且软件单位的功能不受到影响时,父类才能真正被复用,而子类也能够在基类的基础上增加新的行为。
  5. 里氏代换原则是对开闭原则的补充
    • 实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范

4. 接口隔离

  1. 不应该强迫客户依赖于它们不用的方法 (通过protected/private权限修饰符,控制其可见性,不允许客户使用其不能或者不希望使用的接口);
  2. 类之间依赖关系应该建立在最小的接口上
  3. 一般用于处理一个类拥有比较多的接口,而这些接口涉及到很多职责;简单理解:复杂的接口,根据业务拆分成多个简单接口;
  4. 接口的设计粒度越小,系统越灵活,但是灵活的同时结构复杂性提高,开发难度也会变大,维护性降低

5. 依赖倒置

  1. 高层模块不应该依赖低层模块,两者都应该依赖抽象接口
  2. 抽象接口不应该依赖具体实现,具体实现应该依赖于抽象接口
    依赖倒置
    • 自动驾驶系统公司是高层,汽车生产厂商为低层,它们不应该互相依赖,不然一方变动另一方也会跟着变动;而是应该抽象一个自动驾驶行业标准,高层和低层都依赖它;这样以来就解耦了两方的变动;
    • 自动驾驶系统、汽车生产厂商都是具体实现,它们应该都依赖自动驾驶行业标准(抽象);
  3. 依赖倒转原则的注意事项和细节
    • 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好 .
    • 变量的声明类型尽量是抽象类或接口 , 这样我们的变量引用和实际对象间,就存在一个缓冲层,利于程序扩展和优化
    • 继承时遵循里氏替换原则

6. 迪米特原则

  1. 又称最少知道原则,尽量降低类与类之间的耦合;一个对象应该对其他对象有最少的了解。
  2. 通俗地讲,对于被依赖类而言,无论实现逻辑多复杂,都尽量地将逻辑封装在内部,对外除了提供公有方法,不对外泄露任何信息。
  3. 迪米特法则还有另外一种解释:只和自己直接的朋友交流。首先,看一下什么是朋友和直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。
class Memcached;
class Demo
{    
    public function demo()
    {
        /* Memcached 以局部变量的形式出现在 Demo 类中,
        * 所以它不是 Demo 类的直接朋友,所以违背了迪米特原则。
        */
        ...
        Memcached* cache = new Memcached();
        cache->write('test', 'test123');
        cache->read('test');
        cache->delete('test');
        ...
    }
}
Demo demo = new Demo();
demo->demo();
  1. 遵循迪米特法则可以从以下几个设计思路入手:
    • 不要将依赖类以局部变量的形式在类中使用
    • 依赖类尽可能少地公布公有方法
    • 如果一个方法放在本类中,既不增加类间关系,也不对本类产生负面影响,那就放置在本类中

7. 合成复用原则

  1. 尽量使用合成/聚合的方式,而不是使用继承(组合优于继承,继承耦合度高,组合耦合度低);
  2. 复用一个类有两种常用形式,继承和组合。尽量使用对象组合,而不是继承来达到复用的目的,因为继承子类可以覆盖父类的方法,将细节暴露给子类,而且会建立强耦合关系,是一种静态关系,不能在运行时更改等等弊端。

二、设计模式原则总结

  1. 开闭原则是其他设计原则的精神领袖,其他设计原则是对开闭原则在不同角度的解释和具体实施方式。
  2. 各项设计原则并不是一个完全独立的个体,各设计原则相互之间存在是一定的关联关系。例如:
    • 比如单一职责原则与接口隔离原则,本质都是要职责专一类提供单一的功能的实现,接口不要有大而全的功能,约定职责专一就能降低耦合,就更有可能被复用;
    • 使用组合而不是继承,可以避免子类对父类的修改,这种情况也就符合了里氏替换原则,也就符合了开闭原则;
  3. 设计原则是软件开发过程中,前人以“高内聚,低耦合”、“提高复用性”、“提高可维护性”为根本目标;在实践中总结出来的经验,进而演化出来的具体的行为准则 ;设计模式则是符合设计规则的具体的类/接口的设计解决方案,也就是设计原则的具体化形式 。
  4. 一个设计良好的程序应该遵循的是设计原则,而并非一定是某个设计模式
  5. 所有的原则都是指导方针,而不是硬性规则,是在很多场景下一种优秀的解决方案,而并不是一成不变的。在实际的项目中,你既不能完全放弃使用继承,也可能让一个类完全不同“陌生人”讲话,也不可能子类完全不重写父类的方法。面向抽象进行编程,你也不可能让项目中所有的类都有抽象对应,这也是不可能的,也不能是被允许的。设计模式设计原则是经验之谈,当然是非常宝贵的经验,也是经过实践检验过的 ,但是最大的忌讳就是生搬硬套,矫枉过正,那将是最失败的设计模式的应用方式

三、如何学习设计模式

  1. 找稳定点和变化点,把变化点隔离出来
  2. 先满足设计原则,慢慢迭代出设计模式
  3. 培养计算机编程里两个最重要的思维:抽象思维、分治思维;其中,分治思维是指,将复杂的问题,通过分模块等形式化繁为简,分别处理。

四、类之间的关系

根据类与类之间的耦合度从弱到强排列,UML中的类图有以下几种关系:依赖关系,关联关系,聚合关系,组合关系,泛化关系和实现关系。其中,泛化和实现关系耦合度相等,是最强的。

1. 依赖

  • 与关联关系不同,依赖关系是一种临时性的关系,通常在运行期间产生,并且随着运行时的变化,依赖关系也有可能发生变化。
  • 依赖关系用一条带箭头的虚线表示。依赖关系也有方向,双向依赖是一种非常糟糕的结构,我们应该总是保持单向依赖,杜绝双向依赖的产生。
  • 在最终代码中,依赖关系体现为类构造方法及类方法的传入参数,箭头的指向为调用关系;依赖关系除了临时知道对方外,还“使用”对方的方法和属性;
    依赖关系
class MobilePhone
{
public:
   void transfer();
}
class Person
{
public:
   /*依赖关系体现为类构造方法及类方法的传入参数*/
   Person(MobilePhone mp);
   void fun()
   {
   	MobilePhone* mp = new MobilePhone;
   }
}	

2. 关联

  • 关联关系描述不同类的对象之间的结构关系;它是一种静态的关系,通常与运行状态无关,一般由常识等因素决定;它一般用来定义对象之间静态的、天然的结构;所以关联关系是一种"强关联"的关系。
  • 关联关系用一条实线表示。关联关系默认不强调方向,表示对象之间相互知道;如果特别强调方向,则表示只有其中一方知道。
  • 在最终代码中,关联对象通常是以成员变量的形式实现的
    关联关系
// 单向关联
class Teacher
{
public:
   void teach();
}
class Student
{
public:
   void study();
private:
   /*关联对象通常是以成员变量的形式实现的*/
   Teacher m_teacher;
}

// 双向关联
class Teacher
{
public:
   void teach();
private:
    /*关联对象通常是以成员变量的形式实现的*/
   list<Student> stus;
}
class Student
{
public:
   void study();
private:
   /*关联对象通常是以成员变量的形式实现的*/
   list<Teacher> teas;
}

3. 聚合

  • 聚合关系用于表示实体对象之间的关系,表示整体由部分构成的语义(has-a)。与组合关系不同的是,整体和部分不是强依赖的,即使整体不存在了,部分仍然存在。例如:一个部门由多个员工组成,部门撤销了,员工不会消失,他们仍然存在。
  • 聚合关系用一条带空心菱形箭头的实线表示,菱形指向整体。
  • 代码中,与关联关系一样,聚合关系也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而单独存在。比如学校和老师的关系,学校可以包含老师,但是如果学校停办了,老师依然存在。
    聚合关系

4. 组合

  • 组合关系同样表示整体由部分组成的语义(contains-a)。但组合关系是一种强依赖的特殊聚合关系,如果整体不存在了,则部分也不存在了。例如:公司由多个部门组成,如果公司不存在了,则部门也不存在了。
  • 组合关系用一条带实心菱形箭头的实线表示,菱形指向整体。
    组合关系

5. 泛化

  • 泛化关系表现为继承非抽象类
  • 泛化关系是对象之间耦合度最高的一种关系,表示一般与特殊的关系,是父类与子类之间关系,是一种继承关系,是is-a的关系
  • 泛化关系用一条带空心三角形箭头的实线表示,箭头指向父类
  • 在代码实现时,使用面向对象的继承非抽象类机制来实现泛化关系。
    泛化关系
class Person
{
public:
	void speak();
private:
	string name;
	int age;
}

/*使用面向对象的继承非抽象类机制来实现泛化关系*/
class Student : public Person
{
public:
	void study();
private:
	long studentNo;
}

/*使用面向对象的继承非抽象类机制来实现泛化关系*/
class Teacher : public Person
{
public:
	void teaching()
private:
	long teacherNo;
}

6. 实现

  • 实现关系表现为继承抽象类
  • 实现关系用一条带空心三角形箭头的虚线表示,箭头从实现类指向接口
    实现
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值