我曾经就是那个开发者——把本已完美的功能代码重构为"整洁代码"的典范,然后翘首以盼从未到来的赞美。
如果你也曾花费数小时让代码变得"优雅",却换来沉默的怨恨或充满暗示的代码评审,这篇文章或许能解释原因。在咨询数百个开发团队并为Codestop客户进行代码评审后,我收集了开发者对"整洁代码"神圣原则的真实不满。
准备好面对现实吧——当你的同事读到这篇文章时,他们可能正在频频点头。
在深入探讨之前,我们必须承认:整洁代码原则的诞生有其合理性。随着代码库膨胀,可维护性变得至关重要。Robert C. Martin(Uncle Bob)在其经典著作《整洁代码》中推广的理念本身并无错误——但狂热的应用往往弊大于利。
就像所有教条主义,盲目追随才是问题根源。
我们都听过这个说法:"好代码应该自我注释!添加注释说明是能力不足的表现!"
看看这个真实案例:
// 原始代码
if (user.lastLoginDate < (Date.now() - 7776000000)) {
notifyUserOfInactivity(user);
}
// "整洁"版本
if (user.isInactive()) {
notifyUserOfInactivity(user);
}
看似优雅?代码现在"不言自明"。
除了...现在同事必须全局搜索isInactive()方法才能理解业务逻辑。而当他们找到时,会发现所谓"inactive"的定义可能因用户偏好、订阅等级或管理员设置而变化。
原本通过"7776000000"(90天的毫秒数)至少能暗示的上下文,现在被隐藏在一个看似清晰实则抽象的方法调用背后。
同事痛恨的原因:他们花费更多时间在"整洁"的抽象中寻找答案,而不是阅读解释业务规则的注释。
正确做法:仅在真正能澄清问题时使用抽象。更重要的是,添加解释"为什么"而不仅是"什么"的注释。
Uncle Bob建议"函数应该专注做一件事,并且做好"、"函数应该短小精悍"。
但极端应用会导致这种代码:
function validateUserInput() {
validateUsername();
validatePassword();
validateEmail();
validateAddressInformation();
}
function validateUsername() {
validateUsernameLength();
validateUsernameCharacters();
checkUsernameAvailability();
}
function validateUsernameLength() {
// 5行代码实现
}
// ...依此类推
啊,小巧函数的优美!除了现在一个简单验证流程分散在4个文件的15个函数中,同事需要跳转追踪才能理解逻辑。
同事痛恨的原因:过度函数分解造成认知负担。这种分解实际上人为制造了碎片化,使代码更难理解。
正确做法:将函数视为理解单元。如果理解一个函数需要理解其他5个函数,就说明抽象过度了。
"有意义、能揭示意图的命名"这一整洁代码信条,一旦走极端就会变成灾难:
// 原始代码
const x = data.filter(i => i.status === 'active');
// "整洁"版本
const activeAndVerifiedCustomersWhoHaveOptedIntoMarketingEmails =
customers.filter(customer =>
customer.status === 'active' &&
customer.isVerified &&
customer.preferences.marketing.optIn);
等等...这完全曲解了原始代码的意图!这个"整洁"的变量名引入了原代码中并不存在的假设和特异性。现在修改时,必须仔细推敲每个条件是否真的适用。
同事痛恨的原因:过度具体的变量名制造虚假信心。其他人会以为名称完全描述了变量的所有含义,但随着代码演进,名称和实现会产生偏离。
正确做法:保持描述准确但不冗余。变量名长度应以准确传达意思为限,绝不要包含未在实现中反映的信息。
"不要重复自己"(DRY)可能是被误用最危险的原则。它导致这样的代码:
// Before:两个相似但不同的表单验证函数
function validateSignupForm(formData) {
// 注册专用验证逻辑,有10行与登录验证相似的代码
}
function validateLoginForm(formData) {
// 登录专用验证逻辑,有10行与注册验证相似的代码
}
// After:"DRY"版本
function validateForm(formData, formType) {
// 通用验证逻辑
if (formType === 'signup') {
// 注册专用逻辑
} else if (formType === 'login') {
// 登录专用逻辑
} else if (formType === 'passwordReset') {
// 后续添加的需求
} else if (formType === 'accountDeletion') {
// 更后续添加的需求
}
// ...随着表单类型增加而持续扩展
}
最初是消除重复的善意尝试,最终演变成处理不断增长职责的臃肿函数。
同事痛恨的原因:过早抽象导致本不应耦合的组件产生依赖。当修改注册验证时,必须小心处理同时负责登录、密码重置和账户删除的代码。
正确做法:认识到某些重复优于错误的抽象。看似相似的代码可能只是巧合相似而非本质相关。遵循三次法则——等到出现三次重复再抽象。
没有什么比花数天解开某人为"面向未来"创造的复杂抽象更让开发者沮丧的了:
// 需求本质:
function fetchUserData(userId) {
return api.get(`/users/${userId}`);
}
// 最终产物:
class DataAccessLayer {
constructor(config) {
this.baseUrl = config.baseUrl;
this.authProvider = config.authProvider;
this.cacheStrategy = config.cacheStrategy || new NoCache();
// ...20多个配置选项
}
async fetch(resourceType, identifier, options = {}) {
// 200行"灵活"代码
}
}
// 使用时:
const dataLayer = new DataAccessLayer({
baseUrl: config.apiUrl,
authProvider: new OAuth2Provider(config.clientId, config.clientSecret),
// ...更多选项
});
async function fetchUserData(userId) {
return dataLayer.fetch('user', userId, { fields: ['profile', 'settings'] });
}
初衷良好:创建能处理所有未来需求灵活的数据层。现实?一个错综复杂的抽象,使简单任务变复杂,且一旦需求变更就迅速过时。
同事痛恨的原因:过度设计的抽象让简单事情复杂化,制造了灵活性的假象,实际上却限制了未来发展路径。
正确做法:践行YAGNI原则(You Aren't Gonna Need It)。为现有需求而非想象中的可能需求构建。
函数式编程原则已渗透主流开发,常打着"整洁代码"的旗号。虽然不变性和纯函数等优点,但教条式应用可能导致性能问题:
function processItems(items) {
return items
.filter(item => item.isValid)
.map(item => transformItem(item))
.map(item => enrichItem(item))
.filter(item => item.meets.criteria)
.reduce((acc, item) => {
return [...acc, finalizeItem(item)];
}, []);
}
这段代码创建多个中间数组,每次分配新内存。对于小集合不成问题,但对大数据集可能导致显著性能问题。
同事痛恨的原因:他们不得不向利益相关者解释为什么"整洁"的重构比"混乱"的原生代码慢得多。
正确做法:理解不同编程范式的性能影响。有时传统的for循环才是正确选择,特别是在性能敏感场景。
设计模式是工具,不是荣誉徽章。但有些开发者像收集宝可梦一样收集它们:
// 需求本质:简单发送通知的方式
function sendNotification(user, message) {
if (user.preferences.notificationMethod === 'email') {
emailService.send(user.email, message);
} else if (user.preferences.notificationMethod === 'sms') {
smsService.send(user.phone, message);
}
}
// 最终产物:模式狂欢
// NotificationStrategy.js
class NotificationStrategy {
send(user, message) {
throw new Error('Strategy not implemented');
}
}
// EmailStrategy.js
class EmailStrategy extends NotificationStrategy {
send(user, message) {
return emailService.send(user.email, message);
}
}
// SMSStrategy.js, PushStrategy.js, WebhookStrategy.js 等等
// NotificationFactory.js
class NotificationFactory {
createStrategy(method) {
if (method === 'email') return new EmailStrategy();
if (method === 'sms') return new SMSStrategy();
// ...其他策略
throw new Error(`Unknown notification method: ${method}`);
}
}
// NotificationFacade.js
class NotificationFacade {
constructor(factory = new NotificationFactory()) {
this.factory = factory;
}
sendNotification(user, message) {
const strategy = this.factory.createStrategy(user.preferences.notificationMethod);
return strategy.send(user, message);
}
}
// 使用
const notifier = new NotificationFacade();
notifier.sendNotification(user, message);
原本7行的简单函数现在分散在多个文件中,充斥着类层级结构和间接层。
同事痛恨的原因:过度使用设计模式制造了不必要复杂性。每个模式引入的间接层并没有带来相应的价值提升。
正确做法:仅在解决实际问题时使用设计模式,而非为了代码"看起来专业"或尝试刚学到的新pattern。
或许最隐蔽的"整洁代码"反模式是写出让人觉得自己愚笨的"聪明"代码:
// 所有人都能理解的原始代码
let total = 0;
for (let i = 0; i < items.length; i++) {
if (items[i].isSelected) {
total += items[i].price;
}
}
// "优雅"的函数式一行代码
const total = items.reduce((sum, { isSelected, price }) => sum + (isSelected ? price : 0), 0);
虽然函数式版本更简短,但也更密集。对熟悉函数式编程的人可能易读——但对许多其他人,需要更多思维努力才能解析。
同事痛恨的原因:这种代码制造恐惧和怨恨。开发者害怕修改自己不完全理解的"聪明"代码。
正确做法:编写为人而读的代码,而非为了打动其他开发者。清晰度优先于简洁性。请记住,六个月后再次阅读代码的可能是你自己——那时你可能已忘记当初使用的巧妙技巧。
如果我对整洁代码原则的批评显得严苛,那只是因为我亲眼见过它们的误用造成的伤害。这些原则本身——可读性、简洁性、可维护性——是值得追求的目标。问题在于脱离上下文、团队动态和业务需求的教条式应用。
以下是我的替代方案:
• 为理解而优化,而非纯粹性。如果"整洁"方法让代码更难理解,它实际上并不整洁 • 考虑团队整体经验水平。你的代码将被不同经验水平的开发者阅读和修改。为现有团队写作,而非你理想中的团队 • 重视领域清晰度而非技术优雅。清晰表达业务概念的代码比展示技术实力的代码更有价值 • 添加解释"为什么"的注释。再整洁的代码也无法解释业务规则存在的原因或历史背景。好的注释是无价的 • 明智地接受权衡。有时性能比纯粹性更重要。有时快速交付比完美抽象更重要。要有意识地做出这些权衡
整洁代码原则本身没有错——但它们也不是普世真理。它们是需要谨慎应用的启发式原则,要考虑到上下文、团队动态和业务需求。
下次当你忍不住想重构"工作正常"的代码使其"更整洁"时,请问问自己:"我这样做是为下一个需要理解代码的开发者提供便利,还是仅仅为了让自己感觉聪明?"
如果开始考虑同事的需求而非仅仅是自己的审美偏好,或许你的同事最终会停止痛恨你的"整洁"代码。