让我们继续有关代码重构的一系列简短帖子!在其中,我们讨论了可以帮助您改善代码和项目的技术和工具。
今天,我们将讨论如何在模块之间设置清晰的边界并限制重构代码时的更改范围。
波纹效应问题
重构中最烦人的问题之一是连锁效应问题。这种情况是,一个模块变化为代码库的另一部分(有时是遥远)的情况。
当变化的传播不限于不限时,我们会害怕修改代码。感觉就像一切都会炸毁,或者我们需要更新很多代码。
当模块彼此了解得太多时,连锁反应问题通常会出现。
高耦合
一个模块知道其他模块的结构的程度称为coupling。
当模块之间的耦合较高时,这意味着它们依靠内部结构和实现细节。
这正是导致连锁反应的原因。耦合越高,隔离到特定模块的更难更难。
看看这个示例。假设我们开发了一个博客平台,并且有一个函数为当前用户创建新帖子:
import { api } from 'network'
async function createPost(content) {
// ...
const result = await api.post(
api.baseUrl + api.posts.create,
{ body: content });
// ...
}
此代码的问题位于network
模块中:
- 它将其太多的内部细节暴露于其他模块中。
- 它没有为其他模块提供清晰的公共API,可以指导他们如何使用
network
模块。
如果我们在模块之间更清晰和较窄之间的界限,我们可以解决该问题。
不清楚和广泛的边界
正如我们之前说的,连锁反应的根本原因是耦合。耦合越高,变化越宽。
在上面的示例中,我们可以计算createPost
函数与network
模块结合的程度:
import { api } from 'network' /* (1) */
async function createPost(content) {
// ...
const result = await api.post( /* (2) */
/* (3) */ api.baseUrl + api.posts.create, /* (4) */
/* (5) */ { body: content });
// ...
}
/**
* 1. The “entry point” to the `network` module.
* 2. Using the `post` method of the `api` object.
* 3. Using the `baseUrl` property...
* 4. ...And the `.posts.create` property to build a URL.
* 5. Passing the post content as the value for the `body` key.
*/
这个数量(5)的数量太多了。 api
对象详细信息中的任何更改都会立即影响createPost
函数。
如果我们假设还有许多其他位置使用了api
对象,那么所有这些模块也会受到影响。
createPost
和network
之间的边界宽且不清楚。 network
模块没有为使用的消费者声明一组清晰的功能(例如createPost
)。
我们可以使用合同来解决此问题。
API合同
合同是对其他实体的保证。它指定了如何使用模块可以使用以及如何使用它们 。
合同允许该计划的其他部分不依赖于模块的实施,而只能依靠其承诺 。 /p>
在打字稿中,我们可以使用类型和接口来声明合同。让我们使用它们为network
模块设置合同:
type ApiResponse = {
state: "OK" | "ERROR";
};
interface ApiClient {
createPost(post: Post): Promise<ApiResponse>;
}
然后,让我们在network
模块内实施此合同,仅公开公共API(合同承诺),而不会透露任何额外的细节:
const client: ApiClient = {
createPost: async (post) => {
const result = await api.post(
api.baseUrl + api.posts.create,
{ body: post })
return result
}
};
我们隐藏了ApiClient
接口背后的所有实现详细信息,并揭示了消费者真正需要的方法。
顺便说一句,它可以提醒您有关“Facade” pattern或“Anti-Corruption Layer” technic。
此更改后,我们将在createPost
函数中使用network
模块:
import { client } from 'network'
async function createPost(post) {
// ...
const result = await client.createPost(post);
// ...
}
耦合点的数量现在仅减少到2:
import { client } from 'network' /* (1) */
async function createPost(post) {
// ...
const result = await client.createPost(post); /* (2) */
// ...
}
我们不依赖client
在引擎盖下的工作原理,而只能是 承诺我们的工作。
它使我们能够更改network
模块的内部结构。因为当合同(ApiClient
接口)保持不变时,消费者不需要更新其代码。
顺便说一句,合同不一定是类型签名或接口。它们可以是声音或书面协议,DTO,消息格式等。重要的是,这些协议应声明并固定系统的各个部分相互朝向。
它还允许将代码库分为不同的内聚零件,这些零件与狭窄且明确指定的合同相连:
这又使我们可以在重构代码时限制更改的传播,因为它们会在模块内范围内范围:
有关我书中重构的更多信息
在这篇文章中,我们仅讨论了耦合和模块边界。
我们尚未提及该主题的另一个重要部分,这是凝聚力。我们跳过了合同的正式定义,没有讨论关注原则的分离,这可以帮助我们看到划定这些边界的地方。
如果您想进一步了解这些方面并一般重构,我鼓励您查看我的在线书:
这本书是免费的,可以在Github上获得。在其中,我更详细地解释了这个主题。
希望您发现它有帮助!享受这本书ð