一篇聊透好重構(gòu)與壞重構(gòu)
這些年來,我雇傭過很多開發(fā)人員。他們當(dāng)中有很多人都堅信我們的代碼需要大量重構(gòu)。但問題是:幾乎在每一個案例中,其他開發(fā)人員都發(fā)現(xiàn)他們新重構(gòu)的代碼更難理解和維護(hù)。此外,代碼的運(yùn)行速度通常也更慢,漏洞也更多。
別誤會我的意思。重構(gòu)本質(zhì)上并不是壞事。它是保持代碼庫健康的關(guān)鍵部分。問題是,糟糕的重構(gòu)就是糟糕。而且,在試圖讓事情變得更好的同時,我們很容易掉入讓事情變得更糟的陷阱。
因此,讓我們來看看什么是好的重構(gòu),什么是壞的重構(gòu),以及如何避免成為大家都害怕在代碼庫附近看到的那個開發(fā)人員。
重構(gòu)的好與壞
抽象可以是好的。抽象可以是壞的。關(guān)鍵是要知道何時以及如何應(yīng)用抽象。讓我們來看看一些常見的陷阱以及如何避免它們。
1. 大幅改變編碼風(fēng)格
我見過的最常見的錯誤之一,就是開發(fā)人員在重構(gòu)過程中完全改變編碼風(fēng)格。這種情況通常發(fā)生在來自不同背景的人或?qū)μ囟ň幊谭妒接袕?qiáng)烈意見的人身上。
讓我們來看一個例子。假設(shè)我們有一段代碼需要清理:
重構(gòu)之前:
// ?? 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;
}
壞的重構(gòu):
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)
}))
);
雖然這個重構(gòu)版本可能會吸引函數(shù)式編程愛好者,但它引入了一個新庫(Ramda)和一種完全不同的編碼風(fēng)格。對于不熟悉這種方法的團(tuán)隊(duì)來說,維護(hù)起來可能是一場噩夢。
好的重構(gòu):
// ? 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
}));
}
該版本改進(jìn)了原始代碼,使用了 filter 和 map 等更多慣用的 JavaScript 方法,更加簡潔易讀,但并未引入全新的范式或外部依賴關(guān)系。
2.不必要的抽象
我曾經(jīng)雇傭過一個人,他在不了解底層代碼的情況下添加了大量新的抽象概念。他們開始將不應(yīng)該分組的東西分組,并且隨著時間的推移(故意)產(chǎn)生分歧。他們合并了一些不該合并的配置(不同的 API 需要不同的配置)。
重構(gòu)之前:
// ?? 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;
}
壞的重構(gòu):
// ?? 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();
};
這種重構(gòu)引入了一個具有多個方法的類,看起來似乎更 "面向?qū)ο?,但實(shí)際上更復(fù)雜,也更難一目了然。
好的重構(gòu):
// ? 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);
}
該版本將邏輯分解為可重復(fù)使用的小函數(shù),而不會引入不必要的復(fù)雜性。
3.增加不一致性
我曾見過這樣的情況:開發(fā)人員更新代碼庫的一部分,使其工作方式與其他部分完全不同,試圖使其 "更好"。這往往會給其他開發(fā)人員帶來困惑和挫敗感,因?yàn)樗麄儾坏貌辉诓煌L(fēng)格之間進(jìn)行上下文切換。
假設(shè)我們有一個 React 應(yīng)用程序,在該應(yīng)用程序中,我們始終使用 React Query 來獲取數(shù)據(jù):
// 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>;
}
現(xiàn)在,想象一下開發(fā)人員決定只在一個組件中使用 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>;
}
這種不一致性令人沮喪,因?yàn)樗鼉H僅為一個組件引入了完全不同的狀態(tài)管理模式。
更好的方法是堅持使用 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 在整個應(yīng)用程序中獲取數(shù)據(jù)。它更簡單,不需要其他開發(fā)人員只為一個組件學(xué)習(xí)新的模式。
請記住,代碼庫的一致性非常重要。如果你需要引入一種新模式,請首先考慮如何獲得團(tuán)隊(duì)的認(rèn)同,而不是制造一次性的不一致。
4.重構(gòu)前不了解代碼
我所見過的最大問題之一就是在學(xué)習(xí)代碼的過程中,為了學(xué)習(xí)而重構(gòu)代碼。這是一個糟糕的想法。我曾看到過這樣的評論:你應(yīng)該用 6-9 個月的時間來處理一段特定的代碼。否則,你很可能會產(chǎn)生錯誤、影響性能等。
重構(gòu)之前:
// ?? a bit too much hard coded stuff here
function fetchUserData(userId: string) {
const cachedData = localStorage.getItem(`user_${userId}`);
if (cachedData) {
return JSON.parse(cachedData);
}
return api.fetchUser(userId).then(userData => {
localStorage.setItem(`user_${userId}`, JSON.stringify(userData));
return userData;
});
}
壞的重構(gòu):
// ?? where did the caching go?
function fetchUserData(userId: string) {
return api.fetchUser(userId);
}
重構(gòu)者可能會認(rèn)為他們在簡化代碼,但實(shí)際上他們已經(jīng)刪除了一個重要的緩存機(jī)制,而該機(jī)制是為了減少 API 調(diào)用和提高性能而設(shè)置的。
好的重構(gòu):
// ? cleaner code preserving the existing behavior
async function fetchUserData(userId: string) {
const cachedData = await cacheManager.get(`user_${userId}`);
if (cachedData) {
return cachedData;
}
const userData = await api.fetchUser(userId);
await cacheManager.set(`user_${userId}`, userData, { expiresIn: '1h' });
return userData;
}
此次重構(gòu)在保持緩存行為的同時,還可能通過使用更復(fù)雜的過期緩存管理器來改進(jìn)緩存行為。
5.了解業(yè)務(wù)背景
我曾經(jīng)加入過一家公司,它背負(fù)著可怕的傳統(tǒng)代碼包袱。我領(lǐng)導(dǎo)了一個項(xiàng)目,將一家電子商務(wù)公司遷移到一個新的、現(xiàn)代的、更快的、更好的技術(shù)...... angular.js
事實(shí)證明,這項(xiàng)業(yè)務(wù)在很大程度上依賴于搜索引擎優(yōu)化,而我們構(gòu)建了一個緩慢而臃腫的單頁面應(yīng)用程序。
兩年來,我們除了提供一個速度更慢、漏洞更多、可維護(hù)性更差的網(wǎng)站復(fù)制品外,什么也沒提供。為什么會這樣?領(lǐng)導(dǎo)這個項(xiàng)目的人(我--我是這個場景中的混蛋)以前從未在這個網(wǎng)站上工作過。我當(dāng)時又年輕又笨。
讓我們來看一個現(xiàn)代錯誤的例子:
壞的重構(gòu):
// ?? a single page app for an SEO-focused site is a bad idea
function App() {
return (
<Router>
<Switch>
<Route path="/product/:id" component={ProductDetails} />
</Switch>
</Router>
);
}
這種方法看似現(xiàn)代簡潔,但完全是客戶端渲染。對于嚴(yán)重依賴搜索引擎優(yōu)化的電子商務(wù)網(wǎng)站來說,這可能是災(zāi)難性的。
好的重構(gòu):
// ? server render an SEO-focused site
export const getStaticProps: GetStaticProps = async () => {
const products = await getProducts();
return { props: { products } };
};
export default function ProductList({ products }) {
return (
<div>
...
</div>
);
}
這種基于 Next.js 的方法提供開箱即用的服務(wù)器端渲染,這對搜索引擎優(yōu)化至關(guān)重要。它還能提供更好的用戶體驗(yàn),加快初始頁面加載速度,并為連接速度較慢的用戶提高性能。Remix 也同樣適用于這一目的,在服務(wù)器端渲染和搜索引擎優(yōu)化方面具有類似的優(yōu)勢。
6.過度合并代碼
我曾經(jīng)雇過一個人,他第一天在我們的后臺工作,就立即開始重構(gòu)代碼。我們有很多 Firebase 函數(shù),有些函數(shù)的設(shè)置與其他函數(shù)不同,比如超時和內(nèi)存分配。
這是我們最初的設(shè)置。
重構(gòu)之前:
// ?? we had this same code 40+ times in the codebase, we could perhaps consolidate
export const quickFunction = functions
.runWith({ timeoutSeconds: 60, memory: '256MB' })
.https.onRequest(...);
export const longRunningFunction = functions
.runWith({ timeoutSeconds: 540, memory: '1GB' })
.https.onRequest(...);
這個人決定將所有這些函數(shù)封裝在一個 createApi 函數(shù)中。
壞的重構(gòu):
// ?? blindly consolidating settings that should not be
const createApi = (handler: RequestHandler) => {
return functions
.runWith({ timeoutSeconds: 300, memory: '512MB' })
.https.onRequest((req, res) => handler(req, res));
};
export const quickFunction = createApi(handleQuickRequest);
export const longRunningFunction = createApi(handleLongRunningRequest);
這次重構(gòu)將所有 API 設(shè)置為相同的設(shè)置,而無法覆蓋每個 API。這是個問題,因?yàn)橛袝r我們需要對不同的函數(shù)進(jìn)行不同的設(shè)置。
更好的方法是允許 Firebase 選項(xiàng)通過每個應(yīng)用程序接口傳遞。
好的重構(gòu):
// ? setting good defaults, but letting anyone override
const createApi = (handler: RequestHandler, options: ApiOptions = {}) => {
return functions
.runWith({ timeoutSeconds: 300, memory: '512MB', ...options })
.https.onRequest((req, res) => handler(req, res));
};
export const quickFunction = createApi(handleQuickRequest, { timeoutSeconds: 60, memory: '256MB' });
export const longRunningFunction = createApi(handleLongRunningRequest, { timeoutSeconds: 540, memory: '1GB' });
這樣,我們既能保持抽象的優(yōu)勢,又能保留我們所需的靈活性。在合并或抽象時,請始終考慮你所服務(wù)的用例。不要為了 "更簡潔" 的代碼而犧牲靈活性。確保你的抽象實(shí)現(xiàn)了原始實(shí)現(xiàn)所提供的全部功能。
說真的,在開始 "改進(jìn)" 代碼之前,請先了解代碼。我們在下一次部署一些應(yīng)用程序接口時就遇到了問題,如果不進(jìn)行盲目的重構(gòu),這些問題是可以避免的。
如何正確重構(gòu)
值得注意的是,你確實(shí)需要重構(gòu)代碼。但要正確對待。我們的代碼并不完美,我們的代碼需要清理,但要與代碼庫保持一致,熟悉代碼,對抽象要精挑細(xì)選。
下面是一些成功重構(gòu)的技巧:
- Be incremental: Make small, manageable changes rather than sweeping rewrites.循序漸進(jìn):進(jìn)行小規(guī)模、可控的修改,而不是大刀闊斧的改寫。
- Deeply understand code before doing significant refactors or new abstractions.在進(jìn)行重大重構(gòu)或新抽象之前,深入理解代碼。
- Match the existing code style: Consistency is key for maintainability.與現(xiàn)有代碼風(fēng)格相匹配:一致性是可維護(hù)性的關(guān)鍵。
- Avoid too many new abstractions: Keep it simple unless complexity is truly warranted.避免過多的新抽象概念:保持簡單,除非確實(shí)需要復(fù)雜。
- Avoid adding new libraries, especially of a very different programming style, without buy-in from the team.避免在未獲得團(tuán)隊(duì)認(rèn)可的情況下添加新的庫,尤其是編程風(fēng)格迥異的庫。
- Write tests before refactoring and update them as you go. This ensures you're maintaining the original functionality.在重構(gòu)前編寫測試,并在重構(gòu)過程中更新測試。這能確保你保持原有的功能。
- Hold your coworkers accountable to these principles.讓你的同事對這些原則負(fù)責(zé)。
圖片
更好地重構(gòu)的工具和技術(shù)
為了確保你的重構(gòu)是有益而非有害的,請考慮使用以下技術(shù)和工具:
規(guī)范工具
使用規(guī)范工具來執(zhí)行一致的代碼風(fēng)格并捕捉潛在的問題。Prettier 可以幫助自動格式化為一致的風(fēng)格,而 Eslint 則可以幫助進(jìn)行更細(xì)致的一致性檢查,你可以用自己的插件輕松定制。
代碼審查
在合并重構(gòu)代碼之前,實(shí)施全面的代碼審查,以獲得同行的反饋意見。這有助于及早發(fā)現(xiàn)潛在問題,并確保重構(gòu)代碼符合團(tuán)隊(duì)標(biāo)準(zhǔn)和期望。
測試
編寫并運(yùn)行測試,確保重構(gòu)代碼不會破壞現(xiàn)有功能。Vitest[1] 是一款快速、可靠、易用的測試運(yùn)行程序,默認(rèn)情況下無需任何配置。對于可視化測試,可以考慮使用 Storybook[2]。React Testing Library[3] 是一套用于測試 React 組件的實(shí)用工具(還有 Angular 和更多變體)。
人工智能工具
讓人工智能來幫助你進(jìn)行重構(gòu),至少是那些能夠與你現(xiàn)有的編碼風(fēng)格和習(xí)慣相匹配的重構(gòu)。
結(jié)論
重構(gòu)是軟件開發(fā)的必要組成部分,但在進(jìn)行重構(gòu)時需要深思熟慮,并尊重現(xiàn)有的代碼庫和團(tuán)隊(duì)動態(tài)。重構(gòu)的目的是在不改變代碼外部行為的情況下改進(jìn)代碼的內(nèi)部結(jié)構(gòu)。
請記住,最好的重構(gòu)往往是最終用戶看不到的,但卻能大大方便開發(fā)人員的工作。它們能提高可讀性、可維護(hù)性和效率,而不會破壞整個統(tǒng)。
下一次,當(dāng)你有為一段代碼制定 "大計劃" 的沖動時,請退后一步。徹底了解它,考慮更改帶來的影響,然后逐步改進(jìn),你的團(tuán)隊(duì)一定會感謝你的。
本文譯自:https://www.builder.io/blog/good-vs-bad-refactoring
Reference
[1] Vitest: https://vitest.dev/
[2] Storybook: https://storybook.js.org/
[3] React Testing Library: https://github.com/testing-library/react-testing-library