继承的缺点
最近在看项目里一些前人写的代码,其中大量的继承关系让我非常地头疼。
主要问题是,其中子类经常重写父类的方法,但是又不一定重写完全。这就导致在阅读代码的时候难度倍增。其中的一些原因包括,IDE无法每次都帮你精确定位到实际方法定义的位置。比如以下代码:
# python
class Stage():
def Run(self):
self.OnEnter()
def OnEnter(self):
# dosomething
pass
class MainTask(Stage):
def OnEnter(self):
# dosomething else
pass
maintask = MainTask()
maintask.Run()
定位maintask.Run()
的定义会定位到Stage.Run()
没错,因为MainTask没有重写这个方法。然后顺藤摸瓜,定位OnEnter()
,会定位到Stage.OnEnter()
,这是可以理解的,因为IDE无法知道具体执行的时候,是不是直接实例化Stage还是实例化MainTask,所以默认定位到最近的(如果本类也没有OnEnter定义,会继续往父类找)。显然在这个代码中,实际运行的MainTask.OnEnter()
的代码。
总结就是:继承会丢失某些信息,这部分信息只有在运行的时候才能知道。一旦继承关系变得复杂,重写关系变得复杂,那么其中如果出现错误,调试起来会变得很困难。
解决办法很简单,那就是尽量保持继承关系的简单,或者使用组合代替继承。关于对继承的痛斥,和具体的解决方案,可以参考这篇文章:Inheritance Is Evil. Stop Using It.。
大型源代码的阅读
通常来说,阅读源代码主要是做两件事情:1. 搞清楚调用关系,2. 搞清楚参数含义。要想弄明白这两件事情,最重要的手段的就是调试。当然,有一些工具可以静态分析代码帮你画出函数的调用关系图。但是这依然是不完全的。比如继承如果滥用,导致实际继承的对象要到运行时才能确定的话,那么静态分析工具就不能够分析出调用的是哪个类的哪个方法。
断点调试可以帮助解决问题。然而,想要成功运行一个大型项目往往是需要付出很多努力的。比如,复杂的构建环境;比如Unity的一款项目涉及到安卓的代码,比如要打包到安卓上运行安装包后才能知道结果。对于这种情况,如果能够将部分代码“拆出来”单独调试,也是可以的。