里氏代换原则

里氏代换原则(LSP

面向对象设计的重要原则是创建抽象化,并且从抽象化导出具体化。具体化可以给出不同的版本,每个版本都给出不同的实现。

从抽象化到具体化的导出要是用继承关系和里氏代换原则。(Liskov Substitution Principle.(LY注:实现OCP原则(开闭原则)的关键步骤是抽象化,而继承是实现抽象方法的重要手段。里氏原则是对开闭原则的抽象化的具体步骤的补充。)

里氏代换原则由Barbara Liskov提出。

(LY注:Barbara Liskov,就职于麻省理工学院(MIT)计算机科学实验室。里氏代换原则出自她1988年发表的经典文章Data Abstraction and Hierarchy,含义大致如下:使用指向基类的指针或引用的函数,必须能够在不知道具体派生类对象类型的情况下使用它们.(FUNCTIONS THAT USE POINTERS OR REFERENCES TO BASE CLASSES MUST BE ABLE TO USE OBJECTS OF DERIVED CLASSES WITHOUT KNOWING IT.)。原文见后,这里需要如下的替换性质:若对于每一个类型S的对象o1,都存在一个类型T的对象o2,使得在所有针对T编写的程序P中,用o1替换o2后,程序P的行为功能不变,则S是T的子类型。(What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T. )。她刚刚获得了2004年度的冯·诺依曼奖。美国工程院和艺术科学院的双院士。并且在麻省理工担任软件程实验课程的教学。)

(LY注:LSP让我们得出一个非常重要的结论:一个模型,如果孤立的看,并不具有真正意义上的有效性。模型的有效性只能通过它的客户程序来表现。在考虑一个特定设计是否恰当时,不能完全孤立地来看这个解决方案。必须要根据该设计的使用者作出的合理假设来审视它。)

7.1 美猴王的智慧

一个例子,孙悟空勾生死簿。对猴类作的操作,对它的子类石猴和猕猴都适用。

7.2 什么是里氏代换原则

里氏代换原则

定义:如果对每一个类型为T1的对象O1,都有类型为T2 的对象O2,使得以T1定义的所有程序P在所有的对象O1都代换为O2时,程序P的行为没有变化,那么类型T2是类型T1的子类型。

即,一个软件实体如果使用的是一个基类的话,那么一定适用于其子类。而且它觉察不出基类对象和子类对象的区别。(LY注:此处我仍有些糊涂。这种多态的性质,应该是由后期绑定机制保证的。而后期绑定应该是对象中保存了某些类型信息吧。这样即使进行转型,仍然根据对象可以判断出正确的类型,每种语言的后期绑定不完全一致,但这应该是相同的。各语言具体的后期绑定机制的细节,有时间要了解一下。)

反过来的代换不成立

反之,如果一个软件实体使用的是一个子类的话,那么它不一定适用于基类。(LY注:因为子类往往是对基类的扩展,所以子类的接口可能会比基类宽。窄化是危险的。PS:“将某个object reference视为一个reference to base type”的动作叫做向上转型(upcasting)。)

Java语言对里氏代换的支持

Java语言会在编译期进行检查,假设子类中实现了一个在基类中声明的方法,如果基类中的访问权限是public,那么子类不能降低它的访问权限。

因为,如果有客户端程序调用父类的public方法,而用子类代替父类时,因为降低了访问权限,客户端程序不能再继续调用。这样就违反了里氏代换原则。

Java语言对里氏代换支持的局限

Java语言对里氏代换的支持是有局限的。

就象描述一个物体大小的量有精度和准确度两种属性。精度是指量的有效数字有多少位;准确度则是指这个量与真实的物体大小相符合到什么程度。一个量可以有很高的精度,但是却无法和真实的物体情况相符合。Java编译器能检查的,就像是精度一样,它无法检查这个量与真实物体的差距。

Java编译器不能检查一个系统在实现和商业逻辑上是否满足里氏代换法则。(LY注:语法上可以检查,语义上不能区分。)

一个典型的例子:正方形是不是长方形的子类。

7.3 里氏代换原则在设计模式中的体现

策略模式(Strategy

如果有一组算法,那么就将算法封装起来,使得它们可以互换。

客户端依赖于基类类型,而变量的真实类型则是具体策略类。这是具体策略焦色可以“即插即用”的关键。

合成模式(Composite

合成模式通过使用树结构描述整体与部分的关系,从而可以将单纯元素与符合元素同等看待。由于单纯元素和符合元素都是抽象元素角色的子类,因此两者都可以替代抽象元素出现在任何地方。

里氏代换原则是合成模式能够成立的基础。

代理模式(Proxy

代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。代理模式能够成立的关键,就在于代理模式与真实主题模式都是抽象主题角色的子类。客户端只知道抽象主题,而代理主题可以替代抽象主题出现在任何需要的地方,而将真实主题隐藏在幕后。

里氏代换原则是代理模式能够成立的基础。

7.4 墨子论“取譬”

《墨子》有大取小取两章。“取”是“取譬”的意思。用面向对象的语言来解释,“取譬”研究的就是类和类的实例。(LY注:觉得墨子是诸子中最喜欢物理的一位,让我想起亚里士多德。)

白马与马

《墨子 小取》中说,“白马,马也;乘白马,乘马也。骊马,马也;乘骊马,乘马也。”骊马是黑色的马。墨子说,不论黑马、白马均是马的一种。既然马可以骑,那么白马和黑马必可骑。

马是抽象的马,白马和黑马是马的具体子类。一个操作如果适用于马,必然适用于黑马和白马。基类可以出现的地方,一定可以替换为子类。

反过来的代换不成立

《墨子 小取》中说,“娣,美人也,爱娣,非爱美人也….盗,人也;恶盗,非恶人也。”妹妹虽然是美人,但喜欢妹妹并不代表喜欢美人。盗贼是人,但讨厌盗贼也并不代表就讨厌人类。

7.5 从代码重构的角度理解

当两个具体类AB之间的关系违反了里氏代换原则是,重构的方案有两种:

(1)       创建一个新的抽象类C,作为两个具体类的父类,将AB的共同行为移动到C中,从而解决AB行为不完全一致的问题。

(2)       B继承自A改为委派关系。(LY注:改为用组合实现,参看合成/聚合原则)

长方形和正方形

正方形是否是长方形的子类的问题,西方一个很著名的思辨题。

正确的写法是:

长方形类:两个属性,宽度和高度;正方形类:一个属性,边。

(LY注:这是至少流行了十年的思辨题目,最早来自于C++和Smalltalk领域。类似的这种思辨问题还有哪些呢?让我不禁对哲学又感冒起来了。查阅资料时意外找到了一个讨论区,里面有读者和作者关于此处的拓展讨论,真让人高兴。)

(LY注:书中没有提契约即Design by Contract的概念。子类应当完全继承父类的contract。《敏捷软件开发:原则、模式与实践》一书中这样写,"基于契约设计(Design By Constract),简称DBC"这项技术对LISKOV代换原则提供了支持.该项技术Bertrand Meyer伯特兰做过详细的介绍:使用DBC,类的编写者显式地规定针对该类的契约.客户代码的编写者可以通过该契约获悉可以依赖的行为方式.契约是通过每个方法声明的前置条件(preconditions)和后置条件(postconditions)来指定的.要使一个方法得以执行,前置条件必须为真.执行完毕后,该方法要保证后置条件为真.就是说,在重新声明派生类中的例程(routine)时,只能使用相等或者更弱的前置条件来替换原始的前置条件,只能使用相等或者更强的后置条件来替换原始的后置条件。  本书中长方形的Contract是width和height可以独立变化,这个contract在正方形中被破坏了。)

(LY注:注意,我们所有讨论的基础都应由类的行为决定。这使得长方形等类是动态的,而不是象现实生活中一样是静态的概念。)

 

正方形不可以作为长方形的子类

如果设定一个resize方法,一直增加长方形的宽度,直到增加到宽度超过高度才可以。

那么如果针对子类正方形的对象调用resize方法,这个方法会导致正方形的边不断地增加下去,直到溢出为止。换言之,里氏法则被破坏掉了。

这个例子很重要,它意味着里氏代换与通常的数学法则和生活常识有不可混淆的区别。

(LY 注:常识认为,正方形is a 长方形,而且是一类特殊的长方形。但是在这里出了问题,如果我们系统中不会有这样的resize操作,是否正方形就可以作为长方形的子类了呢?看后文是可以的)

代码的重构

长方形和正方形到底应该是什么关系呢?

它们应该都是四边形类的子类。四边形类中没有赋值方法,因类似上文的resize()方法不可能适用于四边形类,而只能只用于不同的具体子类长方形和正方形。因此里氏代换原则不会被破坏。(LY注:针对需要赋值操作的情况)

从抽象类继承

应尽量从抽象类继承,而不是从具体类继承。

上文对长方形和正方形的重构使用了重构的第一种方法。增加一个抽象类,让两个具体类都成为抽象类的子类。

记住一条指导性的原则,如果有一个由继承关系形成的等级结构的话,在等级结构树图上的所有树叶节点都应当是具体类;而所有的树枝节点都应当是抽象类或者Java接口。

问答题

1、  一个有名的思辨题,filename能不能作为string类的子类?

答:不能。Filename对象不能实现string对象的所有行为。比如两个string对象相加可以给出一个新的有效的string对象。而两个filename对象相加未必会得到一个新的有效的Filename对象。

       另外,Java中的String类型是final类型,因此不可以继承。

2、  如果正方形的边长不会发生改变,是否可以成为长方形的子类呢?(LY注:不变正方形,就是边长不会发生变化的正方形,也就是遵守不变模式的正方形。不变(Immutable)模式,一个对象在对象在创建之后就不再变化。)

答:可以。实现时,父类有两个属性宽度和高度。子类有三个属性宽度、高度和边。针对每一个属性,包含一个内部变量,一个Set值方法,一个Get值方法。子类正方形只需要将Set值方法不写任何语句即可。

3、  从里式代换角度看JavaPropertiesHashtable的关系是否合适?

答:不合适。在Java中,PropertiesHashtable的子类。显然Properties是一种特殊的Hashtable,它只接受string类型的键(Key)和值(Value)。但是,父类Hashtable可以接受任何类型的键和值。这意味着,在一些需要非String类型的键和值的地方,Properties不能取代Hashtable

(LY注:合成/聚合复用原则中有更详细的讨论,应使用合成/组合而不是继承。它们是has a的关系而不是is a的关系。)

 

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


什么是设计模式一套被反复使用、多数人知晓的、经过分类编目的、代码 设计经验 的总结;使用设计模式是为了 可重用 代码、让代码 更容易 被他人理解、保证代码 可靠性;设计模式使代码编制  真正工程化;设计模式使软件工程的 基石脉络, 如同大厦的结构一样;并不直接用来完成代码的编写,而是 描述 在各种不同情况下,要怎么解决问题的一种方案;能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免引
单一职责原则定义(Single Responsibility Principle,SRP)一个对象应该只包含 单一的职责,并且该职责被完整地封装在一个类中。Every  Object should have  a single responsibility, and that responsibility should be entirely encapsulated by t
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强烈推荐。原文截图*************************************************************************************************************************原文文本************
适配器模式将一个类的接口转换成客户期望的另一个接口,使得原本接口不兼容的类可以相互合作。
策略模式定义了一系列算法族,并封装在类中,它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,它是针对软件开发中经常遇到的一些设计问题,总结出来的一套通用的解决方案。
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
迭代器模式提供了一种方法,用于遍历集合对象中的元素,而又不暴露其内部的细节。
外观模式又叫门面模式,它提供了一个统一的(高层)接口,用来访问子系统中的一群接口,使得子系统更容易使用。
单例模式(Singleton Design Pattern)保证一个类只能有一个实例,并提供一个全局访问点。
组合模式可以将对象组合成树形结构来表示“整体-部分”的层次结构,使得客户可以用一致的方式处理个别对象和对象组合。
装饰者模式能够更灵活的,动态的给对象添加其它功能,而不需要修改任何现有的底层代码。
观察者模式(Observer Design Pattern)定义了对象之间的一对多依赖,当对象状态改变的时候,所有依赖者都会自动收到通知。
代理模式为对象提供一个代理,来控制对该对象的访问。代理模式在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。
工厂模式(Factory Design Pattern)可细分为三种,分别是简单工厂,工厂方法和抽象工厂,它们都是为了更好的创建对象。
状态模式允许对象在内部状态改变时,改变它的行为,对象看起来好像改变了它的类。
命令模式将请求封装为对象,能够支持请求的排队执行、记录日志、撤销等功能。
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。 基本介绍 **意图:**在不破坏封装性的前提下,捕获一个对象的内部状态,并在该
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为
享元模式(Flyweight Pattern)(轻量级)(共享元素)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结