- 一个类只负责一个功能领域中的相应职责;就一个类而言,应该只有一个引起它变化的原因。
- 单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计的是否优良,但“职责”和“变化原因”都是不可度量的,因项目而异,因环境而异。
- 接口的设计一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。
- 一个软件实体应该
对扩展开放,对修改关闭
。
-
开闭原则对扩展开放,对修改关闭,并不意味着不做任何修改,底层模块的变更,必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段。
-
如果一个系统在扩展时只涉及到修改配置文件,而原有的代码没有做任何修改,该系统即可认为是一个符合开闭原则的系统。
在面向对象的语言中,
继承
是必不可少的、非常优秀的语言机制优点:
代码共享,减少创建类的工作量,每个子类有拥有父类的属性和方法。
提高代码的重用性。
子类可以形似父类,但又已于父类。
提高代码的可扩展性,实现父类的方法就可以“为所欲为”类,君不见很多开源框架的扩展接口都是通过继承父类来完成的。
提高产品或项目的开放性。
缺点:
继承是侵入性的。只要继承就必须拥有父类的所有属性和方法。
降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了许多约束。
增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大段的代码需要重构。
Java使用
extends
关键字来实现继承,它采用了单一继承的规则。怎样才能让继承的“利”的因素发挥最大的作用,同时减少“弊”带来的麻烦呢?
解决方案就是引入里氏代换原则(Liskov Substitution Principle,LSP)
-
第一种:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的字类型。
-
第二种:
所有引用基类的地方必须能透明地使用子类的对象
。
里氏代换原则为良好的继承定义了一个规范,一句简单的定义包含了4层含义。
子类必须完全实现父类的方法
在类中调用其它类时务必要使用类或接口,否则,则说明类的设计已经违背了LSP原则。
如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。
子类可以有自己的个性
子类可以有自己的行为和外观,也就是方法和属性。
里氏代换原则可以正着用,不能反过来用。在子类出现的地方,父类未必就可以胜任。
覆盖或实现父类的方法时输入参数可以被放大
父类方法的输入参数是HashMap类型,子类方法的输入参数是Map类型,也就是说子类方法的输入参数的范围扩大了,子类代替父类传递到调用者中,子类的方法永远都不会执行,这是正确的。如果你想让子类的方法运行,就必须覆盖父类的方法。
覆盖或实现父类的方法时输出结果可以被缩小
父类方法的返回值是一个类型T,子类方法的返回值是一个类型S,那么里氏代换原则就要求S必须小于等于T,也就是说,要么S和T是同一种类型,要么S是T的子类,分两种情况:
如果是覆盖,父类和子类的同名方法的输入参数是相同的,参数的类型和范围也是相同的,这是覆盖的要求。
如果是重载,则要求方法的输入参数类型或数量不同,在里氏代换原则的要求下,就是子类的输入参数范围大于或等于父类的输入参数,也就是说你写的这个方法是不会被调用的,参考第3层含义。
子类的所有方法必须在父类中声明
,或子类必须完全实现父类中声明的方法。根据里氏代换原则,为了保证系统的扩展性,在程序中通常使用父类来进行定义,如果一个方法只存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法。- 我们在运用里氏代换原则时,
尽量把父类设计为抽象类或者接口
,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无需修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。 - Java语言中,在编译阶段,Java编译器会检查一个程序是否符合里氏代换原则,这是一个与实现无关、纯语法意义上的检查,但Java编译器的检查是有局限的。
- 高层模块不应该依赖低层模块,两者都应该依赖其抽象。
- 抽象不应该依赖细节。
- 细节应该依赖抽象。
- 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。
- 接口或抽象类不依赖于实现类。
- 实现类依赖接口或抽象类。
- 开闭原则、里氏代换原则和依赖倒转原则,在大多数情况下,这三个设计原则会同时出现。
开闭原则是目标,里氏代换原则是基础,依赖倒转原则是手段。
它们相辅相成,相互补充,目标一致,只是分析问题时所站角度不同。
- 使用多个专门的接口,而不是用单一的总接口,即
客户端不应该依赖那些它不需要的接口
。
-
根据接口隔离原则拆分接口时,首先必须满足单一职责原则。
-
在使用接口隔离原则时,我们需要注意控制接口的粒度,接口不能太小,如果太小会导致系统中接口泛滥,不利于维护;接口也不能太大,太大的接口将违背接口隔离原则,灵活性较差,使用起来很不方便。
迪米特法则也称为最少知识原则
(Least Knowledge Principle,LKP)
-
一个软件实体应当尽可能少地与其它实体发生相互作用。
-
通俗地讲,一个类应该对自己需要耦合或调用的类知道的最少。
-
迪米特法则的核心观念就是类间
解耦
,弱耦合。
-
只和朋友交流
朋友类
:出现在成员变量、方法的输入输出参数中的类称为朋友类,而出现在方法体内的类不属于朋友类。 -
朋友间也是有距离的
迪米特法则要求类“羞涩”一点,尽量不要对外公布太多的public方法和非静态的public变量,尽量收敛,多使用private、package-private、protected等访问权限。
-
是自己的就是自己的
如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中。
-
谨慎使用Serializable
合成复用原则又称为组合/聚合复用原则(Composition/Aggregate Reuse Principle,CARP)
- 尽量使用对象组合,而不是继承来达到复用的目的。
- 在面向对象设计中,可以通过两种方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承,但首先应该考虑使用组合/聚合,组合/聚合可以是系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要谨慎使用继承复用。
- 设计模式Java版
- 设计模式之禅