最近收到读者反馈,《架构整洁之道》第 25 章“层次与边界”中,图 25.3 和解释这张图的一段文字的描述让人很费解。
如果我们进一步查看 GameRules 内部,就会发现 GameRules 组件的代码中使用的 Boundary 多态接口是由 Language 组件来实现的;同时还会发现 Language 组件使用的 Boundary 多态接口由 GameRules 代码实现。
读者的疑惑是 GameRules 和 Language 分别定义接口让对方实现,那么这两个组件不就形成双向依赖了吗?
我们仅凭直觉就知道双向依赖肯定是错的,那么这句话到底该作何解释?为了排除翻译出错的可能性,我仔细比照了原文,确定了译文并不损失原意。
If we were to look inside GameRules, we would find polymorphic Boundary interfaces used by the code inside GameRules and implemented by the code inside the Language component. We would also find polymorphic Boundary interfaces used by Language and implemented by code inside GameRules.
我回头翻阅了当时技术审校的文章,遗憾地发现自己确实疏漏了对此处的解释,所以我重新翻阅了书中和边界(Boundary)相关的章节,比如第 22 章的“整洁架构”和第 24 章的“不完全边界”,给出一个牵强的解释:我认为 Bob 大叔在这里提到的 Boundary 多态接口(polymorphic Boundary interfaces)指的是系统在构建完全边界时需要的 inputBoundary 和 outputBoundary 的。但是不管如何,也不可能出现他说的低级别的组件(此处的 Language)定义接口让高级别的组件(GameRules)去实现的情况。
为了解决读者心中的疑惑,我写了封邮件给 Bob 大叔。这封邮件的内容重点在询问 GameRules 为什么会有对 Language 的依赖。Bob 大叔的回复得很迅速,内容如下:
The secret here is that the polymorphic interfaces used by Language and implemented by Game Rules are contained within Game Rules. This keeps the arrows pointing in the same direction.
他强调 Language 使用的多态接口其实是定义在 GameRules 当中的,所以依赖方向依然是从 Language 指向 GameRules 的。看到这样的回复,我还是有些迷惑,不过认真思考了一段时间,我大概知道这里的误解到底是什么了。
其实答案就隐藏在第 22 章“整洁架构”里图22.2 一个基于 Web 的、使用数据库的常见 Java 程序。
结合这张图,我们不难总结出 Language和 GameRules 的依赖关系,边界和高层次的接口定义,这里面最需要澄清的点就是“使用”并不意味着“定义”,而只是引用。为了确保我的理解正确,我引用了原文并逐行提出我的问题,然后画了一些图,发送一封确认邮件。Bob 大叔很仔细地回复了我的问题。我把邮件原文列在底下。其中,Q 代表我的问题,Answer 是 Bob 大叔的回答。
"The diagram in Figure 25.3 has gotten a little complicated, but should contain no surprises. The dashed outlines indicate abstract components that define an API that is implemented by the components above or below them. For example, the Language API is implemented by English and Spanish."
Q1: Language components define interfaces that should be implemented by its derivates like English and Spanish that are below the Language component. So why you emphasise ABOVE them?
Answer: In Figure 25.3 you’ll see SMS and Console above Text Delivery. Text Delivery does not actually exist as an independent module. It is an abstraction, or a set of conventions, that both SMS and Console adhere to. English and Spanish appear below Language. Again, Language does not actually exist as a module. English and Spanish simply adhere to the conventions that we call Language.
"GameRules communicates with Language through an API that GameRules defines and Language implements. Language communicates with TextDelivery using an API that Language defines but TextDelivery implements. The API is defined and owned by the user, rather than by the implementer."
Q2: GameRules defines API (interface) that Language component implements, does it mean GameRules and Language are separated components?
Answer: Not quite. English and Spanish are separate from Game Rules and both implement the API defined in Game Rules. The Language abstraction is draw there to denote that English and Spanish follow the conventions that Game Rule and Text Delivery require.
"If we were to look inside GameRules, we would find polymorphic Boundary interfaces used by the code inside GameRules and implemented by the code inside the Language component. We would also find polymorphic Boundary interfaces used by Language and implemented by code inside GameRules."
Q3: In Chapter 22, there is figure 22.2 a typical scenario for a web-based Java system utilising a database.
Here, I mark connections to explain my understanding for "used by" and "implemented by". So "used by" is not meaning "defined by", right?
Answer: Right.
If my understanding is correct, how about the diagram I draw as follow? does it explain the secret you mentioned above?
"If we were to look inside of Language, we would find the same thing: Polymorphic Boundary interfaces implemented by the code inside TextDelivery, and polymorphic Boundary interfaces used by TextDelivery and implemented by Language."
"In each case, the API defined by those Boundary interfaces is owned by the upstream component.”
Q4: In GameRules and Language components context, upstream component means the GameRules? In Language and TextDelivery components context, upstream means the Language?
Answer: Yes. Upstream means “higher level".
当我把邮件完整地转发给读者后,他表示“我看了 Bob 大叔的回答,感觉对依赖反转的认识又深了一层。不,应该说看了你的回答(商业互吹)。抽象出的接口,我从来没想过接口的归属问题,这么看来就合理了。”
小结
以后我们在设计组件时一定要关心边界和接口定义的归属。它代表着依赖反转原则在更大的架构层面上的运用。
于 2019-10-27