六大设计原则
a、单一职责原则
Single Responsibility Principle(SRP):主要讲述“接口”的单一职责,我们一直想要实现的都是“类”的单一职责。但是类本属于实体的抽象,好的抽象结合单一职责,一般会导致类过多。
b、里氏替换原则
Liskov Substitution Principle(LSP):所有引用基类的地方必须能透明地使用其子类的对象。但是反过来未必成立。
该原则主要讨论基类与子类之间的关系,即继承关系。继承可以实现代码共享,提高代码重用,代码的可扩展性,项目的开放性。但是继承是侵入性的,子类必须拥有父类的所有属性和方法,降低代码的灵活性,增强了耦合性(父类的修改要考虑子类的修改)。Java使用extends关键字来实现继承,其采用单一继承的规则;C++则采用了多重继承的规则,一个子类可以继承多个父类。
LSP包含四层含义:
1、子类必须完全实现父类的方法
在类中调用其他类,务必使用父类或接口。例子:士兵使用枪械,而枪械有多种。枪械为一个父类,而具体的手枪,步枪为其具体实现。在士兵类中调用枪械,则调用枪械父类,而具体传入则可以穿手枪对象或者步枪对象,这就是父类可以透明地被子类对象替换。
但是如果为玩具枪,则其不具备杀人的特点,即父类中的这一属性与子类本身不匹配。这就是如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。
2、子类可以有自己的个性
子类可以增加其特有的变量和方法,在调用子类的地方,不能够乡下转型(downcast),即子类出现的地方父类未必就可以出现。
3、覆盖或实现父类的方法时输入参数可以被放大
方法中的输入参数被称为前置条件,所谓前置条件就是你要让我执行,就必须满足我的条件;后置条件就是我执行完了需要反馈,标准是什么。
子类中的方法名与父类的方法名相同,但是传入参数类型不一样,不属于覆写(override),是重载(overload),不在同一个类中,但是也属于重载,因为父类中的方法在子类中都存在。例如:父类参数为HashMap,子类参数为Map,子类的输入参数范围扩大了,调用子类的该方法并传入HashMap,则编译器会选择继承于父类的方法来使用,而且这种情况必须要求子类的输入参数范围扩大。如果子类的输入参数范围缩小,即父类方法参数为Map,子类方法参数为HashMap,再重复上面的操作,此时会调用子类重载的方法……按照作者的意思,这就完蛋了,子类在没有覆写父类方法的前提下,居然调用了子类自己的方法,会引起业务逻辑混乱,即子类为实现类,传递一个实现类就会“歪曲”父类的意图,如上面枪的例子,枪存在杀人的功能,如果一个子类实现了同样的方法,功能确实求人,在参数范围缩小的情况下,传入较小范围的参数,就能导致最终调用子类的方法,从而引发混乱。正常想要调用子类的方法,则子类就需要覆写(重写)该方法,即方法名与参数都要与父类相同,只是功能上不同。
所以子类中方法的前提条件必须与基类中被覆写的方法的前置条件相同或者更宽松。
4、覆写或实现父类的方法时输出结果可以被缩小
即父类的一个方法的返回值是一个类型T,子类相同方法(重载或覆写)的返回值为S,里氏替换原则要求S必须小于等于T,即要么同类,要么S是T的子类。
总结:采用里氏替换原则的目的就是增加程序的健壮性,版本升级时也可以保持非常好的兼容性。即使增加子类,原来的子类还可以继续运行。在实际项目中,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类完成不同的业务逻辑,非常完美!在项目中,采用里氏替换原则,尽量避免子类的“个性”,一旦子类有“个性”,这个子类和父类的关系就很难调和,把子类当父类用,限制了其个性;子类单独作为一个业务来使用,则会让代码中间的耦合关系变复杂,缺乏类替换的标准。