关于 JavaScript 中的封装,"Decoupling Implementation Details"是什么意思?



我正在读一篇关于封装的博客文章,看完后我想我明白了什么是封装以及为什么我们需要使用它,但最后,作者谈到了封装的优点,他列出了"解耦实现细节";作为使用封装的好处之一。我在这里引用它:

由于封装允许使用公共接口对数据进行操作,因此更容易更新实现,因为消除了紧密耦合。这是一种始终针对接口进行编码的极好方法。这也意味着,使用封装,对象可以在不破坏兼容性的情况下进行扩展,以适应未来的更改。耦合是指两个模块相互依赖的程度。解耦是指消除这种依赖关系。

我正试图在脑海中解决这个问题,并认为也许SO成员可以帮助我理解他在说什么。有人能向我解释一下吗?

提前感谢!

封装是一个有意识的决定,它为您的代码(某些功能的实现)提供了一个特定的接口,这样您的实现的消费者就只能假设他们必须做什么。

我将从一个非JS示例开始,但最后将显示一个。


主机名是一个接口。IP地址是实现细节。

当你访问Stack Overflow时,你会访问stackoverflow.com,而不是访问151.101.193.69。如果你去了151.101.193.69,你会注意到这是一个Fastly CDN地址,而不是Stack Overflow地址。

很可能,当Stack Overflow刚刚开始时,它使用自己的服务器在不同的IP地址上实现了网络访问,例如198.51.100.253。

如果每个人都为198.51.100.253添加了书签,那么当Stack Overflow开始使用Fastly CDN时,突然间,每个为其添加书签的人——数百万人——都必须进行调整
这是一个破坏兼容性的情况,因为那数百万人本来会连接到IP地址198.51.100.253。

通过将IP地址198.51.100.253(网络访问实现的实际细节)封装在用户需要知道的东西——网站名称——stackoverflow.com——公共接口后面,Stack Overflow能够迁移到Fastly CDN,而数百万用户对此一无所知
这是可能的,因为所有这些用户都没有连接到IP地址198.51.100.253,所以当它变为151.101.193.69时,没有人受到影响。


这一原则适用于许多领域。以下是一些例子:

  • 能源:电费由您支付。供应商可以使用煤炭、天然气、柴油、核能、水力发电来提供,他们可以将其从一种改为另一种,但你一点也不知道,你没有连接到水力发电,因为你的接口是插座,而不是发电机
  • 商业:当一栋办公楼让清洁公司保持建筑清洁时,他们只与该公司签订了合同;他们的清洁工被雇佣和解雇,他们的工资也在变化,但这一切都由清洁公司决定,不会影响大楼
  • 钱:你不需要钱,你需要食物、住所和衣服。但这些都是实现细节。你出口给雇主的界面是钱,所以他们不必向你支付食物费用,如果你改变了饮食或风格,他们也不必调整给你买的食物或衣服
  • 工程:当一栋办公楼的暖通空调坏了,业主只会打电话给暖通空调公司,他们不会自己修理。如果他们这样做了,他们就失去了保修,因为如果其他人接触了暖通空调,暖通空调公司就不能保证产品是好的。他们的公共界面是维护合同和面向暖通空调用户的控件——不允许直接访问实现

当然,软件:假设您有一个分布式密钥值存储,它有以下客户端API:

client = kv.connect("endpoint.my.db");
bucket = crc(myKey)
nodeId = bucket % client.nodeCount();
myValue = client.get(nodeId, bucket, myKey);

此接口:

  • 允许调用者直接轻松地找到将存储密钥的节点
  • 允许调用者缓存bucket信息以进一步保存调用
  • 允许调用者避免额外的调用来将密钥映射到bucket

然而,它在接口中泄露了大量的实现细节:

  • 桶的存在
  • 使用CRC将密钥映射到存储桶
  • bucket分布和再平衡策略-使用bucket%nodeCount作为将bucket映射到节点的逻辑
  • 存储桶由各个节点所有

现在调用程序与所有这些实现细节耦合在一起。如果DB的维护者想要进行某些更改,他们将中断所有现有用户。示例:

  • 使用CRC32而不是CRC,大概是因为它更快。这将导致现有代码使用错误的bucket和/或节点,导致查询失败
  • 不是循环存储桶,而是根据存储节点的可用空间、可用CPU、可用内存等分配存储桶。这会破坏bucket % client.nodeCount(),同样会导致错误的存储桶/节点并导致查询失败
  • 允许多个节点拥有一个bucket-请求仍将发送到单个节点
  • 更改再平衡策略-如果一个节点出现故障,则nodeCount从例如3变为2,因此必须重新平衡所有桶,以便bucket % client.nodeCount()找到该桶的正确节点
  • 允许从任何节点读取,而不是从bucket所有者读取——请求仍然会发送到单个节点

要将调用程序与实现解耦,您不允许它们缓存任何内容、保存调用或假设任何内容:

client = kv.connect("endpoint.my.db");
myValue = client.get(myKey);

调用者不知道CRC、bucket,甚至不知道节点。

在这里,客户端必须做额外的工作来确定向哪个节点发送请求。也许是使用Zookeeper或使用八卦协议。

使用此接口:

  • 散列逻辑,例如CRC不是在调用者中硬编码的,它在服务器端,更改它不会破坏调用者
  • 任何bucket分发策略同样只在服务器端
  • 任何再平衡逻辑同样不在客户端中

只需升级客户端,但不更改调用程序中的任何代码,就可以进行其他更改:

  • 允许多个节点拥有一个bucket
  • 从任何节点读取(例如,选择延迟最低的节点)
  • 从基于Zookeeper的节点查找基础设施切换到基于八卦的基础设施

让我们以Map类为例。它有一个API,用于将键/值条目添加到映射中,获取键的值等。您不知道Map如何存储其键/值对的详细信息,您只知道如果将set与键一起使用,那么稍后将get与同一个键一起使用时,您会得到值。实现的细节对您来说是隐藏的,在很大程度上与您的代码无关。这是封装如何将API与实现细节解耦的示例。


cco您do知道关于这个特定示例中的实现的little位:规范说"映射必须使用哈希表或其他机制来实现,这些机制平均提供的访问时间与集合中元素的数量次线性">所以你知道Map并不是实现为(比如)键/值对的数组,因为该实现不会提供对元素数量的次线性访问时间。但你不知道它是一个哈希表还是B树。这些是实施细节

相关内容

最新更新