混用Subtyping(子类型化)与Overriding(重写)是否属于不良实践?
我曾认为继承的主要优势是代码复用,但后来意识到通过子类型化实现的多态才是继承的核心优势,它让复杂系统的代码复用成为可能。不过我对子类型化与重写的关系存在诸多疑问:
- 子类型化原本就该与重写搭配使用吗?混用二者是否属于不良实践?
- 以下面的代码为例:
class Logger { public void count(string filepath) { Writeline("I'm counting lines of %s pretty hard!", filepath); } } class JSONLogger extends Logger { public override void count(string filepath) { Writeline("I'm counting JSON objects from file %s pretty hard!", filepath); } } class Shipment { private Logger logger; public Shipment (Logger logger) { this.logger = logger; } public void count(string filepath) { this.logger.count(filepath); } } shipment = new Shipment(new JSONLogger()); shipment.count("/my/shipment");
创建Shipment实例的用户需要了解该用Logger还是JSONLogger,甚至要查看实现才能决定行为,这是否违反抽象原则?
3. 若继承树更复杂,需要了解子类如何重写父类方法、父类是否重写基类方法等细节,会让子类型化或运行时动态替换对象变得困难吗?
4. 结合Barbara Liskov的LSP原则:
通过超类型方法使用时,子类型对象的行为应与超类型对象一致
按照这一规则,子类替换父类后系统行为应保持不变,但重写会提供父类方法的新实现,这难道不会改变行为吗?
子类型化与重写的本质关系
子类型化和重写并非“混用”,而是天然的配套关系:子类型化定义了类型兼容规则(子类可以被当作父类使用),重写则是实现行为多态的核心方式——让不同子类型在遵循父类契约的前提下,提供适配自身场景的具体实现。二者结合才是继承实现多态的核心逻辑,本身不是不良实践。
抽象原则的误解
你的顾虑点不在子类型化+重写,而在对象创建的职责划分。抽象原则要求的是Shipment只需依赖Logger的抽象契约(比如“统计文件内容并输出日志”),无需关心具体实现;而选择Logger还是JSONLogger,本就属于配置层或工厂类的职责,而非Shipment使用者的必要知识。如果让创建Shipment的用户直接选择具体日志器,这是职责分配错误,不是子类型化设计的问题。
复杂继承树的维护问题
如果复杂继承树导致替换对象困难,根源通常是违反了LSP或单一职责原则,而非重写本身。比如:
- 父类方法承担了过多职责,子类重写时不得不修改大量无关逻辑;
- 子类重写时破坏了父类的核心契约(比如返回值不符合预期、抛出父类未声明的异常)。
只要严格遵循LSP,子类重写是对父类行为的扩展而非破坏,此时即使继承树复杂,替换对象依然是安全的,无需深入了解所有重写细节。
LSP与重写的矛盾?不存在的
LSP要求的是行为契约的一致性,而非实现细节的完全相同。父类的方法定义了一份契约(比如“统计指定文件的内容,并输出对应日志”),子类重写时只要遵守这份契约,即使实现细节不同(比如统计行数 vs 统计JSON对象),也完全符合LSP。
举个反例:如果JSONLogger的count方法不做统计反而删除文件,这才违反LSP——它彻底破坏了父类“统计并日志”的核心契约。
总结:重写是子类型化实现多态的必要手段,本身不是糟糕设计;问题往往出在违反契约的重写或职责划分混乱上。
内容的提问来源于stack exchange,提问作者LancerHak

