GOF 中提到的可插拔适配器

Pluggable Adapter as mentioned in the GOF

Stackover 上有关此主题的相关帖子: and

以上 post 很好,但我仍然无法解决我的困惑,因此我将其作为新的 post 放在这里。
我的问题 基于 GOF 的 可重用面向对象软件的元素 关于可插拔适配器的书籍内容(在下面的问题之后提到),因此我将不胜感激discussions/answers/comments 更关注 GOF 中关于可插拔适配器的现有示例,而不是其他示例

Q1) 内置界面适配是什么意思?
Q2)与通常的适配器相比,可插拔接口有何特别之处?通常的适配器也会将一个接口适配到另一个接口。
Q3) 即使在这两个用例中,我们也看到了提取的 "Narrow Interface" GetChildren(Node)CreateGraphicNode(Node) 取决于 Node 的方法。 Node 是 Toolkit 的内部。 Node 是否与 GraphicNode 相同,并且在 CreateGraphicNode 中传递的参数是否仅用于填充已创建的 Node 对象的状态(名称,parentID 等)?

根据 GOF(我已将少数 words/sentences 标记为粗体以强调与我的问题相关的内容)

ObjectWorks\Smalltalk [Par90] uses the term pluggable adapter to describe classes with built-in interface adaptation.

Consider a TreeDisplay widget that can display tree structures graphically. If this were a special-purpose widget for use in just one application, then we might require the objects that it displays to have a specific interface; that is, all must descend from a Tree abstract class. But if we wanted to make TreeDisplay more reusable (say we wanted to make it part of a toolkit of useful widgets), then that requirement would be unreasonable. Applications will define their own classes for tree structures. They shouldn't be forced to use our Tree abstract class. Different tree structures will have different interfaces.

Pluggable adapters. Let's look at three ways to implement pluggable adapters for the TreeDisplay widget described earlier, which can lay out and display a hierarchical structure automatically. The first step, which is common to all three of the implementations discussed here, is to find a "narrow" interface for Adaptee, that is, the smallest subset of operations that lets us do the adaptation. A narrow interface consisting of only a couple of operations is easier to adapt than an interface with dozens of operations. For TreeDisplay, the adaptee is any hierarchical structure. A minimalist interface might include two operations, one that defines how to present a node in the hierarchical structure graphically, and another that retrieves the node's children.

然后有两个用例

  1. "Narrow Interface" 被抽象为 TreeDisplay 的一部分 Class

  2. Narrow Interface 提取出来作为一个单独的界面,并在TreeDisplay中有它的组合class

(也有参数化适配器的第三种方法,但为简单起见跳过它,我想这第三种方法更具体于 Small talk)

Q1) 接口适配只是指适配一个接口来实现另一个接口,即适配器是干什么用的。我不确定 "built-in" 是什么意思,但这听起来像是 Smalltalk 的一个特定功能,我对此并不熟悉。

Q2) "Pluggable Adapter" 是一个适配器 class,它通过接受其各个方法的实现作为构造函数参数来实现目标接口。目的是让适配器可以表达得简洁。在所有情况下,这都要求目标接口很小,并且通常需要某种语言工具来简洁地提供计算——lambda 或委托或类似的东西。在 Java 中,内联 classes 和功能接口的功能意味着不需要接受 lambda 参数的特定适配器 class。

可插拔适配器方便。除此之外它们并不重要。然而...

Q3) 引用的文本与可插适配器无关,并且这两个用例中都没有可插适配器。那部分是关于接口隔离原则的,很重要。

在第一个示例中,TreeDisplay 被子class 编辑。实际的适配器接口是 TreeDisplay 中需要实现的方法子集。这不太理想,因为没有适配器必须实现的接口的简洁定义,并且 DirectoryTreeDisplay 不能同时实现另一个类似的目标接口。此外,此类实现往往以复杂的方式与 subclass 交互。

在第二个示例中,TreeDisplay 带有一个 TreeAccessorDelegate 界面,该界面捕获它可以显示的内容的要求。这是一个可以通过多种方式轻松实现的小型接口,包括通过可插拔适配器。 (尽管示例 DirectoryBrowser 不可插入)。此外,接口适配不一定是适配器的唯一目的 class。你看DirectoryBrowserclass实现了和树显示无关的方法

这些示例中的 Node 类型将是一个 empty/small 接口,即另一个适配器目标,甚至是一个泛型类型参数,因此不需要进行任何调整。实际上,我认为可以通过将 Node 设为 适应目标来改进此设计。

