设计模式之六大原则

六大原则

开闭原则

定义

开闭原则(Open-Closed Principle, OCP),一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。

​ 开闭原则定义中,软件实体可以指一个软件模块,一个由多个类组成的局部结构或一个独立的类

​ 任何软件都要面临一个问题,它们的需求会随时间的推移而发生变化。当软件系统需要面对新的需求时,应该尽量保证系统的设计框架是稳定的。

​ 如果一个软件符合开闭原则,那么可以非常方便地对系统进行扩展,而且扩展时无须修改现有代码,使得软件系统适应性和灵活性的同时具备较好的稳定性和延续性。

​ 为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键。一般面向对象编程语言中提供了接口、抽象类等机制,通过定义系统的抽象层,再通过具体类来进行扩展。**如果需要修改系统的行为,则无须对抽象层进行任何改动,只需要增加新的具体类来实现新的业务功能即可。**这样子实现在不修改已有代码的基础上扩展系统的功能,达到开闭原则的要求。

举例设计模式之六大原则_第1张图片
......
if (type.equals("pie")) {
PieChart chart = new PieChart();
chart.display();
}
else if (type.equals("bar")) {
BarChart chart = new BarChart();
chart.display();
}
......

​ 在该代码中,如果需要增加一个新的图表类,如折线图LineChart,则需要修改ChartDisplay类的display()方法的源代码,增加新的判断逻辑,违反了开闭原则。

现对该系统进行重构,使之符合开闭原则。具体做法如下:

  1. 增加一个抽象图表类AbstractChart,将各种具体图表类作为其子类;

  2. ChartDisplay类针对抽象图表类进行编程,由客户端来决定使用哪种具体图表。

    设计模式之六大原则_第2张图片
    ​ 如果要增加一种新的图表,如折线图LineChart,只需要将LineChart也作为AbstractChart的子类,在客户端想ChartDisplay中注入一个LineChart对象即可,无须修改现有类库的源代码。

注意:因为xml和properties等格式的配置文件是纯文本文件,可以直接通过VI编辑器或记事本进行编辑,且无须编译,因此在软件开发中,一般不把对配置文件的修改认为是对系统源代码的修改。如果一个系统在扩展时只涉及到修改配置文件,而原有的Java代码或C#代码没有做任何修改,该系统即可认为是一个符合开闭原则的系统。

为什么使用开闭原则
  1. 只要是面向对象编程,在开发时都会强调开闭原则
  2. 开闭原则是最基础的设计原则,其他的五个设计原则都是开闭原则的具体形态。也就是说其他的五个开闭原则是指导设计的工具和方法,而开闭原则才是精髓;依照Java语言的称谓,开闭原则是抽象类,而其他的五个原则是具体的实现类。
  3. 开闭原则可以提高复用性。为什么要复用?复用可以减少代码的重复,避免相同的逻辑分散在多个角落,减少维护人员的工作量以及系统变化时产生异常的机会。怎么才能提高复用率?设计者需要缩小逻辑粒度,直到一个逻辑不可以分为止。
  4. 开闭原则可以提高维护性。
  5. 面向对象开发的要求。
如何使用开闭原则
  1. 抽象约束

    ​ 抽象是对一组事物的通用描述,没有具体的实现,也就表示它可以由非常多的可能性,可以跟随需求的变化而变化。因此,通过接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放,其包含三层含义:

    • 通过接口或抽象类约束扩散,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法
    • 参数类型,引用对象尽量使用接口或抽象类,而不是实现类,这主要是实现里氏替换原则的一个要求
    • 抽象层尽量保持稳定,一旦确定就不要修改
  2. 元数据控件模块行为

    使用元数据来控制程序的行为,减少重复开发。什么是元数据?用于描述环境和数据的数据,通俗来说就是配置参数,参数可以从文件中获得,也可以从数据库中获得

  3. 制定项目章程

  4. 封装变化

    • 将相同的变化封装到一个接口或抽象类中
    • 将不同的变化封装到不同的就扣或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。封装变化,也就是受保护的变化,找出预计有变化或不稳定的点,为这些变化点创建稳定的接口。

里氏代换原则

定义

所有引用基类(父类)的地方必须能透明地使用其子类对象

​ 也就是说,一个基类对象被替换成它的子类对象,程序将不会产生任何错误,反过来则不成立;如果给定一个子类对象,那么它不一定能够使用基类对象。(子类型必须能够替换掉他们的父类型)

​ 举个例子:我喜欢动物,那么我一定喜欢猫;但是我喜欢猫,我不一定喜欢动物。

