企鹅不是鸟——类继承的误解

企鹅不是鸟——类继承的误解

Chris Yue No Comment
Posts

这个标题估计有点常识的看了都想打人,但对于学过面向对象编程的应该都知道是什么意思。这个问题所反映的是面向对象 5 大原则之一的 Liskov 替换原则(LSP),估计大家对 LSP 都能说上一两句。除了『企鹅不是鸟』,熟悉的还有『正方形不是矩形』、『圆不是椭圆』(数学老师哭晕在厕所……)。说归说,实际做不做得到还真不一定,起码我还是有自知之明的,很长一段时间都并没有在意。

对于 LSP 的掌握光靠知道『企鹅不是鸟』肯定是不够的,例子太明显,实际情况要复杂得多。我们可以对此做一个知识点掌握度测试:

猫是动物,动物需要吃食物获得能量,但猫作为食肉动物,只能吃肉。动物、猫、食物、肉,以及猫吃肉、动物吃食物,这些名词和动作,如果用面向对象来描述,最符合设计规范的方案是什么?

下面我会给出一个方案,不过在看方案之前,建议大家先自己想一下。

很多语言都有重载(overload)的功能,如果 Cat 的 eat 方法接收的是 Meat 类型的参数,那么父类的 eat(Food food) 方法也是依然能调用的,等同于说猫什么食物都能吃,出现逻辑上的问题。有些语言,比如 PHP,并没有重载,Cat 类这么写连编译也通过不了。

『自然而然』的我们能想到如下解决方案:

逻辑上的问题的确是解决了,但依然有两个小问题:

  1. 方法签名看不出来猫只能吃肉,代码不够『直白』,如果一个接口还需要深入到实现内部才能完全掌握其用法,基本可以断定接口本身还有再改进的空间
  2. 即使方法体检查如果食物不是肉就抛异常,不就等同于 eat(Food food)?其实就是一种自欺欺人的做法

所有这些问题其实都可以归结成一句话:因为这个设计不满足 Liskov 替换原则,所以『不够美』。之所以说上面原则违反了 LSP,就是因为 Cat 可能做不了 Animal 能做的事情。说到这里还记得 LSP 说的是什么吗?子类可以无脑替换所有父类用到的地方,其实它的另外一个意思是说,子类一定是比父类能力更强的存在,而不能更弱。从代码的角度来说,这不仅是子类的方法只能比父类多(即使企鹅覆盖鸟的 fly 方法抛异常,这种自欺欺人的方式也不算),也指子类继承的某个方法可以处理的参数范围只能比父类更大。之前介绍 PHP 新版本的改进里也提到过这个事情。

Cat 继承 Animal,只能说其符合『分类学』,却不符合『面向对象设计原则』,其实这也是看事情的角度的问题。从面向对象的角度看世界,有时候让人觉得很符合自然思维,但也可能让人难以接受。超人能做所有人能做的事情,比如当个记者啥的,所以超人也是人;但婴儿很多事情都不会,所以婴儿不是人……(可能西方人的确是这么想的,所以一般都用 it 来指代 baby……)

说到这里,可能大家会问了,那对于猫吃肉这个需求,更符合 OO 原则的设计是什么呢?好吧,其实我也不知道,我也想问同样的问题……但相对来说,我比较能接受的方案是『规则引擎』。

『规则引擎』是来源于游戏开发的概念,对于猫吃肉这件事,游戏里面类似的需求实在太多了,最典型的案例:战士不能用魔杖。而规则引擎则是把类似这种规则,从通过代码实现,改成通过数据(或者说配置)实现。可能对于整个游戏代码来说,都不会有 Warrior 或者 Wizzard 等按职业划分的 class,就一个 Player 类就行了,然后通过配置的方式,来描述这个 Player 是什么样的 Player,有什么样的能力。这么做最大的好处,或者说把代码描述规则变成数据描述规则的最大好处就是,此类问题变成了纯运营或者产品的问题,如果规则有变化让他们自己去改数据就好了(别误会,我并不赞同转嫁工作量这种事情,不过游戏设定的确应该算运营或者产品的范畴)。

插播一条广告:PHP M3U8 3 已经发布了,代码量更少,但因为设计更合理,支持的标签反而更多,而且添加新标签的支持也变得更简单,有很大因素是用了类似『规则引擎』的方式来定义数据类型转化的规则。

最后说一个我自己的经验:如果你的代码里有大量类似于 if (!xxx typeof Yyyy) throw new InvalidArgumentException 这样的代码,极大可能你已经违反 LSP 了,尝试换一种思维改掉它,可能会让你的代码更合理、优雅和更好维护。

企鹅不是鸟——类继承的误解 by Chris Yue is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

微信赞赏码

文章不错,我要帮站长分担建站费!
天使投赏人

发表评论

× one = 4