当我们谈论适配器设计模式时,我们通常会考虑两个我们想要集成的预先存在的 API,但它们不匹配,因为它们是在不同的时间在不同的域中实现的。适配器可能需要从一个 API 到另一个进行大量映射,因为两个 API 的设计都没有考虑到这种可扩展性。

但是如果 Target API 的设计考虑到了未来的适应性呢? Target API 可以通过最小化假设并为适配器实现提供尽可能窄的接口来简化未来适配器的工作。请注意,此设计需要 先验 规划。与适配器模式的典型用例不同,您不能在任意两个 API 之间插入可插拔适配器。 Target API 必须设计为支持可插入适配。

Q1)这就是GoF所说的built-in interface adaptation的意思:一个interface is built into Target API 以支持未来的改编。

Q2) 如前所述,对于适配器来说,这是一个相对不常见的场景,因为该模式的典型优势在于它能够处理 API没有共同的设计。

GoF 列出了三种不同的方法来设计 Target API 适应性。前两个被认为是他们的一些行为设计模式。

  1. 模板方法
  2. 策略
  3. 闭包(Smalltalk 称之为 code blocks

Q3) 在不深入了解 GoF 的 GUI 示例的细节的情况下,设计他们所谓的 "narrow interface" 背后的基本思想是消除尽可能多的领域特异性尽可能。在 Java 中,与域无关的 API 的起点几乎肯定是 functional interfaces.

Target API 依赖于这些接口应该比围绕特定领域方法构建的 API 更容易适应。前者允许创建可插拔适配器,而后者需要一个更典型的适配器,在 APIs.

之间具有大量映射

让我分享一些想法。

首先,由于问题是使用 Smalltalk 标签发布的,我将使用不太冗长的 Smalltalk 语法(例如 #children 而不是 GetChildren(Tree,Node) 等)

作为这个问题的介绍(可能对某些读者有用),假设(通用)框架需要使用通用语言(例如#children)。但是,对于您正在考虑的特定 object,通用术语可能并不自然。例如,对于文件系统,通常有 #files#directories 等,但可能没有选择器 #children。即使添加这些选择器不会杀死任何人,您也不希望每次 "abstract" class 强加其命名约定时都用新的 "generic" 选择器填充 classes .在现实生活中,如果你这样做,迟早你会 end-up 与其他框架发生冲突,对于这些框架,相同的选择器具有不同的含义。这意味着每个框架都有可能与试图从中受益的 object 产生一些阻抗(a.k.a. 摩擦)。好吧,适配器 旨在减轻这些副作用。

有几种方法可以做到这一点。一种是使您的框架 可插入 。这意味着您将不需要客户端实现特定行为。相反,您将要求客户提供一个选择器或一个块,其评估将产生所需的行为。

在目录示例中,如果您的 class Directory 恰好实现了,比如 #entities,那么您不会创建 #children 作为同义词,而是告诉在框架中适当 class 类似 childrenSelector: #entities。 object 接收此方法然后将 "plug" (记住)它必须在查找 children 时发送给您 #entities。如果您没有这样的方法,您仍然可以使用执行所需操作的块来提供所需的行为。在我们的示例中,块看起来像

   childrenSelector: [self directories, self files].

(旁注: 可插入框架可以提供同义词 #childrenBlock: 以使其界面更友好。或者,它可以提供更通用的选择器,例如childrenGetter:, 等等)

接收方现在会将块保存在其 childrenGetter ivar 中,并在每次需要客户端的 children.

时对其进行评估

另一种可能需要考虑的解决方案是要求客户提交 class 摘要 class。这样做的好处是可以非常清楚地暴露客户的行为。但是请注意,此解决方案有一些缺点,因为在 Smalltalk 中,您只能继承一个 parent。因此,强加 superclass 可能会导致不希望的(甚至不可行的)约束。

您提到的另一个选项是在前一个选项上添加一个间接选项:不是 subclassing main "object",而是为 sub[= 提供一个抽象的 superclass 63=]调整您的 object 需要适应的行为。这与第一种方法类似,因为您不需要更改客户端,只是这次您将适配的协议单独放在 class 中。这样,您无需将多个参数插入框架,而是将它们全部放入 object 并将 object 传递(或 "plug")到框架。请注意,这些适配 objects 充当包装器,因为它们知道真实的东西,并且知道如何处理它以翻译框架需要发送的少数消息。通常,包装器的使用提供了很大的灵活性,但代价是用更多 classes 填充您的系统(这会带来重复层次结构的风险)。此外,包装许多 object 可能会影响系统的性能。顺便说一句,GraphicNode 看起来也像 intrinsic/actual Node.

的包装

我不确定我是否回答了你的问题,但既然你让我以某种方式扩展我的评论,我很乐意尝试这样做。