如何建模含共享代码的受限版本类?LSP相关继承问题咨询
嘿,这个问题我之前在项目里也遇到过——领域上DogShelter明明就是AnimalShelter的子类,但take方法的参数收缩(从Animal到Dog)刚好撞在**里氏替换原则(LSP)**的枪口上,直接继承就会污染API,确实头疼。结合你能修改现有代码的前提,我来拆解每个方案的利弊,再给你最优的建模建议:
方案1:直接子类继承(不推荐)
这个方案的唯一优点是能直接复用usefulMethod,但问题太致命:
- 继承来的
take(x: Animal)会留在DogShelter的公开API里,调用者完全可以传入一只Cat,这在领域逻辑上完全说不通 - 如果把继承的
take改成空实现或抛异常,等于放弃了静态类型语言的类型安全优势,要等到运行时才会暴露错误 - 代码示例:
class AnimalShelter { func usefulMethod() {} func take(x: Animal) {} } class DogShelter: AnimalShelter { var dogMedianCuteness: String = "normal (= very cute)" func take(x: Dog) {} }
// 致命问题:let shelter = DogShelter(); shelter.take(Cat()) 编译完全通过,但逻辑彻底错误
方案2:协议+协议扩展(适合快速复用,但领域语义弱)
这个方案绕开了继承的LSP问题,通过协议扩展共享usefulMethod的实现,两个类各自实现符合自身逻辑的take方法。但缺点也很明显:
- 领域语义表达不准确:
AnimalShelter和DogShelter本质是is-a的继承关系,而协议只能表达can-do的能力关系,没法体现“狗收容所是动物收容所的一种”这个核心语义 - 代码示例:
protocol UsefulThing { func usefulMethod() } extension UsefulThing { func usefulMethod() { // 这里写共享的实现逻辑 } } class AnimalShelter: UsefulThing { func take(x: Animal) {} } class DogShelter: UsefulThing { var dogMedianCuteness: String = "normal (= very cute)" func take(x: Dog) {} }
方案3:泛化父类,拆分原有AnimalShelter(兼顾语义和类型安全,推荐)
这是我最推荐的轻量改法,既保留了领域上的继承关系,又完美解决了LSP问题:
- 核心思路是把原来的
AnimalShelter拆成两层:- 原
AnimalShelter只保留共享的usefulMethod,变成一个抽象的基础父类(如果需要,可以标记为抽象类) - 新增
AnyAnimalShelter继承这个父类,实现take(x: Animal),完全替代原来AnimalShelter的所有使用场景 DogShelter同样继承基础父类,实现自己的take(x: Dog)
- 原
- 优势拉满:
- 完全符合领域语义:
DogShelter和AnyAnimalShelter都是AnimalShelter的子类,清晰表达is-a关系 - 类型安全:每个收容所的
take方法只接受对应类型的动物,不会出现API污染 - 零重复代码:
usefulMethod的逻辑完全复用,不需要在两个类里写两遍
- 完全符合领域语义:
- 代码示例:
class AnimalShelter { func usefulMethod() {} } class AnyAnimalShelter: AnimalShelter { func take(x: Animal) {} } class DogShelter: AnimalShelter { var dogMedianCuteness: String = "normal (= very cute)" func take(x: Dog) {} }
方案4:组合+泛型化父类(适合必须满足老板“保留继承”要求的场景)
如果老板咬死了要保留DogShelter继承AnimalShelter的写法,那可以通过泛型重构原有代码来解决,同时还能提升扩展性:
- 第一步:先给团队明确原
AnimalShelter的设计缺陷:原来的take(x: Animal)并不是“所有动物收容所的通用行为”,而是“接受任意动物的收容所”的行为,所以应该用泛型来抽象这个差异 - 第二步:把
AnimalShelter改成泛型类,子类可以指定具体的动物类型:
class AnimalShelter<T: Animal> { func usefulMethod() {} func take(x: T) {} } class DogShelter: AnimalShelter<Dog> { var dogMedianCuteness: String = "normal (= very cute)" // 可以直接复用父类的take(x: Dog),也能根据需要重写 } // 原来用AnimalShelter的地方,替换成AnimalShelter<Animal>即可
- 这样既满足了继承的要求,又完全符合LSP:
DogShelter是AnimalShelter<Dog>的子类,而AnimalShelter<Dog>和AnimalShelter<Animal>是不同的泛型实例,不存在子类替换父类的场景,自然不会违反LSP
最终选择建议
如果可以修改现有代码,优先选方案3,改动小、风险低,完全符合领域逻辑;如果未来可能需要扩展更多类型的收容所(比如CatShelter),那泛型化的方案4会更灵活,扩展性更强。
内容的提问来源于stack exchange,提问作者Binarian
相关产品推荐
相关产品推荐

