浏览 7775 次
锁定老帖子 主题:LSP替换原则探讨
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
|
|
---|---|
作者 | 正文 |
发表时间:2004-05-09
我很早就知道了OOD的一个重要原则是LSP,但当时苦于无法理解。记得当时看阎宏的<Java与模式>一书时,给我弄得很晕。但最近看了<敏捷开发方法>一书,才弄明白一点。希望和网友们一起探讨。 对于LSP的解释如下:子类型必须能够替换掉它们的基类型。而Iiskov在1988年下的定义为:如果每个类型S的对象o1,都存在个类型T的对象o2,使得在所有针对T编写的程序P中,用o1替换o2后,程序P行为功能不变,则S是T的子类型。 看看上面的定义,只有当程序P行为功能不变时,才符合LSP原则。但是,什么是“行为功能不变”,程序出错叫行为功能改变?程序没有符合预期的行为叫行为功能改变?我认为后者是正确的。 请看一个经典的问题:正方形是长方形的子类么? class Rectangle { public: virtual void SetWidth(double w); { itsWidth=w; } virtual void SetHeight(double h); { itsHeight=h; } double GetHeight(); const { return itsHeight; } double GetWidth(); const { return itsWidth; } private: Point itsTopLeft; double itsWidth; double itsHeight; }; class Square:public Rectangle { public: virtual void SetWidth(double w);; virtual void SetHeight(double h);; }; void Square::SetWidth(double w); { Rectangle::SetWidth(w);; Rectangle::SetHeight(w);; } void Square::SetHeight(double w); { Rectangle::SetWidth(w);; Rectangle::SetHeight(w);; } 上面这段代码看起来不错,如果有如下函数: void f(Rectangle& r); { r.SetWidth(32);; } 那么传给该函数的不管是Rectangle实例的地址,还是Square的地址,都可以满足需要。 但请看如下函数: void g(Rectangle& r); { r.SetWidth(5);; r.SetHeight(4);; assert(r.Area();==20);; } 注意函数的这行:assert(r.Area();==20);; 也就是说,该断言表明认为传入的一定是一个Rectangle对象的引用!但实际上,如果传入的是Square的应用,那么上面的断言就会失败!换句话书,对这个函数而言,Square不能够替换Rectangle!也就是他违反了Lisp原则。 有些人会对函数g中存在的问题争辩,他们认为函数g的编写者不能假设宽和长是独立变化的。G的编写者不会同意这种说法的。函数g以Rectangle作为参数。并且确实有一些不变性质(改变宽,不会改变长);和原理说明明显适用于Rectangle类,其中一个不变性质就是长和宽可以独立变化。G的编写者完全可以对这个不变性质进行断言。倒是Square的编写者违反了这个不变性。 从上所述,我们看出,LSP让我们得出一个重要的结论:在考虑一个特定设计是否恰当时,不能完全孤立地来看这个解决方案。必须要根据该设计的使用者所做出的合理假设来审视它。 但是很多开发人员可能对“合理假设”感到不安,怎样才能知道客户真正的要求呢?DBC(Design By Contract);就可以使这些合理的假设明确化,从而支持LSP。 DBC:类的编写者可以显式的规定者针对该类的契约,它通过为每个方法申明的前置条件和后置条件来决定。要使一个方法得以执行,前置条件必须为真,执行完毕后,该方法要保证后置条件为真。DBC原则为: 在重新申明派生类的例程时,只能使用相等或者更弱的前置条件来替换原始的前置条件,只能使用相等或者更强的后置条件来替换原始的后置条件。 这里的“弱”是指,如果X没有遵从Y的所有约束,那么X就比Y弱。X所遵从的新约束的数目是无关紧要的。 很明显,Rectangle::SetWidth(double w);的后置条件可以看作: asset((itsWidth==w); && (itsHeight == old.itsHeight););; 显然,Square::SetWidth(double w);的后置条件比Rectangle::SetWidth(double w);的后置条件弱,因而,Square的SetWidth的方法违反了基类定下的契约。 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2004-05-11
怎么我看《java与模式》的时候觉得讲的很清楚啊?
也可能是我在此之前看过thinking in java的缘故吧 java与模式这本书对于OO的那几个基本原则讲述的很清楚,我觉得。 |
|
返回顶楼 | |
发表时间:2004-05-11
呵呵,我也看过<Thinking in java> J
实际上,LSP原则表面上看起来容易理解,但是仔细体会,确实不是那么简单的。 我刚看<java与模式>时,对于软件设计的几个原则自认为弄懂了,但看了<敏捷…..>一书一后,才彻底弄明白了。 <java 与模式>至少没有讲清楚两点: 1. 没有讲清楚“程序没有符合预期的行为才叫行为功能改变”,这就意味着,并不是简单的“只要子类型必须能够替换掉它们的基类型”就符合LSP原则。什么叫“符合预期”?和客户需求有关?(一和客户需求相关,问题就大了)?和什么约定有关么?实际上,是和某个约定有关。也就是DBC。 2. <java 与模式>没有讲DBC。实际上,某些语言,比如Eiffel,对DBC是支持的,但是,C++和JAVA没有此特性。 学习这些模式,要仔细思考,才能有更深入的体会。比如DIP原则(依赖倒置原则),看看原理很简单,但仔细体会,纯的IOC就完全符合DIP原则。另外,ISP原则(接口隔离原则),在某些情况下,会说明在设计中多重继承的必要性(C++中是多重继承,在JAVA中是实现多个接口)。有关上面的论述,如果各位网友感兴趣,我可以写“序”。:) |
|
返回顶楼 | |
发表时间:2004-05-11
startunix 写道 呵呵,我也看过<Thinking in java> J
实际上,LSP原则表面上看起来容易理解,但是仔细体会,确实不是那么简单的。 我刚看<java与模式>时,对于软件设计的几个原则自认为弄懂了,但看了<敏捷…..>一书一后,才彻底弄明白了。 <java 与模式>至少没有讲清楚两点: 1. 没有讲清楚“程序没有符合预期的行为才叫行为功能改变”,这就意味着,并不是简单的“只要子类型必须能够替换掉它们的基类型”就符合LSP原则。什么叫“符合预期”?和客户需求有关?(一和客户需求相关,问题就大了)?和什么约定有关么?实际上,是和某个约定有关。也就是DBC。 2. <java 与模式>没有讲DBC。实际上,某些语言,比如Eiffel,对DBC是支持的,但是,C++和JAVA没有此特性。 学习这些模式,要仔细思考,才能有更深入的体会。比如DIP原则(依赖倒置原则),看看原理很简单,但仔细体会,纯的IOC就完全符合DIP原则。另外,ISP原则(接口隔离原则),在某些情况下,会说明在设计中多重继承的必要性(C++中是多重继承,在JAVA中是实现多个接口)。有关上面的论述,如果各位网友感兴趣,我可以写“序”。:) 首先,我不认为这个LSP原则有多难理解,它本来就很简单,没有必要复杂化它。其次,我没看明白你说的1是什么意思,我想就算不用DBC,比如你说的java C++ ,也不妨碍它们运用这些OO思想。 这些OO所谓的原则,本来就是一个很简单的抽象原则,正因为抽象和简单,所以才难以权衡和把握,难的是实施的时候在不同的场景下去遵守这些原则。 《敏捷。。。》(Robert Martin的那本)那本书我只是粗略的翻阅了一遍,设计模式部分没有细看 ,所以对这部分不好说什么。 不过对你所说的新的理解和认识,我愿闻其详。 |
|
返回顶楼 | |
发表时间:2004-05-13
《开发高手》第二期上面 徐锋 《大话LSP设计原则》将这个LSP做的解释就很好。
设计要依赖于具体的环境,和当时的要求。 子类需要具备父类所有的属性和行为。 |
|
返回顶楼 | |
发表时间:2004-05-19
我觉得Robert Martin的那本书写的很明确!
|
|
返回顶楼 | |
发表时间:2004-05-21
这个例子用来证明你的观点,力度不够强。
是你给客户提供的接口,造成了用户产生这样的“合理假设” 如果你的父类(长方形)给客户提供了setWidth和setHeight这样的方法,那么正方形就不是它的合适的子类 假如不提供那两个方法(比如,放到构建器里) 那么代码改成(请允许我用java): class Rectangle { //构建器,接受长,宽为参数 public Rectangle(double width,double height);{ this.width=width; this.height=height; } public double area();{ return width*height; } }; class Square extends Rectangle { //构建器,接受边长为参数 public Square(double sideLength);{ this.width=sideLength; this.height=sideLength; } }; 那么正方形与长方形只在“求面积”这个行为上是一致的,只从Rectangle那里继承了area方法,那么长方形的用户就不会对正方形产生那样的“合理假设” |
|
返回顶楼 | |
发表时间:2004-05-22
Robert Martin的<敏捷软件开发 原则、模式与实践>中,
确实已经将了很明确了。 LSP 清楚地指出,OOD中的IS-A 关系是就行为方式而言的。 IS-A 是关于行为的。 所以说,这个Square, Rectangle之间是否可以extend的关系,不是 个数学上的关系,主要要看2个类的行为。 楼上的兄弟要是把类的行为都改了,那和原来的例子就是2码事情了。 |
|
返回顶楼 | |