您可能遇到的三种最常见的重构机会
所有开发人员都会在某个时候进行重构。我们往往拥有第六感,能够判断何时重构代码。有些人在做重构决策时会遵循一些可靠的原则。而另一些人则在实际操作过程中,能够感觉到重构的必要性。
无论你属于哪个阵营,重构都是所有代码库的必要流程。现实情况是,需求在不断演变,决策也瞬息万变。生产支持事件迫使我们不得不比预想的更快地做出反应。各种各样的情况都可能导致代码重构成为必要。
但是我们如何知道何时需要它呢?这因代码库而异。不过,大多数开发人员至少认识到三个常见的机会。
1. 重复代码
作为开发者,我们天生就具备察觉自己重复的本能。无论正确与否,我们都明白,重复自己是不好的,我们不应该这样做。
所以,我们自然而然地首先发现需要重构的是那些重复的代码。我们甚至有一个以此命名的编码最佳实践。
不要重复自己(DRY)是软件开发的一项原则,旨在减少软件模式的重复[1],用抽象或数据规范化来避免冗余。(来源:维基百科)
重复代码通常是意外出现的,或者只是暂时性的,我们稍后会修复。它可能看起来像这样。
function doSomething(x, y, z) {
let squared = Math.pow(x, 2)
let summed = y + z
let result = summed / squared
if (result > 0) {
return 'Yes'
} else {
return 'No'
}
}
function doAnotherThing(x, y, z) {
let squared = Math.pow(x, 2)
let summed = y + z
let result = summed / squared
if (result <= 0) {
return 'Yes'
} else {
return 'No'
}
}
这段代码虽然简单,但却充分展现了我们作为开发者的直觉。当我们仔细查看这段代码时,尤其是当两个函数像这里一样相邻时,我们立刻就能注意到其中的重复逻辑。我们看到,它会通过参数计算出一个结果,并根据该结果返回'Yes'
或'No'
。返回值取决于 是否result
小于 0doAnotherThing
或 是否大于 0 doSomething
。
我们可以在这里进行的重构很简单。
function getResult(x, y, z) {
let squared = Math.pow(x, 2)
let summed = y + z
return summed / squared
}
function doSomething(x, y, z) {
let result = getResult(x, y, z)
if (result > 0) {
return 'Yes'
} else {
return 'No'
}
}
function doAnotherThing(x, y, z) {
let result = getResult(x, y, z)
if (result <= 0) {
return 'Yes'
} else {
return 'No'
}
}
现在我们的两个函数调用一个getResult
包含共享代码的公共函数。
但这对我们有什么好处呢?它允许将通用逻辑集中在一个地方。
例如,我们的代码中有一个 bug,summed / squared
由于我们没有检查除数是否为 0,因此很容易出现除以零的错误。x
在重构之前,我们必须在代码重复的两个地方都处理这个问题。现在,我们可以在新函数中处理这个问题,并且两个函数都得到了修复。
function getResult(x, y, z) {
let squared = Math.pow(x, 2)
let summed = y + z
if (squared > 0) {
return summed / squared
} else {
return 0
}
}
function doSomething(x, y, z) {
let result = getResult(x, y, z)
if (result > 0) {
return 'Yes'
} else {
return 'No'
}
}
function doAnotherThing(x, y, z) {
let result = getResult(x, y, z)
if (result <= 0) {
return 'Yes'
} else {
return 'No'
}
}
重复代码会导致许多 bug,因为 bug 会随之重复。开发人员不擅长更新出现重复的各个区域,因此某个区域可能得到修复,而另一个区域则没有。如果没有我们上面的重构,我们很容易在一个函数中添加零值检查,而忽略了另一个函数。
但是,所有重复的代码都是不好的吗?
不。作为开发者,我们有时违反 DRY 原则是合理的。有些人甚至对 DRY 有不同的看法,他们不会创建一个通用函数,除非他们不得不重复代码两次以上。
其他场景,例如基于微服务的架构,在必要时支持违反该原则。这是因为共享代码在跨越服务边界时会产生耦合。当你遇到重复代码重构机会时,需要牢记这些要点。
2. 长函数
接下来,我们倾向于认为需要重新粉刷的下一个东西是长函数。与重复代码不同,我们重构长函数是为了更好地理解正在发生的事情。
这与重构重复代码的优势不同。在那种情况下,我们重构是为了集中共享的逻辑,避免重复。在这个新的机会里,我们将重构成更小的函数。这并不是因为它们的逻辑可以共享,而是为了让我们更好地理解每个部分处理的内容。
让我们看一个长函数的例子。
const run = async () => {
const databaseOneClient = new Client({
user: process.env.DB1_USER,
host: process.env.DB1_HOST,
database: process.env.DB1_DB,
password: process.env.DB1_PASSWORD,
port: 5439,
})
const databaseTwoClient = new Client({
user: process.env.DB2_USER,
host: process.env.DB2_HOST,
database: process.env.DB2_DB,
password: process.env.DB2_PASSWORD,
port: 5432,
})
const databaseOneCountResult = await databaseOneClient.query(`select count(*) from table_one`)
const databaseOneCount = databaseOneCountResult.rows[0].count
const result = await databaseTwoClient.query(`select count(*) from copy_table`)
const existingCount = result.rows.length != 1 ? 0 : result.rows[0].row_count
for (var i = existingCount; i <= databaseOneCount; i++) {
let newObj = {
x: `lat_x_${i}`,
y: `long_y_${i}`,
z: `d_z_${i}`,
url: ''
}
try {
const response = await fetch(`https://somurl-to-call.com/api/do/${i}`)
const jsonResponse = await response.json()
if (jsonResponse.location >= 10) {
newObj['url'] = jsonResponse.lookup
}
const insertQuery = `insert into copy_table(x, y, z, url) values ($1, $2, $3, $4) `
await databaseTwoClient.query(insertQuery, [newObj.x, newObj.y, newObj.z, newObj.url])
} catch (error) {
console.error(error)
}
}
}
表面上看,这似乎没什么问题。我们连接到两个不同的数据库。连接后,我们会从每个数据库的表中获取行数。如果行数不相等,我们会在调用外部服务的 API 后,将生成的一些数据复制到第二个数据库中。
当我们用这样的文字表达时,它就变得清晰了。但是当我们通读代码时,要理解到底发生了什么,需要相当多的认知负担。幸运的是,这个函数还不错,因为它仍然可以在一个屏幕上显示。
如果它不适合一个屏幕,当我们向下滚动到底部时,我们可能会失去顶部发生的事情的上下文。
重构长函数的目的是保持代码的字面含义不变,同时减少理解代码的认知负担。为此,我们将函数的多个部分移到单独的函数中。
下面是一个示例。
const run = async () => {
const databaseOneClient = initializeDatabaseOne()
const databaseTwoClient = initializeDatabaseTwo()
const databaseOneCount = await getDatabaseOneTotalCount(databaseOneClient)
const existingCount = await getDatabaseTwoTotalCount(databaseTwoClient)
for (var i = existingCount; i <= databaseOneCount; i++) {
let newObj = {
x: `lat_x_${i}`,
y: `long_y_${i}`,
z: `d_z_${i}`,
url: ''
}
try {
const response = await fetch(`https://somurl-to-call.com/api/do/${i}`)
const jsonResponse = await response.json()
if (jsonResponse.location >= 10) {
newObj['url'] = jsonResponse.lookup
}
const insertQuery = `insert into copy_table(x, y, z, url) values ($1, $2, $3, $4) `
await databaseTwoClient.query(insertQuery, [newObj.x, newObj.y, newObj.z, newObj.url])
} catch (error) {
console.error(error)
}
}
}
const getDatabaseOneTotalCount = async (databaseOneClient) => {
const databaseOneCountResult = await databaseOneClient.query(`select count(*) from table_one`)
return databaseOneCountResult.rows[0].count
}
const getDatabaseTwoTotalCount = async (databaseTwoClient) => {
const result = await databaseTwoClient.query(`select count(*) from copy_table`)
const existingCount = result.rows.length != 1 ? 0 : result.rows[0].row_count
return existingCount
}
const initializeDatabaseOne = () => {
return new Client({
user: process.env.DB1_USER,
host: process.env.DB1_HOST,
database: process.env.DB1_DB,
password: process.env.DB1_PASSWORD,
port: 5439,
})
}
const initializeDatabaseTwo = () => {
return new Client({
user: process.env.DB2_USER,
host: process.env.DB2_HOST,
database: process.env.DB2_DB,
password: process.env.DB2_PASSWORD,
port: 5432,
})
}
一个屏幕上仍然有很多文本,但现在我们一个大功能的逻辑被分成了更小的功能。
- 我们现在有两个初始化数据库客户端的函数(
initializeDatabaseOne
和initializeDatabaseTwo
)。 - 我们还有两个函数可以从每个数据库(
getDatabaseOneTotalCount
和getDatabaseTwoTotalCount
)获取表计数。
这些不是共享函数,因此我们不会减少重复的代码,但是,我们会使我们的主函数中的代码更容易理解。
这个小小的重构让我们第一次看函数的开头run()
更容易理解。它非常清晰地描述了我们连接到两个数据库并从每个数据库获取行数。如果行数不相等,我们会在调用 API 后将生成的一些数据复制到第二个数据库中。
请注意,我们没有重构循环及其内部结构。这是因为这是我们第三个最常见的重构机会,现在让我们来探讨一下。
3. 复杂循环
第三个也是最后一个重构机会,有点像上一个的延伸。我们经常在代码中使用循环来迭代集合并对其执行操作和/或转换。
我们在上面的示例代码中看到了这一点。
const run = async () => {
const databaseOneClient = initializeDatabaseOne()
const databaseTwoClient = initializeDatabaseTwo()
const databaseOneCount = await getDatabaseOneTotalCount(databaseOneClient)
const existingCount = await getDatabaseTwoTotalCount(databaseTwoClient)
for (var i = existingCount; i <= databaseOneCount; i++) {
let newObj = {
x: `lat_x_${i}`,
y: `long_y_${i}`,
z: `d_z_${i}`,
url: ''
}
try {
const response = await fetch(`https://somurl-to-call.com/api/do/${i}`)
const jsonResponse = await response.json()
if (jsonResponse.location >= 10) {
newObj['url'] = jsonResponse.lookup
}
const insertQuery = `insert into copy_table(x, y, z, url) values ($1, $2, $3, $4) `
await databaseTwoClient.query(insertQuery, [newObj.x, newObj.y, newObj.z, newObj.url])
} catch (error) {
console.error(error)
}
}
}
existingCount
我们从一直迭代到databaseOneCount
。每次迭代,我们都构建一个新对象 ,newObj
并在外部 API 调用后将该对象插入到第二个数据库中。
只需稍加调整,这个循环内部就可以简化,并且更容易理解。
注意:我省略了上面的一些代码,以专注于这个复杂的循环部分。
const run = async () => {
const databaseOneClient = initializeDatabaseOne()
const databaseTwoClient = initializeDatabaseTwo()
const databaseOneCount = await getDatabaseOneTotalCount(databaseOneClient)
const existingCount = await getDatabaseTwoTotalCount(databaseTwoClient)
for (var i = existingCount; i <= databaseOneCount; i++) {
await insertNewObject(i, databaseTwoClient)
}
}
const insertNewObject = async (i, databaseTwoClient) => {
let newObj = {
x: `lat_x_${i}`,
y: `long_y_${i}`,
z: `d_z_${i}`,
url: ''
}
try {
const response = await fetch(`https://somurl-to-call.com/api/do/${i}`)
const jsonResponse = await response.json()
if (jsonResponse.location >= 10) {
newObj['url'] = jsonResponse.lookup
}
const insertQuery = `insert into copy_table(x, y, z, url) values ($1, $2, $3, $4) `
await databaseTwoClient.query(insertQuery, [newObj.x, newObj.y, newObj.z, newObj.url])
} catch (error) {
console.error(error)
}
}
💥 我们的复杂循环已大大简化。
等等,我们不是刚刚把循环的内部代码移到了一个单独的函数里吗?没错,我们就是这么做的。这个小小的改变对理解 中发生的事情产生了巨大的影响run
。有了描述性的函数名,我们可以看到我们从existingCount
向上迭代到 ,databaseOneCount
并且insertNewObject
。
这个小小的改变对于减少我们理解这段代码的作用所需的认知负荷有着巨大的作用。
当然,我们可以更进一步,重构这个insertNewObject
函数。我们可以将 API 调用与实际的数据库插入操作分开。不过,这个练习就留给读者自己去做吧。
结论
重构是每个开发团队的必经流程。事实上,我们今天编写的代码是基于我们当前可用的假设、时间限制和决策。所有这些因素在未来都必然会发生变化,因此我们的代码也需要随之改变。
重构的艺术是一个宏大的话题,我们在这里只是略知皮毛。还有很多其他的模式和机会有待发现,我们在这里没有讨论。但是,这里提到的三种模式应该适用于任何技术栈,应该能帮助你更熟悉这种实践。
想看看我的其他项目吗?
我是 DEV 社区的忠实粉丝。如果您有任何疑问,或者想探讨重构相关的不同想法,请在下方留言或在 Twitter 上联系我们。
除了写博客之外,我还创建了一门“通过使用 AWS 学习 AWS”课程。课程将重点讲解如何实际使用 Amazon Web Services 来托管、保护和交付静态网站。这是一个简单的问题,有很多解决方案,但它非常适合加深你对 AWS 的理解。我最近为该课程添加了两个新的附加章节,分别侧重于“基础设施即代码”和“持续部署”。
我还精心策划了每周的新闻通讯。“ Learn By Doing”新闻通讯每周都包含精彩的云计算、编程和 DevOps 文章。注册即可在您的邮箱中接收。
文章来源:https://dev.to/kylegalbraith/the- Three-most-common-refactoring-opportunities-you-are-likely-to-encounter-448i