OCP:(开闭原则)可以在不必修改源代码的情况下改变这个模式的行为
用面向对象的语言来讲,不允许更改的是系统的抽象层,而允许扩展的是系统的实现层
抽象化是关键
对可变性的封装原则
里氏替换原则
里氏替换原则中说,任何基类可以出现的地方,子类一定可以出现。
里氏替换原则是对OCP的补充。实现OCP的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体体现,所以里氏替换原则是对实现抽象化的具体步骤的规范
一般而言,违反里氏替换原则的,也违背了OCP,反过来不一定成立
依赖倒转原则
依赖倒转原则讲的是,要依赖于抽象,不要依赖于实现
与OCP的关系是目标与手段之间的关系,OCP是目标,而达到这一目标的手段是依赖倒转原则
换言之,要想实现OCP,就应当坚持依赖倒转原则。违反依赖倒转原则,就不可能达到OCP的要求
合成/聚合复用原则
合成/聚合复用原则讲的是,要尽量使用合成/聚合,而不是继承关系达到复用的目的
显然,合成/聚合复用原则是与里氏替换原则相辅相成的,二者又都是对实现OCP原则的具体步骤的规范。前者要求设计师首先考虑合成/聚合关系,后者要求在使用继承关系时,必须确定这个关系是符合一定条件的
遵守合成/聚合复用原则实现OCP的必要条件,违反这一原则就无法使系统实现OCP这一目标
迪米特法则
迪米特法则讲的是,一个软件实体应当与尽可能少的其他实体发生相互作用
当一个系统面临功能扩展的时候,其中会有一些模块,他们需要修改的压力比其他一些模块要大,最后结果可能是这些模块需要修改或者不需要修改。但是不论是哪一种情况,如果这些模块是相对孤立的,那么它们就不会将修改的压力传递给其他模块
这就是说,一个遵守迪米特原则设计出来的系统在功能需要扩展时,会相对更容易地做到对修改的关闭。也就是说,迪米特法则是一条通向OCP的道路
接口隔离原则
接口隔离原则讲的是,应当为客户端提供尽可能小的单独的接口,而不是提供大的总接口
显然,接口隔离原则与广义的迪米特法则都是对一个软件实体与其他的软件实体的通信的限制。广义的迪米特法则要求尽可能限制通信的宽度和深度。接口隔离原则所限制的是通信的宽度,也就是说,通信应当尽可能地窄
显然,遵循接口隔离原则与迪米特法则,会使一个软件系统在功能扩展的过程中,不会将修改的压力传递到其他的对象
补充:
OCP(开闭原则)
定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
OCP的意思就是说,你设计的时候,时刻需要考虑,尽量让这个类是足够好,写好了就不要去修改了,如果新需求来,我们增加一些类就完事了,原来的代码能不动则不动。这个原则有两个特性,一个是说“对于扩展是开放的”,另一个是说“对于更改是封闭的”。面对需求,对程序的改动是通过增加新代码进行的,而不是更改现有的代码,这就是OCP的精神所在。简单一句经典的话来说:过去的事已成历史,是不可修改的,因为时光不可倒流,但现在或明天计划做什么,是可以自己决定(即扩展)的。
LSP(里氏代换原则)
定义1:如果对每一个类型为 T1 的对象 o1 ,都有类型为 T2 的对象 o2,使得以 T1 定义的所有程序 P 在所有对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
定义2:子类型必须能够替换掉它们的父类型。
描述:一个软件实体如果使用的是一个父类的话,那么一定适用于其子类,而且它察觉不出父类对象和子类对象的区别,也就是说,在软件里面,把父类都替换成它的子类,程序的行为没有变化
LSP通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原来的功能。它包含以下4层含义:
1.子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
2.子类可以增加自己特有的方法。
3.当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
4.当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
DIP(依赖倒转原则)
定义:高层模块不应该依赖底层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。即针对接口编程,不要针对实现编程
DIP基于这样一个试试:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。在java中,抽象是指接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。
DIP的中心思想是面向接口编程,传递依赖关系有三种方式,以上的说的是接口传递,另外还有两种传递方式:构造方法传递和setter方法传递,相信用过Spring框架的,对依赖的传递方式一定不会陌生。
在实际编程中,我们需要做到以下3点:
1.低层模块尽量都要有抽象类或接口,或者两者都有。
2.变量的声明类型尽量是抽象类或接口。
3.使用继承时遵循LSP。
总之,DIP就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒置。
ISP(接口隔离原则)
ISP的定义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要视图去建立一个很庞大的接口供所有依赖它的类去调用。在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的“契约”,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
与SRP的区别:
1.SRP注重的是职责;而ISP注重对接口依赖的隔离。
2.SRP主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而ISP主要约束接口,主要针对抽象,针对程序整体框架的构建。
采用ISP对接口进行约束时,要注意以下几点:
1.接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不争的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度
2.为依赖接口类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块定制服务,才能建立最小的依赖关系。
3.提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情
运用ISP原则,一定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。
组合/聚合复用原则
就是说要尽量的使用合成和聚合,而不是继承关系达到复用的目的
该原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分:新的对象通过向这些对象委派达到复用已有功能的目的。其实这里最终要的地方就是区分“has-a”和“is-a”的区别。相对于合成和聚合,继承的缺点在于:父类的方法全部暴露给子类。父类如果发生变化,子类也得发生变化。聚合的复用的时候就对另外的类依赖的比较的少。
合成/聚合复用优点:
新对象存取成分对象的唯一方法是通过成分对象的接口;这些复用是黑箱复用,因为成分对象的内部细节是新对象所看不到的;这种复用支持包装;这种复用所需的依赖较少;每一个新的类可以将焦点集中在一个任务上;这种复用可以在运行时动态进行,新对象可以使用合成/聚合关系将新的责任委派到合适的对象
合成/聚合复用的缺点:
通过这种方式复用建造的系统会有较多的对象需要管理。
继承复用优点:
新的实现较为容易,因为基类的大部分功能可以通过继承关系自动进入派生类;修改或扩展继承而来的实现较为容易。
继承复用缺点:
继承复用破坏包装,因为继承将基类的实现细节暴露给派生类,这种复用也称为白箱复用;如果基类的实现发生改变,那么派生类的实现也不得不发生改变;从基类继承而来的实现是静态的,不可能在运行时发生改变,不够灵活。
LOD(迪米特法则)
LOD其根本思想,是强调了类之间的松耦合,类之间的耦合越弱,越有利于复用,一个处于弱耦合的类被修改,不会对有关系的类造成影响,也就是说,信息的隐藏促进了软件的复用。
LOD还有一个更简单的定义:只与直接的朋友通信。何为直接朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。
一句话就是说:一个对象应该对其他对象保持最少的了解。
SRP(单一职责原则)
定义:不要存在多于一个导致类变更的原因。通俗地讲,即一个类只负责一项职责,应该仅有一个引起它变化的原因
遵循SRP的优点有:
1.可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;
2.提高类的可读性,提高系统的可维护性;
3.变更引起的风险降低,变更时必然的,如果SRP遵循的好,当修改一个功能时,可以显著降低对其他功能的影响。
需要说明的一点是SRP不只是面向对象编程思想所持有的,只要是模块化的程序设计,都需要遵循这一重要原则。