​ 举个贴合的例子:假设有两个类,一个类为BaseClass,另一个是SubClass,并且SubClass是BaseClass的子类。如果存在一个方法可以调用BaseClass类型的基类对象base的话,如:method1(base),那么必然可以接收一个BaseClass类型的子类对象sub为参数,如:method1(sub)。反过来则不成立,如果一个方法能接受sub,如:method2(sub);那么一般不可以有method2(base),除非是重载的方法。

​ 里氏代换原则是实现开闭原则的重要方式之一,使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象

注意事项

​ 使用里氏代换原则需要注意的问题:

  1. 子类的所有方法必须在父类中声明,且子类必须实现父类中声明的所有方法。为了保证系统拓展性,在程序中通常使用父类来进行定义,如果一个方法仅存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法(不满足里氏代换原则)。
  2. 尽量把父类设计为抽象类或接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。里氏代换原则是开闭原则的具体实现手段之一。
举例

在Sunny软件公司开发的CRM系统中,客户(Customer)可以分为VIP客户(VIPCustomer)和普通客户(CommonCustomer)两类,系统需要提供一个发送Email的功能,原始设计方案如图1所示:
设计模式之六大原则_第3张图片
在对系统进行进一步分析后发现,无论是普通客户还是VIP客户,发送邮件的过程都是相同的,也就是说两个send()方法中的代码重复,而且在本系统中还将增加新类型的客户。为了让系统具有更好的扩展性,同时减少代码重复,使用里氏代换原则对其进行重构(如下)。
设计模式之六大原则_第4张图片
​ 增加一个新的抽象客户类Customer,而将CommonCustomer和VIPCustomer类作为其子类,邮件发送类EmailSender类针对抽象客户类Customer编程,根据里氏代换原则,能够接受基类对象的地方必然能够接受子类对象,因此将EmailSender中的send()方法的参数类型改为Customer,如果需要增加新类型的客户,只需将其作为Customer类的子类即可。

里氏代换原则是实现开闭原则的重要方式之一。在本实例中,在传递参数时使用基类对象,除此以外,在定义成员变量、定义局部变量、确定方法返回类型时都可使用里氏代换原则。针对基类编程,在程序运行时再确定具体子类.

依赖倒置原则

定义

依赖倒置原则原话解释为抽象不应该依赖细节,细节应该依赖于抽象,直白地说就是针对接口编程,不要对实现编程。把PC电脑类比为大的软件系统,任何部件如CPU、内存、硬盘、显卡等都可以理解为程序封装的类或程序集。那么,无论主板、CPU、内存、硬盘都是在针对接口设计的,如果针对实现来设计,内存就要对应到具体的某个品牌的主板,那么出现换内存需要把主板也换了的尴尬。

​ 具体定义:

  • 高层模块不应该依赖底层模块,两个都应该依赖抽象。
  • 抽象不应该依赖细节,细节应该依赖抽象。

什么叫高层依赖底层?

​ 比如,项目大多要访问数据库,所以就把访问数据库的代码写成了函数,每次做新项目就去调用这些函数,这就叫做高层模块依赖底层模块。

为什么高层依赖底层不可行?

​ 在做新项目时,发现业务逻辑高层模块都一样,但却希望使用不同的数据库或存储信息方式,这时就出现麻烦了。原来高层模块都是与底层的访问数据库绑定了,现在没办法复用高层模块。就像PC里如果CPU、内存、硬盘都需要依赖具体的主板,主板一坏,所有的部件都没用了。而如果不管高层模块还是底层模块,它都依赖于抽象,具体一点就是接口或抽象类,只要接口是稳定的,那么任何一个的更改都不用担心其他收到影响,这就使得无论高层模块还是底层模块都可以很容易被复用。

依赖倒置原则的作用
  1. 依赖倒置原则可以降低类间的耦合性
  2. 依赖倒置原则可以提高系统稳定性
  3. 依赖倒置原则可以减少并行开发引起的风险
  4. 依赖倒置原则可以提高代码的可读性和可维护性
依赖导致原则的实现方法

​ 依赖倒置原则的目的是通过要面向接口的编程来降低类间的耦合性,所以我们在实际编程中只要遵循以下4点,就能在项目中满足这个规则。

  1. 每个类尽量提供接口或抽象类,或者两者都具备
  2. 变量的声明类型尽量是接口或者是抽象类
  3. 任何类都不应该从具体类派生
  4. 使用继承时尽量遵循里氏代换原则

接口隔离原则

定义

​ 使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。

​ 使用该接口的客户端仅需知道与之相关的方法即可。每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干

