分享

软件设计原则----LisKov替换原则(LSP)

 just_person 2012-06-07

“一个软件实体如果使用的是一个基类的话,一定适用于其子类,而且根本不能觉察出基类对象和子类对象的区别。”

陈述:

  • 子类型(Subtype)必须能够替换他们的基类型(Basetype)
Barbara Liskov对原则的陈述:

若对每个类型S的对象o1,都存在一个类型T的对象o2,使得在所有针对T编写的程序P中,用o1替换o2后,程序P的行为功能不变,则S是T的子类型。

通俗地讲,就是子类型能够完全替换父类型,而不会让调用父类型的客户程序从行为上有任何改变。

意义:

我们在客户程序在调用某一个类时,实际上是对该类的整个继承体系设定了一套约束,继承体系中的所有类必须遵循这一约束,即前置条件和后置条件必须保持一致。这为对象继承加上了一把严格的枷锁。显然,LSP原则对于约束继承的泛滥具有重要意义。

分析:
  • 违反这个职责将导致程序的脆弱性和对OCP的违反
例如:基类Base,派生类Derived,派生类实例d,函数f(Base* p);
    • f(&d) 会导致错误
                  显然D对于f是脆弱的。
    • 如果我们试图编写一些测试,以保证把d传给f时可以使f具有正确的行为。那么这个测试违反了OCP——因为f无法对Base的所有派生类都是封闭的。
经典例子:长方形与正方形驳论

  1. class Rectangle  
  2. {  
  3. private:  
  4.      long width;  
  5.      long height;  
  6. public:  
  7.     void setWidth(long width)  
  8.     {  
  9.         this->width = width;  
  10.     }  
  11.     long getWidth()  
  12.     {  
  13.         return this->width;  
  14.     }  
  15.     void setHeight(long height)  
  16.     {  
  17.         this->height = height;  
  18.     }  
  19.     long getHeight()  
  20.     {  
  21.         return this->height;  
  22.     }  
  23. };  
  24.   
  25. //正方形类  
  26. class Square  
  27. {  
  28. private:  
  29.     long side;  
  30.       
  31. public:  
  32.     void setSide(long side)  
  33.     {  
  34.         this->side = side;  
  35.     }  
  36.     long getSide()  
  37.     {  
  38.         return side;  
  39.     }  
  40. };  
  1. //正方形类(如果继承自长方形类):  
  2. class Square : public Rectangle  
  3. {  
  4. private:  
  5.     long side;  
  6. public:  
  7.     void setWidth(long width)  
  8.     {  
  9.         setSide(width);  
  10.     }  
  11.     long getWidth()  
  12.     {  
  13.         return getSide();  
  14.     }  
  15.     void setHeight(long height)  
  16.     {  
  17.         setSide(height);  
  18.     }  
  19.     long getHeight()  
  20.     {  
  21.         return getSide();  
  22.     }  
  23.     long getSide()  
  24.     {  
  25.         return side;  
  26.     }  
  27.     void setSide(long side)  
  28.     {  
  29.         this->side = side;  
  30.     }  
  31. };  

  1. class SmartTest  
  2. {  
  3. public:  
  4.     void resize(Rectangle r)  
  5.     {  
  6.     while (r.getHeight() <= r.getWidth() )  
  7.         {  
  8.         r.setWidth(r.getWidth() + 1);  
  9.         }  
  10.     }  
  11. };  

从上面小函数可见,只想改变长方形的宽时,如果把正方形看成一种长方形的话,则正方形的长和宽都被改变了。LSP原则被破坏了,Square不应成为Rectangle的子类。
结论:
里氏代换与通常的数学法则和生活常识有不可混淆的区别。
考虑一个设计是否恰当时,不能孤立的看待并判断,应该从此设计的使用者所作出的假设来审视它!

思考:
这个看似明显正确的模型怎么会出错呢?
“正方形是一种长方形”
对不是SmartTest函数的编写者而言,正方形可以是长方形,但是对SmartTest函数的编写者而言,Square绝对不是Rectangle!!
OOD中对象之间是否存在IS-A关系,应该从行为的角度来看待。
->而行为可以依赖客户程序做出合理的假设。

改进:
引入一个Quadrangle(四边形)类,并将Rectangle 与Square变成它的具体子类,解决了Rectangle 与Square的关系不符合里氏替换原则的问题。
  1. class Quadrangle  
  2. {  
  3. public:  
  4.     virtual long getWidth() = 0;      
  5.     virtual long getHeight() = 0;  
  6. };  
 Quadrangle类只声明两个取值方法,不声明任何的赋值方法。
//长方形类:
  1. class Rectangle : public Quadrangle   
  2. {  
  3. private:  
  4.     long width;  
  5.     long height;  
  6.       
  7. public:  
  8.     void setWidth(long width)  
  9.     {  
  10.         this->width = width;  
  11.     }  
  12.     long getWidth()  
  13.     {  
  14.         return this->width;  
  15.     }  
  16.     void setHeight(long height)  
  17.     {  
  18.         this->height = height;  
  19.     }  
  20.     long getHeight()  
  21.     {  
  22.         return this->height;  
  23.     }  
  24. };  
//正方形类:
  1. class Square : public Quadrangle   
  2. {  
  3. private:  
  4.     long side;  
  5.       
  6. public:  
  7.     void setSide(long side)  
  8.     {  
  9.         this->side = side;  
  10.     }  
  11.       
  12.     long getSide()  
  13.     {  
  14.         return side;  
  15.     }  
  16.       
  17.     long getWidth()  
  18.     {  
  19.         return getSide();  
  20.     }  
  21.       
  22.     long getHeight()  
  23.     {  
  24.         return getSide();  
  25.     }  
  26. };  
问题如何得以避免?
基类Quadrangle类没有赋值方法,因此类似于 SmartTest的resize()方法不可能适用于Quadrangle类型,而只能适用于不同的具体子类Rectangle 和Square,因此里氏替换原则不可能被破坏。
结论:
  • 尽量从抽象类继承,而不从具体类继承。
  • 如果有两个具体类A和B有继承关系,那么一个最简单的修改方案应当是建立一个抽象类C,让类A和B成为抽象类C的子类。
  • 更进一步:  如果有一个由继承关系形成的等级结构的话,那么在等级结构的树图上面所有的树叶节点都应该是具体类,而所有的树枝节点都应该是抽象类或接口。

相应设计模式:
  • Strategy
  • Composite
  • Proxy 

参考资源:

《设计模式:可复用面向对象软件的基础》,ERICH GAMMA RICHARD HELM RALPH JOHNSON JOHN VLISSIDES著作,李英军 马晓星 蔡敏 刘建中译,机械工业出版社,2005.6

《敏捷软件开发:原则、模式与实践》,Robert C. Martin著,邓辉译,清华大学出版社,2003.9

《设计模式解析》,Alan Shalloway等著(徐言声译),人民邮电出版社,2006.10

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多