piglei / one-python-craftsman

来自一位 Pythonista 的编程经验分享,内容涵盖编码技巧、最佳实践与思维模式等方面。

Home Page:https://www.piglei.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

OOP 原则

jschwinger233 opened this issue · comments

不是 issue, 只是 comment.

里式替换的另一个比较隐蔽的错误会发生在多层抽象的耦合里.

比如有两个抽象 A 和 B, 类型声明是 A 以 B 作为入参 A(B), 但是如果实际的实现里出现了类型特定化, 那么说明这里的抽象失败了.
具体来讲, 抽象 A 有两个实现 A1, A2; 抽象 B 也有两个实现 B1, B2; 一个符合里式替换的实现应该允许 A1(B1), A1(B2), A2(B1), A2(B2) 这些调用组合, 但是稍微缺少经验的工程师可能会实现为特定类型的入参, 只能运行 A1(B1), A2(B2), 传入不正确的类型就会报错.
这类问题在使用抽象工厂的时候特别常见, 因为抽象工厂里就有多个抽象的多个实现, 一旦业务把多个抽象耦合起来, 就容易违反里式替换.
抽象工厂的 UML 就有 A1 A2 B1 B2:
image


依赖倒置其实和开放封闭是有关的, 应该说依赖倒置就是为了开放封闭.

拿你的例子来说:
image
如果未来需要实现另一种 HNWebPage, 比如叫做 LocalHNWebPage, 没有依赖倒置的话, 业务逻辑代码里 (SiteSourceGrouper) 就要去修改 import 模块, 这就违反了开放封闭, 因为并没有修改业务逻辑, 却要改业务代码; 而使用依赖倒置就完美地开放封闭了.
实际上 Django ORM 就是这样的, 想象一下 view 层里的 import model, 这个 model 其实只是一个抽象, 这样将来就算更换下层数据库的实现 (从 MySQL 换成 PostgreSQL), view 层代码(理论上)不需要做相应更改, 这就是依赖倒置这层抽象带来的好处.

感谢你的评论,信息很有价值。我遇到过你说的问题,所以应该理解了你的意思。简单补充一点无关紧要的想法。

是否违反“里式替换原则”

声明了 Interface A(Interface B),却只允许 A1(B1), A2(B2) 之类的组合。和你一样,我觉得这肯定是个问题,但关于这违反的究竟是不是“里式替换原则”,我有些不同看法。

“里式替换原则”针对的主要是“父类对象与子类对象”之间的互相替换,也就是当父类对象的行为能满足需求方时,变化后的子类对象行为却无法继续满足需求,形成一种对多态的破坏。但在这个例子里,没有父类与子类,只有接口(抽象)与实现,而接口(抽象)本身是没有具体行为的,没有行为,原则里的“可替代性”似乎就无从谈起。

因此在我看来,这个也许不能被算成违反“里式替换”。但具体违反了啥?我也不知道……也许是“接口隔离原则”?可能在一开始,A 和 B 两个 Interface 本身就设计的有问题,假如调整接口设计,让其更精确,那些错误的实体类型检查约束也许就能删掉。

关于“开放关闭原则”与“依赖倒置原则”

如你所言,原则之间确实有很多联系。在我看来,开放关闭原则像是个“大杀器”,不牵扯什么具体技术,但非常核心。这原则像在不断提醒大家,软件开发应有的一种理想状态——只扩增加代码(扩展),不修改原有代码,就能满足所有的需求

“依赖倒置原则”直接理解起来更偏具体技术(接口或抽象类),泛一些的话,可以把它当成一种“凡事加一层”的思维模式。而加一层抽象,许多时候是会对达到“开放关闭”要求有一定帮助。最大问题就是可能抽象过度 :)

又琢磨了会这个问题。发现当父类是没提供实现的抽象接口时,可能就是“里式替换原则”里模棱两可的地方。引用 Liskov substitution principle - Wikipedia 的 “Criticism”部分:

While widely used, the characterization of behavioral subtyping as the ability to substitute subtype objects for supertype objects has been said to be flawed. It makes no mention of specifications, so it invites an incorrect reading where the implementation of the supertype is compared to the implementation of the subtype. This is problematic for several reasons, one being that it does not support the common case where the supertype is abstract and has no implementation. Also, more subtly, in the context of object-oriented imperative programming it is difficult to define precisely what it means to universally or existentially quantify over objects of a given type, or to substitute one object for another.[2] When applying subtyping, generally we are not substituting subtype objects for supertype objects, we are simply using subtype objects as supertype objects. That is, it is the same objects, the subtype objects, that are also supertype objects.

In an interview in 2016, Liskov herself explains that what she presented in her keynote address was an "informal rule", that Jeannette Wing later proposed that they "try to figure out precisely what this means", which led to their joint publication[1] on behavioral subtyping, and indeed that "technically, it's called behavioral subtyping".[3] During the interview, she does not use substitution terminology to discuss the concepts.

真是让人头大。