为何OOP语言允许重写基类非抽象方法?是否违反里氏替换原则?
对重写非抽象方法与LSP相关疑问的澄清
为什么OOP语言要支持重写基类的非抽象方法
- 支撑模板方法类设计的落地。很多类的设计是先固化核心流程骨架,再开放可调整的扩展点。比如基类定义的订单结算方法是完整流程:计算商品金额、抵扣优惠、生成支付链接,其中“抵扣优惠”环节可以给一个无优惠的默认实现作为非抽象方法存在;子类针对会员场景、企业采购场景,只需要重写“抵扣优惠”这一个非抽象方法,就能复用整个结算流程,不需要把基类的整套结算逻辑重新实现,大幅减少重复代码。
- 满足无源码场景下的渐进式扩展。很多时候开发者会依赖第三方类库提供的基类,既无法修改基类源码把需要扩展的方法改成抽象方法,也没必要从零实现整套基类功能。比如类库提供的缓存基类默认把缓存存在内存里,业务需要把缓存存在Redis,就可以直接重写基类非抽象的
get/set方法,在保留基类所有缓存过期、容量淘汰逻辑的基础上,替换底层存储实现。 - 提供灵活的默认行为兜底。如果把所有可能被扩展的方法都设为抽象方法,就会强制所有子类必须重新实现一遍默认逻辑,造成大量无意义的冗余代码。允许重写非抽象方法的本质是给开发者选择权:不需要调整行为的子类直接继承使用默认实现,有特殊需求的子类再针对性重写对应方法。
关于“重写非抽象方法必然违反LSP”的认知偏差
这种认知确实存在明显的理解错误,核心问题是对LSP的定义做了过度窄化的解读:
里氏替换原则的核心要求,从来不是“派生类替换基类之后,行为必须和基类完全一模一样”——如果要求行为完全一致,多态机制本身就没有存在的意义了。LSP的本质是约束派生类必须严格遵守基类定义的方法契约,包括不能放大方法的前置条件、不能削弱方法的后置条件、不能破坏类的固有不变式。
那种“替换基类为派生类就改变了原有程序行为,就算违反LSP”的判断,完全把“行为兼容性”偷换成了“行为无差异”,和LSP的原本定义相去甚远。
真正会导致LSP违反的重写行为,从来不是“重写非抽象方法”这个动作本身,而是重写过程中破坏了基类的契约:
- 比如基类的
getUserById()方法约定“传入合法用户ID就返回对应的用户对象,不会抛出未声明的运行时异常”,结果派生类重写时遇到不存在的ID直接抛出未捕获的运行时错误,这就属于破坏契约,违反LSP。 - 比如基类的
addItem()方法约定“添加元素后容器的总长度会同步+1,不会出现长度和实际元素数不匹配的情况”,结果派生类重写时悄悄把重复元素拦截不添加,但长度计数依然+1,破坏了类的不变式,这也是违反LSP。
反过来,只要重写时严格对齐基类的方法契约,哪怕重写后的实现和基类的默认实现差异很大,也完全符合LSP要求。比如前面提到的把内存缓存改成Redis缓存的场景,只要重写后的get/set方法依然遵守“设置后可正常读取、过期时间到了自动失效、容量满了按规则淘汰”的契约,哪怕底层存储从内存换成了Redis,把基类对象全部替换成派生类对象,整个程序的预期行为完全不会被破坏,根本不存在违反LSP的问题。
语言层面允许重写非抽象方法,从来不是设计上的缺陷;真正的风险来自开发者重写时随意突破基类的契约边界。不少语言还专门提供了语法层面的辅助机制,比如C++的override关键字、PHP的#[Override]注解、Python的@typing.override装饰器,就是为了帮助开发者在重写时明确对齐基类的方法定义,减少无意的契约破坏。
内容的提问来源于stack exchange,提问作者legale
相关产品推荐
相关产品推荐