​ 接口隔离是为了强内聚、松耦合。在开发中,通常会先定义好需要开发的接口,并由各个实现类实现。但如果没有经过考虑和设计,就很可能造成一个接口中包含众多的接口方法,而这些接口并不一定在每一个类中都需要实现。这样的接口很难维护,也不易于扩展,每一次修改验证都有潜在的风险。

优点
  1. 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
  2. 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
  3. 使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
  4. 能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。
实现方法
  1. 接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。
  2. 为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。
  3. 了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同深入了解业务逻辑。
  4. 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。

迪米特法则–最少知识原则

定义

​ 如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。

​ 迪米特法则首先强调的前提是在类的结构设计上,每一个类都应当尽量降低成员的访问权限。也就是说,一个类包装好自己的private状态,不需要让别的类直到的字段或行为就不要公开。这就是封装的思想。面向对象的设计原则和面向对象的三大特性并不矛盾。

迪米特法则其根本思想,是强调了类之间的松耦合。类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成波及。也就是说,信息的隐藏促进了软件的复用!

举例

​ 一个新人初次到一所IT公司上岗需要联系IT部门分配电脑,不需要具体找到IT部门的某一个人进行操作,只需要找到一个第三者联系这个IT部门,从上往下的分配任务这样子效率才高。把IT部类比成抽象的,哪怕里面的人离职了换了新人,电脑出问题也可以找IT部门解决,而不需要认识其中的同事。

优点
  1. 降低了类之间的耦合度,提高了模块的相对独立性
  2. 耦合度降低,从而提高了类的可复用率和系统的拓展性
缺点

​ 过度使用该法则会产生大量的中介类,从而增加系统的复杂性,使得模块之间通行效率降低。物极必反!!!

实现方法
  1. 从依赖者角度来说,只依赖应该依赖的对象。
  2. 从被依赖者的角度说,只暴露应该暴露的方法
注意事项
  1. 在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
  2. 在类的结构设计上,尽量降低类成员的访问权限
  3. 在类的设计上,优先考虑将一个类设置成不变类
  4. 在对其他类的引用上,将引用其他对象的次数降到最低
  5. 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。

单一职责原则

定义

​ 单一职责原则(SRP),就一个类而言,应该仅有一个引起它变化的原因。(一个类只负责一个功能领域中的相应职责)

​ 假设一个类负责两个不同的职责:职责P1,职责P2 。由于职责P1需要发生改变而要修改类时,有可能会导致原本运行正常的职责P2功能发生故障。

​ 也就时说,如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当发生变化的时,设计会遭到意想不到的破坏。

​ 因此,要将这些职责进行分析,将不同的职责封装在不同的类中,即将不同的变化封装在不同的类中,如果多个职责总是同时发生变化则可将它们封装在同一个类中。

​ 单一职责原则是实现强内聚、松耦合的指导方针,需要发现类的不同职责并将其分离。

举例
/**
上面的的类图对应的接口入下
*/
public interface IPhone{
  //拨通电话
  public void dial(String phoneNumber);
 
  //通话
  public void chat(Object o);
 
  //挂断电话
  public void hangup();
}

​ 仔细分析,该接口包含两个职责:一个时间协议管理、一个是数据传送。dial()和hangup()两个方法实现的是时间协议管理,分别是拨通和挂机;而chat()实现的是数据传送,把声音转换成模拟信号或者数字信号进行传递,然后把收到的信号进行解析。这里协议接通和数据传送的变化都会引起该接口或实现类的变化

​ 这两个职责相互影响吗?显然不是。因为协议接通只负责把电话接通或挂断,而数据传输协议只需要传输数据,这两个职责的变化并不会互相影响。于是这就可以考虑把这一个接口分成两个接口。

优点
  1. 降低类的复杂性。一个类只负责一项职责,其逻辑肯定要比负责多项职责简单多;
  2. 提高类的可读性,维护系统的稳定性
  3. 降低变更引起的风险。变更是必然的,但如果能遵守单一职责原则,当修改一个功能是,可以显著降低对其他功能的影响;
总结

笼统地讲:是否需要拆分取决于变化。

​ 当变化发生,只影响其中一个职责,那就需要拆分。

​ 当变化发生,多个职责都被影响了,那就不需要拆分。

​ 接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间耦合性,体现了封装思想,但两者是不同的:

  1. 单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
  2. 单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。

本文参考资料:程杰老师的 《大话设计模式》 以及 CSDN博主「心猿意碼」的原创文章https://blog.csdn.net/yucaixiang/article/details/90239817

你可能感兴趣的:(设计模式,设计模式,开发语言)