譯者 | 劉汪洋
審校 | 重樓
多年來,我招聘了許多開發人員,其中一些人堅信代碼需要頻繁重構。然而,事實是,幾乎每次他們完成重構并將代碼交付給其他開發人員時,大家往往發現這些代碼反而變得更難理解和維護。更糟糕的是,重構后的代碼通常運行效率更低,且問題頻發。
需要明確的是,重構本身并無不妥。它是保持代碼庫健康和可持續發展的關鍵。然而,不當的重構會帶來負面影響,試圖改進代碼時出現的錯誤,往往會適得其反,這種情況并不罕見。
接下來,我們將探討如何區分好的重構與不良重構,并討論如何避免成為那個讓團隊成員都不愿意接觸代碼庫的開發者。
重構的優點、缺點與陷阱
在編程中,抽象既可能帶來好處,也可能造成問題,關鍵在于何時以及如何應用。下面,我們將探討一些常見的陷阱,并討論如何避免這些問題。
1. 大幅改變編碼風格
我經常看到開發人員在重構過程中完全改變編碼風格,這是最常見的錯誤之一。通常,這種情況發生在開發人員來自不同背景或對某種編程范式有強烈偏好的情況下。
讓我們來看一個例子。假設我們有一段需要重構的代碼:
重構前:
// ?? 這段代碼可以更簡潔
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';
// ?? 采用了完全不同的風格和庫
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)以及一種完全不同的編碼風格。對于不熟悉這種方法的團隊來說,維護起來可能會非常困難。
好的重構:
// ? 更加簡潔且更符合慣例
function processUsers(users: User[]): FormattedUser[] {
return users
.filter(user => user.age >= 18)
.map(user => ({
name: user.name.toUpperCase(),
age: user.age,
isAdult: true
}));
}
這個版本通過使用 JavaScript 的 filter
和 map
方法改進了原始代碼。它不僅更簡潔易讀,而且保持了代碼庫的連貫性,沒有引入新的編程范式或外部依賴。
2. 不必要的抽象
我曾經遇到一位開發人員,他在不了解底層代碼的情況下,添加了大量不必要的抽象。他將一些不應歸類在一起的組件強行合并,導致代碼變得難以維護。此外,他還將不應合并的配置(例如不同 API 需要的配置)混在一起,使得后續管理變得復雜。
重構前:
// ?? 這段代碼可以更簡潔
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;
}
不好的重構:
// ?? 這里引入了過多的層級和抽象
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();
};
這個重構引入了一個多方法的類,雖然看似更“面向對象”,但實際上增加了不必要的復雜性,導致代碼變得難以理解和維護。
好的重構:
// ? 更加簡潔且更符合慣例
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:
// 應用程序的其他部分
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 Toolkit:
// 不一致的做法
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:
// 保持一致的做法
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 進行數據獲取。這樣不僅簡化了代碼,還避免了其他開發人員因單一組件而不得不學習新的模式。
請記住,代碼庫中的一致性至關重要。如果你需要引入新的模式,請確保先與團隊達成共識,而不是孤立地引入不一致性。
4. 在未理解代碼的情況下進行重構
我見過的最大問題之一是,有些開發人員在尚未充分理解代碼的情況下進行重構,以此作為學習代碼的手段。這種做法往往會帶來嚴重后果。經驗表明,你應該至少在使用某段代碼 6-9 個月后再進行重構,否則可能會引入 bug 或降低性能。
重構前:
// ?? 這里的硬編碼較多
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;
});
}
不好的重構:
// ?? 緩存機制被移除
function fetchUserData(userId: string) {
return api.fetchUser(userId);
}
重構者可能認為他們簡化了代碼,但實際上他們移除了減少 API 調用并提升性能的緩存機制。
好的重構:
// ? 保留現有行為的同時簡化了代碼
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;
}
這個重構保留了緩存行為,并通過使用更復雜的緩存管理器(帶有過期時間設置)來優化緩存機制。
5. 理解業務背景
在未充分理解業務背景的情況下進行重構,可能導致項目失敗。我曾在一家維護大量遺留代碼的公司工作,并領導了一個將其電商平臺遷移到新技術上的項目。當時我們選擇了 Angular.js 作為技術棧。然而,這家公司高度依賴 SEO,而我們卻構建了一個緩慢且臃腫的單頁應用程序(SPA)。兩年后,發布的結果是一個更慢且漏洞百出的難以維護的網站。原因在于,作為項目負責人,我之前從未在這個網站上工作過,缺乏對其業務背景的理解。我當時年輕且經驗不足。
讓我們來看看一個類似的現代例子:
不好的重構:
// ?? 為依賴 SEO 的網站構建單頁應用程序是個糟糕的主意
function App() {
return (
<Router>
<Switch>
<Route path="/product/:id" component={ProductDetails} />
</Switch>
</Router>
);
}
這種代碼看似現代且簡潔,但它完全依賴客戶端渲染。對于一個高度依賴 SEO 的電商網站,這可能會帶來災難性的后果。
好的重構:
// ? 為 SEO 優化的網站使用服務器端渲染
export const getStaticProps: GetStaticProps = async () => {
const products = await getProducts();
return { props: { products } };
};
export default function ProductList({ products }) {
return (
<div>
...
</div>
);
}
這個基于 Next.js 的方法提供了開箱即用的服務器端渲染功能,對 SEO 至關重要。它不僅提升了初始頁面的加載速度,還為網絡連接較慢的用戶提供了更好的體驗。Remix 也適用于這種場景,提供了類似的服務器端渲染和 SEO 優化優勢。
6. 過度整合代碼
我曾招聘過一名程序員,他在加入團隊的第一天就開始重構代碼。我們有許多 Firebase 函數,不同函數在超時和內存分配方面的配置有所不同。 這是我們原有的設置:
重構前:
// ?? 代碼庫中有超過 40 處相同的代碼,或許可以進行整合
export const quickFunction = functions
.runWith({ timeoutSeconds: 60, memory: '256MB' })
.https.onRequest(...);
export const longRunningFunction = functions
.runWith({ timeoutSeconds: 540, memory: '1GB' })
.https.onRequest(...);
這位新人決定將所有這些函數封裝到一個 createApi
函數中。
不好的重構:
// ?? 盲目地整合了不應整合的設置
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);
這種重構將所有 API 的配置統一為相同的參數,并且沒有提供單獨覆蓋的選項。問題在于,不同的函數可能需要不同的配置參數。
更好的方法是允許每個 API 傳遞自定義的 Firebase 配置:
好的重構:
// ? 設置了良好的默認值,但允許覆蓋
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' });
這種方法在保留抽象優勢的同時,也保留了必要的靈活性。在整合或抽象代碼時,一定要考慮所屬服務的具體用例。不要為了追求“簡潔”而犧牲代碼的靈活性。確保你的抽象能夠滿足原始實現的所有需求。
最重要的是,在開始“改進”代碼之前,必須深入理解它。我們曾在下一次部署 API 時遇到問題,而這些問題本可以通過避免這種盲目重構來規避。
如何正確地進行重構
正確地進行代碼重構至關重要。盡管代碼庫不可能完美無缺,并且重構在某些時候是必要的,但重構時必須保持代碼庫的一致性,并在深入理解代碼的基礎上謹慎處理抽象。
以下是一些成功重構的建議:
- 逐步進行:采取小幅、可控的更改,而非大規模的重寫。
- 深入理解代碼:在進行重大重構或引入新抽象之前,務必充分理解現有代碼。
- 保持與現有代碼風格一致:一致性是提升代碼可維護性的關鍵。
- 避免過度抽象:保持簡單,除非確實有必要增加復雜性。
- 避免引入新庫:特別是那些風格迥異的庫,除非團隊已達成共識,否則不要輕易引入。
- 在重構前編寫測試,并在重構過程中更新測試。這能確保在改進過程中維持原有功能的穩定性。
- 確保團隊成員遵循這些原則:確保所有團隊成員都遵循這些重構原則,每個成員都應對其負責。
提高重構質量的工具和技巧
為確保重構能有效提升代碼質量,可以考慮以下工具和技巧:
Linting 工具
使用 Linting 工具強制執行一致的代碼風格并捕捉潛在問題。Prettier 可以自動格式化代碼以保持風格一致,而 Eslint 能進行更細致的檢查,且支持通過自定義插件適應團隊的特殊需求。
代碼審查
在合并重構后的代碼之前,進行徹底的代碼審查并獲取同事的反饋。這樣有助于及早發現潛在問題,確保代碼符合團隊標準和預期。
測試
編寫并運行測試,確保重構不會破壞現有功能。Vitest 是一個快速、穩定且易于使用的測試工具,默認情況下無需復雜配置。對于視覺測試,可以使用 Storybook。React Testing Library 是測試 React 組件的優秀工具,而 Angular Testing Library 及其他變種適用于不同框架。
AI 工具
合理利用 AI 工具進行重構,特別是那些能夠匹配現有編碼風格和約定的工具。 Visual Copilot 是一個前端開發中非常有用的 AI 工具,它有助于將設計轉化為代碼,同時保持編碼風格的一致性,并正確使用設計系統的組件和標記。這些工具和技巧可以幫助你在重構過程中保持代碼質量,確保重構帶來的改進是可持續且有意義的。
這些工具和技巧可以幫助你在重構過程中保持代碼質量,確保重構帶來的改進是可持續且有意義的。
結論
重構是軟件開發中不可或缺的一部分,但它必須經過深思熟慮,并尊重現有代碼庫和團隊的工作方式。重構的目標是在不改變代碼外部行為的情況下,優化其內部結構。
請記住,最好的重構往往是讓終端用戶毫無察覺,卻能極大地方便開發人員。通過提升代碼的可讀性、可維護性和效率,同時保持系統的穩定性,你將為整個團隊創造更高的工作效率并減少技術債務。
所以,下次當你有“大計劃”要改進某段代碼時,先停下來,深入理解這段代碼,評估改動可能帶來的影響,并選擇團隊會感謝的漸進式改進方法。未來的你(以及你的同事們)一定會感激你這種周到且維護良好的代碼庫的做法。
譯者介紹
劉汪洋,51CTO社區編輯,昵稱:明明如月,一個擁有 5 年開發經驗的某大廠高級 Java 工程師,擁有多個主流技術博客平臺博客專家稱號,博客閱讀量 400W+,粉絲 3W+。2022 年騰訊云優秀創作者,2022 年阿里云技術社區最受歡迎技術電子書 TOP 10 《性能優化方法論》作者,慕課網:剖析《阿里巴巴 Java 開發手冊》、深度解讀《Effective Java》 技術專欄作者。
原文標題:Good Refactoring vs Bad Refactoring,作者:Steve Sewell
鏈接:https://dev.to/builderio/good-refactoring-vs-bad-refactoring-2361