软件变得复杂的一个例子
由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!
让我们用 JavaScript 编写一个缓存程序,看看保持简单意味着什么。
我们经常听到软件开发人员说,我们应该保持代码简洁,需要控制复杂性。与此同时,我们也提倡代码重用和共享,并使其易于扩展。
编写软件时,很容易写出比复杂更复杂的代码,这些代码试图做太多事情,难以使用。
每个人都告诉你应该化繁为简。
而且基本上我们都认同这听起来合情合理。
如果我们都清楚自己的目标,为什么项目随着时间的推移不断演变,最终却往往变得一团糟,难以推进呢?
或许我们需要更多例子来说明追求简单解决方案的意义所在。
我们来构建一个简单的缓存。
缓存应该允许我们设置键值对并检索一次值。
一个简单的实现方式如下:
const cache = () => {
const store = {}
const set = (key, value) => {
store[key] = value
}
const remove = key => {
const value = store[key]
delete store[key]
return value
}
return { set, remove }
}
// Let's use the cache
const simpleCache = cache()
simpleCache.set('a', 1)
simpleCache.set('b', 2)
simpleCache.set('b', 3)
console.log(simpleCache.remove('a')) // 1
console.log(simpleCache.remove('b')) // 3
console.log(simpleCache.remove('b')) // undefined
随着项目的推进,新的需求不断涌现,缓存也需要对缓存中的条目进行过期处理。需要指定一个生存时间(TTL),并在缓存条目过期时执行回调函数。相应地修改代码:
const cache = (ttl, expirationHandler) => {
const store = {}
const set = (key, value) => {
// Clear existing timer
const record = store[key]
if (record) {
clearTimeout(record.timer)
}
// Set expiration timer
const timer = setTimeout(() => {
expirationHandler(key, store[key].value)
delete store[key]
}, ttl)
// Store timer and value
store[key] = { timer, value }
}
const remove = key => {
// Find record
const record = store[key]
if (!record) {
return undefined
}
delete store[key]
const { timer, value } = record
// Clear timer and store
clearTimeout(timer)
return value
}
return { set, remove }
}
const expirationHandler = (key, value) => {
console.log(`expired ${key}: ${value}`) // expired b: 2
}
const expiringCache = cache(1000, expirationHandler)
expiringCache.set('a', 1)
expiringCache.set('b', 2)
console.log(expiringCache.remove('a')) // 1
console.log(expiringCache.remove('a')) // undefined
setTimeout(() => {
console.log(expiringCache.remove('b')) // undefined
}, 1100)
一切运行良好,但在审查你的代码时,你的同事注意到同一个缓存被用在了另一个严格要求缓存中的项目永不过期的环境中。
现在你可以简单地在代码库中保留旧的和新的缓存实现,但你更喜欢保持代码简洁(DRY)。
因此,您需要调整新的缓存以支持这两种使用场景:
const cache = (ttl, expirationHandler) => {
const store = {}
const set = (key, value) => {
// If no TTL is specified, behave as before and return early
if (!ttl) {
store[key] = value
return
}
// Clear existing timer
const record = store[key]
if (record) {
clearTimeout(record.timer)
}
// Set expiration timer
const timer = setTimeout(() => {
expirationHandler(key, store[key].value)
delete store[key]
}, ttl)
// Store timer and value
store[key] = { timer, value }
}
const remove = key => {
// Find record
const record = store[key]
if (!record) {
return undefined
}
delete store[key]
// If no TTL is specified, behave as before and return early
if (!ttl) {
return record
}
const { timer, value } = record
// Clear timer and store
clearTimeout(timer)
return value
}
return { set, remove }
}
// Let's use the simple cache
const simpleCache = cache()
simpleCache.set('a', 1)
simpleCache.set('b', 2)
simpleCache.set('b', 3)
console.log(simpleCache.remove('a')) // 1
console.log(simpleCache.remove('b')) // 3
console.log(simpleCache.remove('b')) // undefined
// Let's use the expiring cache
const expirationHandler = (key, value) => {
console.log(`expired ${key}: ${value}`) // expired b: 2
}
const expiringCache = cache(1000, expirationHandler)
expiringCache.set('a', 1)
expiringCache.set('b', 2)
console.log(expiringCache.remove('a')) // 1
console.log(expiringCache.remove('a')) // undefined
setTimeout(() => {
console.log(expiringCache.remove('b')) // undefined
}, 1100)
真快。你只需要添加两个IF语句就行了。
事情就是这样变得复杂的:原本简单的缓存机制不再简单,而是与过期缓存机制纠缠在一起。原本简单的方案变得难以理解,运行速度更慢,而且更容易引入 bug。
每当你通过简单地添加一个IF语句来实现一个功能时,你就是在帮助它越长越大——就像一个大泥球。
如何保持原始缓存的简洁性?
与其把简单的事情复杂化,不如重复编写代码。
复制代码时,更容易看出哪些部分可以共享和重用。
构建专用工具,每个工具只做一件事。然后将这些工具组合起来,构建其他工具。
这句话以前已经说过很多遍了。
如何在不使简单缓存复杂化的情况下创建过期缓存?
在我们的示例中,过期行为可以很容易地在初始缓存实现的基础上构建:
const cache = () => {
const store = {}
const set = (key, value) => {
store[key] = value
}
const remove = key => {
const value = store[key]
delete store[key]
return value
}
return { set, remove }
}
const expire = (cache, ttl, expirationHandler) => {
const timers = {}
const set = (key, value) => {
// Store value
cache.set(key, value)
// Clear existing timer
clearTimeout(timers[key])
// Set expiration timer
timers[key] = setTimeout(() => {
const value = cache.remove(key)
delete timers[key]
expirationHandler(key, value)
}, ttl)
}
const remove = key => {
clearTimeout(timers[key])
delete timers[key]
return cache.remove(key)
}
return { set, remove }
}
// Let's use the simple cache
const simpleCache = cache()
simpleCache.set('a', 1)
simpleCache.set('b', 2)
simpleCache.set('b', 3)
console.log(simpleCache.remove('a')) // 1
console.log(simpleCache.remove('b')) // 3
console.log(simpleCache.remove('b')) // undefined
// Let's use the expiring cache
const expirationHandler = (key, value) => {
console.log(`expired ${key}: ${value}`)
}
const expiringCache = expire(cache(), 1000, expirationHandler)
expiringCache.set('a', 1)
expiringCache.set('b', 2)
console.log(expiringCache.remove('a')) // 1
console.log(expiringCache.remove('a')) // undefined
setTimeout(() => {
console.log(expiringCache.remove('b')) // undefined
}, 1100)
在某些情况下,例如本例中,工具组合使用效果很好。但在其他情况下,只能重用部分功能。将部分逻辑移至单独的函数中,可以实现功能的共享,并使其能够作为独立的工具使用。
在现有程序中引入新条件时,务必谨慎。思考哪些部分可以作为独立的、可重用的工具。不要害怕复制代码。
文章来源:https://dev.to/jorinvo/an-example-of-how-software-becomes-complicated-ahf