开闭原则(Open-Closed Principle, OCP),一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。
开闭原则定义中,软件实体可以指一个软件模块,一个由多个类组成的局部结构或一个独立的类。
任何软件都要面临一个问题,它们的需求会随时间的推移而发生变化。当软件系统需要面对新的需求时,应该尽量保证系统的设计框架是稳定的。
如果一个软件符合开闭原则,那么可以非常方便地对系统进行扩展,而且扩展时无须修改现有代码,使得软件系统适应性和灵活性的同时具备较好的稳定性和延续性。
为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键。一般面向对象编程语言中提供了接口、抽象类等机制,通过定义系统的抽象层,再通过具体类来进行扩展。**如果需要修改系统的行为,则无须对抽象层进行任何改动,只需要增加新的具体类来实现新的业务功能即可。**这样子实现在不修改已有代码的基础上扩展系统的功能,达到开闭原则的要求。
......
if (type.equals("pie")) {
PieChart chart = new PieChart();
chart.display();
}
else if (type.equals("bar")) {
BarChart chart = new BarChart();
chart.display();
}
......
在该代码中,如果需要增加一个新的图表类,如折线图LineChart,则需要修改ChartDisplay类的display()方法的源代码,增加新的判断逻辑,违反了开闭原则。
现对该系统进行重构,使之符合开闭原则。具体做法如下:
增加一个抽象图表类AbstractChart,将各种具体图表类作为其子类;
ChartDisplay类针对抽象图表类进行编程,由客户端来决定使用哪种具体图表。
如果要增加一种新的图表,如折线图LineChart,只需要将LineChart也作为AbstractChart的子类,在客户端想ChartDisplay中注入一个LineChart对象即可,无须修改现有类库的源代码。
注意:因为xml和properties等格式的配置文件是纯文本文件,可以直接通过VI编辑器或记事本进行编辑,且无须编译,因此在软件开发中,一般不把对配置文件的修改认为是对系统源代码的修改。如果一个系统在扩展时只涉及到修改配置文件,而原有的Java代码或C#代码没有做任何修改,该系统即可认为是一个符合开闭原则的系统。
抽象约束:
抽象是对一组事物的通用描述,没有具体的实现,也就表示它可以由非常多的可能性,可以跟随需求的变化而变化。因此,通过接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放,其包含三层含义:
元数据控件模块行为
使用元数据来控制程序的行为,减少重复开发。什么是元数据?用于描述环境和数据的数据,通俗来说就是配置参数,参数可以从文件中获得,也可以从数据库中获得
制定项目章程
封装变化
所有引用基类(父类)的地方必须能透明地使用其子类对象。
也就是说,一个基类对象被替换成它的子类对象,程序将不会产生任何错误,反过来则不成立;如果给定一个子类对象,那么它不一定能够使用基类对象。(子类型必须能够替换掉他们的父类型)
举个例子:我喜欢动物,那么我一定喜欢猫;但是我喜欢猫,我不一定喜欢动物。
举个贴合的例子:假设有两个类,一个类为BaseClass,另一个是SubClass,并且SubClass是BaseClass的子类。如果存在一个方法可以调用BaseClass类型的基类对象base的话,如:method1(base),那么必然可以接收一个BaseClass类型的子类对象sub为参数,如:method1(sub)。反过来则不成立,如果一个方法能接受sub,如:method2(sub);那么一般不可以有method2(base),除非是重载的方法。
里氏代换原则是实现开闭原则的重要方式之一,使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
使用里氏代换原则需要注意的问题:
在Sunny软件公司开发的CRM系统中,客户(Customer)可以分为VIP客户(VIPCustomer)和普通客户(CommonCustomer)两类,系统需要提供一个发送Email的功能,原始设计方案如图1所示:
在对系统进行进一步分析后发现,无论是普通客户还是VIP客户,发送邮件的过程都是相同的,也就是说两个send()方法中的代码重复,而且在本系统中还将增加新类型的客户。为了让系统具有更好的扩展性,同时减少代码重复,使用里氏代换原则对其进行重构(如下)。
增加一个新的抽象客户类Customer,而将CommonCustomer和VIPCustomer类作为其子类,邮件发送类EmailSender类针对抽象客户类Customer编程,根据里氏代换原则,能够接受基类对象的地方必然能够接受子类对象,因此将EmailSender中的send()方法的参数类型改为Customer,如果需要增加新类型的客户,只需将其作为Customer类的子类即可。
里氏代换原则是实现开闭原则的重要方式之一。在本实例中,在传递参数时使用基类对象,除此以外,在定义成员变量、定义局部变量、确定方法返回类型时都可使用里氏代换原则。针对基类编程,在程序运行时再确定具体子类.
依赖倒置原则原话解释为抽象不应该依赖细节,细节应该依赖于抽象,直白地说就是针对接口编程,不要对实现编程。把PC电脑类比为大的软件系统,任何部件如CPU、内存、硬盘、显卡等都可以理解为程序封装的类或程序集。那么,无论主板、CPU、内存、硬盘都是在针对接口设计的,如果针对实现来设计,内存就要对应到具体的某个品牌的主板,那么出现换内存需要把主板也换了的尴尬。
具体定义:
什么叫高层依赖底层?
比如,项目大多要访问数据库,所以就把访问数据库的代码写成了函数,每次做新项目就去调用这些函数,这就叫做高层模块依赖底层模块。
为什么高层依赖底层不可行?
在做新项目时,发现业务逻辑高层模块都一样,但却希望使用不同的数据库或存储信息方式,这时就出现麻烦了。原来高层模块都是与底层的访问数据库绑定了,现在没办法复用高层模块。就像PC里如果CPU、内存、硬盘都需要依赖具体的主板,主板一坏,所有的部件都没用了。而如果不管高层模块还是底层模块,它都依赖于抽象,具体一点就是接口或抽象类,只要接口是稳定的,那么任何一个的更改都不用担心其他收到影响,这就使得无论高层模块还是底层模块都可以很容易被复用。
依赖倒置原则的目的是通过要面向接口的编程来降低类间的耦合性,所以我们在实际编程中只要遵循以下4点,就能在项目中满足这个规则。
使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
使用该接口的客户端仅需知道与之相关的方法即可。每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干。
接口隔离是为了强内聚、松耦合。在开发中,通常会先定义好需要开发的接口,并由各个实现类实现。但如果没有经过考虑和设计,就很可能造成一个接口中包含众多的接口方法,而这些接口并不一定在每一个类中都需要实现。这样的接口很难维护,也不易于扩展,每一次修改验证都有潜在的风险。
如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。
迪米特法则首先强调的前提是在类的结构设计上,每一个类都应当尽量降低成员的访问权限。也就是说,一个类包装好自己的private状态,不需要让别的类直到的字段或行为就不要公开。这就是封装的思想。面向对象的设计原则和面向对象的三大特性并不矛盾。
迪米特法则其根本思想,是强调了类之间的松耦合。类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成波及。也就是说,信息的隐藏促进了软件的复用!
一个新人初次到一所IT公司上岗需要联系IT部门分配电脑,不需要具体找到IT部门的某一个人进行操作,只需要找到一个第三者联系这个IT部门,从上往下的分配任务这样子效率才高。把IT部类比成抽象的,哪怕里面的人离职了换了新人,电脑出问题也可以找IT部门解决,而不需要认识其中的同事。
过度使用该法则会产生大量的中介类,从而增加系统的复杂性,使得模块之间通行效率降低。物极必反!!!
单一职责原则(SRP),就一个类而言,应该仅有一个引起它变化的原因。(一个类只负责一个功能领域中的相应职责)
假设一个类负责两个不同的职责:职责P1,职责P2 。由于职责P1需要发生改变而要修改类时,有可能会导致原本运行正常的职责P2功能发生故障。
也就时说,如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当发生变化的时,设计会遭到意想不到的破坏。
因此,要将这些职责进行分析,将不同的职责封装在不同的类中,即将不同的变化封装在不同的类中,如果多个职责总是同时发生变化则可将它们封装在同一个类中。
单一职责原则是实现强内聚、松耦合的指导方针,需要发现类的不同职责并将其分离。
/**
上面的的类图对应的接口入下
*/
public interface IPhone{
//拨通电话
public void dial(String phoneNumber);
//通话
public void chat(Object o);
//挂断电话
public void hangup();
}
仔细分析,该接口包含两个职责:一个时间协议管理、一个是数据传送。dial()和hangup()两个方法实现的是时间协议管理,分别是拨通和挂机;而chat()实现的是数据传送,把声音转换成模拟信号或者数字信号进行传递,然后把收到的信号进行解析。这里协议接通和数据传送的变化都会引起该接口或实现类的变化。
这两个职责相互影响吗?显然不是。因为协议接通只负责把电话接通或挂断,而数据传输协议只需要传输数据,这两个职责的变化并不会互相影响。于是这就可以考虑把这一个接口分成两个接口。
笼统地讲:是否需要拆分取决于变化。
当变化发生,只影响其中一个职责,那就需要拆分。
当变化发生,多个职责都被影响了,那就不需要拆分。
接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间耦合性,体现了封装思想,但两者是不同的:
本文参考资料:程杰老师的 《大话设计模式》 以及 CSDN博主「心猿意碼」的原创文章https://blog.csdn.net/yucaixiang/article/details/90239817