【代码重构】好的重构与坏的重构
【译自】 https://www.builder.io/blog/good-vs-bad-refactoring
这些年来,我雇佣过很多开发人员。他们当中有很多人都坚信我们的代码需要大量重构。但问题是:几乎在每一个案例中,其他开发人员都发现他们新重构的代码更难理解和维护。此外,代码的运行速度通常也更慢,漏洞也更多。
别误会我的意思。重构本质上并不是坏事。它是保持代码库健康的关键部分。问题在于,糟糕的重构就是糟糕。而且,在试图让事情变得更好的同时,我们很容易掉入让事情变得更糟的陷阱。
因此,让我们来看看什么是好的重构,什么是坏的重构,以及如何避免成为大家都害怕在代码库附近看到的那个开发人员。
重构的好坏与丑陋
抽象可以是好的。抽象可以是坏的。关键是要知道何时以及如何应用抽象。让我们来看看一些常见的陷阱以及如何避免它们。
1. 大幅改变编码风格
我见过的最常见的错误之一,就是开发人员在重构过程中完全改变编码风格。这种情况通常发生在来自不同背景的人或对特定编程范式有强烈意见的人身上。
让我们来看一个例子。假设我们有一段代码需要清理:
重构前:
// 🫤 this code could be cleaner
function processUsers(users: User[]) {
const result = [];
for (let i = 0; i < users.length; i++) {
if (users[i].age >= 18) {
const formattedUser = {
name: users[i].name.toUpperCase(),
age: users[i].age,
isAdult: true
};
result.push(formattedUser);
}
}
return result;
}
糟糕的重构:
import * as R from 'ramda';
// 🚩 adopted a completely different style + library
const processUsers = R.pipe(
R.filter(R.propSatisfies(R.gte(R.__, 18), 'age')),
R.map(R.applySpec({
name: R.pipe(R.prop('name'), R.toUpper),
age: R.prop('age'),
isAdult: R.always(true)
}))
);
虽然这个重构版本可能会吸引函数式编程爱好者,但它引入了一个新库(Ramda)和一种完全不同的编码风格。对于不熟悉这种方法的团队来说,维护起来可能是一场噩梦。
好的重构:
// ✅ cleaner and more conventional
function processUsers(users: User[]): FormattedUser[] {
return users
.filter(user => user.age >= 18)
.map(user => ({
name: user.name.toUpperCase(),
age: user.age,
isAdult: true
}));
}
该版本改进了原始代码,使用了 filter
和 map
等更多惯用的 JavaScript 方法,更加简洁易读,但并没有引入全新的范式或外部依赖关系。
2. 不必要的抽象
我曾经雇佣过一个人,他在不了解底层代码的情况下添加了大量新的抽象概念。他们开始将不应该分组的东西分组,并且随着时间的推移(故意)产生分歧。他们合并了一些不该合并的配置(不同的 API 需要不同的配 置)。
重构前:
// 🫤 this code could be cleaner
function processUsers(users: User[]) {
const result = [];
for (let i = 0; i < users.length; i++) {
if (users[i].age >= 18) {
const formattedUser = {
name: users[i].name.toUpperCase(),
age: users[i].age,
isAdult: true
};
result.push(formattedUser);
}
}
return result;
}
糟糕的重构:
// 🚩 there are way more layers and abstractions here than necessary
class UserProcessor {
private users: User[];
constructor(users: User[]) {
this.users = users;
}
public process(): FormattedUser[] {
return this.filterAdults().formatUsers();
}
private filterAdults(): UserProcessor {
this.users = this.users.filter(user => user.age >= 18);
return this;
}
private formatUsers(): FormattedUser[] {
return this.users.map(user => ({
name: this.formatName(user.name),
age: user.age,
isAdult: true
}));
}
private formatName(name: string): string {
return name.toUpperCase();
}
}
const processUsers = (users: User[]): FormattedUser[] => {
return new UserProcessor(users).process();
};
这种重构引入了一个具有多个方法的类,看起来似乎更 "面向对象",但实际上更复杂,也更难一目了然。
好的重构:
// ✅ cleaner and more conventional
const isAdult = (user: User): boolean => user.age >= 18;
const formatUser = (user: User): FormattedUser => ({
name: user.name.toUpperCase(),
age: user.age,
isAdult: true
});
function processUsers(users: User[]): FormattedUser[] {
return users.filter(isAdult).map(formatUser);
}
该版本将逻辑分解为可重复使用的小函数,而不会引入不必要的复杂性。
3. 增加不一致性
我曾见过这样的情况:开发人员更新代码库的一部分,使其工作方式与其他部分完全不同,试图让它变得 "更好"。这往往会给其他开发人员带来困惑和挫败感,因为他们不得不在不同风格之间进行上下文切换。
假设我们有一个 React 应用程序,在该应用程序中,我们始终使用 React Query 来获取数据:
// Throughout the app
import { useQuery } from 'react-query';
function UserProfile({ userId }) {
const { data: user, isLoading } = useQuery(['user', userId], fetchUser);
if (isLoading) return <div>Loading...</div>;
return <div>{user.name}</div>;
}
现在,想象一下开发人员决定只在一个组件中使用 Redux 工具包:
// One-off component
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchPosts } from './postsSlice';
function PostList() {
const dispatch = useDispatch();
const { posts, status } = useSelector((state) => state.posts);
useEffect(() => {
dispatch(fetchPosts());
}, [dispatch]);
if (status === 'loading') return <div>Loading...</div>;
return <div>{posts.map(post => <div key={post.id}>{post.title}</div>)}</div>;
}
这种不一致性令人沮丧,因为它仅仅为一个组件引入了完全不同的状态管理模式。
更好的方法是坚持使用 React Query:
// Consistent approach
import { useQuery } from 'react-query';
function PostList() {
const { data: posts, isLoading } = useQuery('posts', fetchPosts);
if (isLoading) return <div>Loading...</div>;
return <div>{posts.map(post => <div key={post.id}>{post.title}</div>)}</div>;
}
该版本保持了一致性,使用 React Query 在整个应用程序中获取数据。它更简单,不需要其他开发人员只为一个组件学习新的模式。
请记住,代码库的一致性非常重要。如果您需要引入一种新模式,请首先考虑如何获得团队的认同,而不是制造一次性的不一致。