论坛首页 Java企业应用论坛

LSP替换原则探讨

浏览 7776 次
精华帖 (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的方法违反了基类定下的契约。
   发表时间:2004-05-11  
怎么我看《java与模式》的时候觉得讲的很清楚啊?
也可能是我在此之前看过thinking in java的缘故吧

java与模式这本书对于OO的那几个基本原则讲述的很清楚,我觉得。
0 请登录后投票
   发表时间:2004-05-11  
呵呵,我也看过&lt;Thinking in java&gt; J
实际上,LSP原则表面上看起来容易理解,但是仔细体会,确实不是那么简单的。
我刚看&lt;java与模式&gt;时,对于软件设计的几个原则自认为弄懂了,但看了&lt;敏捷…..&gt;一书一后,才彻底弄明白了。
&lt;java 与模式&gt;至少没有讲清楚两点:
1. 没有讲清楚“程序没有符合预期的行为才叫行为功能改变”,这就意味着,并不是简单的“只要子类型必须能够替换掉它们的基类型”就符合LSP原则。什么叫“符合预期”?和客户需求有关?(一和客户需求相关,问题就大了)?和什么约定有关么?实际上,是和某个约定有关。也就是DBC。
2. &lt;java 与模式&gt;没有讲DBC。实际上,某些语言,比如Eiffel,对DBC是支持的,但是,C++和JAVA没有此特性。

学习这些模式,要仔细思考,才能有更深入的体会。比如DIP原则(依赖倒置原则),看看原理很简单,但仔细体会,纯的IOC就完全符合DIP原则。另外,ISP原则(接口隔离原则),在某些情况下,会说明在设计中多重继承的必要性(C++中是多重继承,在JAVA中是实现多个接口)。有关上面的论述,如果各位网友感兴趣,我可以写“序”。:)
0 请登录后投票
   发表时间:2004-05-11  
startunix 写道
呵呵,我也看过&lt;Thinking in java&gt; J
实际上,LSP原则表面上看起来容易理解,但是仔细体会,确实不是那么简单的。
我刚看&lt;java与模式&gt;时,对于软件设计的几个原则自认为弄懂了,但看了&lt;敏捷…..&gt;一书一后,才彻底弄明白了。
&lt;java 与模式&gt;至少没有讲清楚两点:
1. 没有讲清楚“程序没有符合预期的行为才叫行为功能改变”,这就意味着,并不是简单的“只要子类型必须能够替换掉它们的基类型”就符合LSP原则。什么叫“符合预期”?和客户需求有关?(一和客户需求相关,问题就大了)?和什么约定有关么?实际上,是和某个约定有关。也就是DBC。
2. &lt;java 与模式&gt;没有讲DBC。实际上,某些语言,比如Eiffel,对DBC是支持的,但是,C++和JAVA没有此特性。

学习这些模式,要仔细思考,才能有更深入的体会。比如DIP原则(依赖倒置原则),看看原理很简单,但仔细体会,纯的IOC就完全符合DIP原则。另外,ISP原则(接口隔离原则),在某些情况下,会说明在设计中多重继承的必要性(C++中是多重继承,在JAVA中是实现多个接口)。有关上面的论述,如果各位网友感兴趣,我可以写“序”。:)


首先,我不认为这个LSP原则有多难理解,它本来就很简单,没有必要复杂化它。其次,我没看明白你说的1是什么意思,我想就算不用DBC,比如你说的java C++ ,也不妨碍它们运用这些OO思想。
这些OO所谓的原则,本来就是一个很简单的抽象原则,正因为抽象和简单,所以才难以权衡和把握,难的是实施的时候在不同的场景下去遵守这些原则。
《敏捷。。。》(Robert Martin的那本)那本书我只是粗略的翻阅了一遍,设计模式部分没有细看 ,所以对这部分不好说什么。
不过对你所说的新的理解和认识,我愿闻其详。
0 请登录后投票
   发表时间:2004-05-13  
《开发高手》第二期上面 徐锋 《大话LSP设计原则》将这个LSP做的解释就很好。
设计要依赖于具体的环境,和当时的要求。
子类需要具备父类所有的属性和行为。
0 请登录后投票
   发表时间:2004-05-19  
我觉得Robert Martin的那本书写的很明确!
0 请登录后投票
   发表时间: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方法,那么长方形的用户就不会对正方形产生那样的“合理假设”
0 请登录后投票
   发表时间:2004-05-22  
Robert Martin的&lt;敏捷软件开发 原则、模式与实践&gt;中,
确实已经将了很明确了。

LSP 清楚地指出,OOD中的IS-A 关系是就行为方式而言的。
IS-A 是关于行为的。

所以说,这个Square, Rectangle之间是否可以extend的关系,不是
个数学上的关系,主要要看2个类的行为。

楼上的兄弟要是把类的行为都改了,那和原来的例子就是2码事情了。
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics