使用接口能否严格遵循Liskov's principle?
里氏替换原则(LSP)与接口实现的合理性
你的核心疑问可以直接给出答案:用接口实现完全符合里氏替换原则,LSP的核心并非要求父类型能实例化,而是子类型对父类型的行为可替换性。
先明确LSP的本质
里氏替换原则的核心定义是:子类型必须能够替换掉它们的父类型,且不改变原有程序的正确性。这里的“父类型”可以是普通类、抽象类,也可以是接口——判断是否符合LSP的关键,从来不是父类型能不能被实例化,而是:
- 依赖父类型的代码,在换成任意子类型实现后,依然能正常工作
- 子类型不会违反父类型定义的行为契约(比如不会抛出父类型方法不允许的异常,不会改变方法的预期输出)
分析你的接口实现案例
你给出的接口版本代码完全符合LSP:
interface Duck { fun quack() fun walk() } interface DuckThatCanSwim : Duck { fun swim() } class NormalDuck : DuckThatCanSwim { override fun swim() { println("The duck is swimming") } override fun quack() { println("The duck says quack") } override fun walk() { println("The duck is walking") } } class MetalDuck : Duck { override fun quack() { println("The duck says quack") } override fun walk() { println("The duck is walking") } }
假设我们有一段依赖Duck接口的代码:
fun interactWithDuck(duck: Duck) { duck.quack() duck.walk() }
不管传入NormalDuck还是MetalDuck,这段代码都能正常执行,行为完全符合预期——这就满足了LSP的核心要求。接口不能实例化根本不是问题,因为实际场景中我们本来就会传入它的子类型实现,而不是直接用接口本身。
对比类实现的差异
你用open class的实现确实符合LSP,但这只是LSP的一种实现方式,而非唯一方式。接口实现的优势在于:
- 更清晰地分离行为契约:
Duck定义“会叫、会走”的基础契约,DuckThatCanSwim扩展“会游泳”的附加契约,避免了类继承带来的强耦合 - 避免了类继承中可能出现的“契约污染”(比如最初案例中
MetalDuck被迫实现swim却抛出异常的问题)
关键误解纠正
LSP从未要求父类型必须能实例化。它关注的是子类型与父类型的行为兼容性:
- 子类型的方法前置条件不能比父类型更强
- 子类型的方法后置条件不能比父类型更弱
- 子类型必须保持父类型的所有不变量
只要满足这些行为要求,不管父类型是接口还是类,都符合LSP。
内容的提问来源于stack exchange,提问作者Ludiras
相关产品推荐
相关产品推荐

