RSS

关于类继承与Liskov替换原则

24 Jul

续《When I click “object-oriented”…

派生类重写父类方法可能违反Liskov替换原则?

如果父类仅仅声明了抽象方法,而各派生类分别实现了该方法,那么就如同实现接口一样,可达到多态的目的;

如果父类实现了某方法,那么它对外所描述的行为也就确定了,派生类如果重写这个方法,那么就修改了这个行为,当派生类实例被父类型的引用使用时,表现出的行为与父类本身的实例不相符,即违反了Liskov substitution principle。

举个例子

现实中,正方形是矩形的一种特殊形式。

现在先有Rectangle作为父类,具有height和width两个property(有邪恶的getter/setter);再有Square继承Rectangle做子类,由于Square长与高相等,所以重写height和width的set方法为同时设置长与高。至此似乎是设计与现实完全相符,接着有人这样调用了:

void  tryAreaCalculation(Rectangle rectangle) {

     rectangle.setHeight(4);
     rectangle.setWidth(5);

     assertEquals(20, rectangle.area());
}

....

tryAreaCalculation(new Square());

结果assertion失败,“expect: 20, but was 25.” 也许我们会说,明知道传入的是Square对象还写那样的assertion是不可能出现的,但不清楚程序的人如果只看tryAreaCalculation方法,就会认为“矩形面积=长×高=4×5=20“是理所当然的。这显然不是我们想看到的事。

当然,这个例子并不是Liskov替换原则所针对问题的典型例子,在以“不择手段地复用代码”为目的的继承例子中(class Students extends List<Student>),大家更能明白Liskov替换原则的意义,以及对组合和继承的选择应首先去考虑面向对象的模型。

不要让OO的眼光被具体编程语言遮挡

是不是觉得我把类继承妖魔化了呢?有没有因为担心人类被官员类继承就不敢给人类添加“说话”的方法呢?不必这样。我们在用面向对象编程时要清楚,什么是从面向对象建模角度认为对的事,什么是所使用的编程语言能够做到的事。

如果你手上的是Java语言,发现官员会说话,人也会说话,官员又是人的一种,那么就在人类中实现“说话”这个方法,再让官员类继承人类(即便在未来可能会在官员类中重写说话方法,加入说空话的语句),这一切已是你能做到的合理的事了。但请心里清楚,这并不是使用OOP对待此模型的最正确方案,因为Java语言的限制。

如果编程语言有了如Mixin的特性(例如Ruby中的Mixin、Scala中的trait,下面以Ruby为例),那么你会发现在这个问题上你能有更好的解决方案。为了官员、教授等也是人的事实,我们依旧让这些类继承人类,但除了固有属性如身高、性别,以及固有行为如睡觉,不继承任何可变的因素;把可能共用的因素都单另放在Module中,之后在需要的类中Mixin进来,如为程序员、司机、中学生等类Mixin普通“说话”方法所在的Module,而在官员类中定义含说空话逻辑的“说话”方法。

总结下经验

  • 设计时的考虑,应首先尊重使用面向对象思想的建模,其次参照具体编程语言的特性,第三考虑与已有设计的配合和约定,必要时坚持正确的事去争取。
  • 用接口描述行为,各类通过实现接口来定义行为的具体内容;留意接口隔离(ISP)。
  • 用基类抽象固有属性和行为,减少public方法,合理使用Template method模式;子类(尽量)不去重写继承自基类的行为,只添加新属性和行为(LSP, OCP)。
  • 用组合描述以借用其他对象的行为为目的的设计。
 

About Wu Shaobo

@ThoughtWorks
Comments Off on 关于类继承与Liskov替换原则

Posted by in Uncategorized

 

Comments are closed.