Categories
程式開發

把同事的代碼重寫得乾淨又整潔,老闆卻讓我做回滾?


夜深了。

我的同事把這週寫的代碼提交了。我們在開發一個圖形編輯器畫布,已經實現了形狀調整功能,即通過拖拽形狀邊緣的手柄來調整形狀(比如矩形和橢圓形)。

代碼可以運行。

但重複代碼有點多。每一種形狀(比如矩形和橢圓形)有不同的手柄,往不同方向拖拽手柄對形狀的位置和大小影響也不一樣。如果用戶同時按住Shift鍵,在改變大小的同時要保持比例不變。這裡涉及了很多數學運算。

代碼看起來像這樣:

let Rectangle = {
  resizeTopLeft(position, size, preserveAspect, dx, dy) {
    // 10行重复的数学运算代码
  },
  resizeTopRight(position, size, preserveAspect, dx, dy) {
    // 10行重复的数学运算代码
  },
  resizeBottomLeft(position, size, preserveAspect, dx, dy) {
    // 10行重复的数学运算代码
  },
  resizeBottomRight(position, size, preserveAspect, dx, dy) {
    // 10行重复的数学运算代码
  },
};

let Oval = {
  resizeLeft(position, size, preserveAspect, dx, dy) {
    // 10行重复的数学运算代码
  },
  resizeRight(position, size, preserveAspect, dx, dy) {
    // 10行重复的数学运算代码
  },
  resizeTop(position, size, preserveAspect, dx, dy) {
    // 10行重复的数学运算代码
  },
  resizeBottom(position, size, preserveAspect, dx, dy) {
    // 10行重复的数学运算代码
  },
};

let Header = {
  resizeLeft(position, size, preserveAspect, dx, dy) {
    // 10行重复的数学运算代码
  },
  resizeRight(position, size, preserveAspect, dx, dy) {
    // 10行重复的数学运算代码
  },  
}

let TextBlock = {
  resizeTopLeft(position, size, preserveAspect, dx, dy) {
    // 10行重复的数学运算代码
  },
  resizeTopRight(position, size, preserveAspect, dx, dy) {
    // 10行重复的数学运算代码
  },
  resizeBottomLeft(position, size, preserveAspect, dx, dy) {
    // 10行重复的数学运算代码
  },
  resizeBottomRight(position, size, preserveAspect, dx, dy) {
    // 10行重复的数学运算代码
  },
};

這些重複代碼看起來真的很礙眼。

這樣的代碼不夠乾淨。

大部分重複是因為朝相同方向調整形狀的代碼都差不多,比如Oval.resizeLeft()和Header.resizeLeft()就很類似。

其他重複是因為同一種形狀的方法之間很相像,比如Oval.resizeLeft()和Oval其他的方法就很類似。另外,Rectangle、Header和TextBlock之間也有重複的地方,因為文本框也是矩形。

我想到了一個辦法。

我們可以給代碼分組,把重複代碼移除掉,比如像下面這樣。

let Directions = {
  top(...) {
    // 5行不一样的数学运算代码
  },
  left(...) {
    // 5行不一样的数学运算代码
  },
  bottom(...) {
    // 5行不一样的数学运算代码
  },
  right(...) {
    // 5行不一样的数学运算代码
  },
};

let Shapes = {
  Oval(...) {
    // 5行不一样的数学运算代码
  },
  Rectangle(...) {
    // 5行不一样的数学运算代码
  },
}

然後,把它們的行為組合起來。

let {top, bottom, left, right} = Directions;

function createHandle(directions) {
  // 20行代码
}

let fourCorners = [
  createHandle([top, left]),
  createHandle([top, right]),
  createHandle([bottom, left]),
  createHandle([bottom, right]),
];
let fourSides = [
  createHandle([top]),
  createHandle([left]),
  createHandle([right]),
  createHandle([bottom]),
];
let twoSides = [
  createHandle([left]),
  createHandle([right]),
];

function createBox(shape, handles) {
  // 20行代码
}

let Rectangle = createBox(Shapes.Rectangle, fourCorners);
let Oval = createBox(Shapes.Oval, fourSides);
let Header = createBox(Shapes.Rectangle, twoSides);
let TextBox = createBox(Shapes.Rectangle, fourCorners);

代碼量減少了一半,重複代碼完全消失了!多麼乾淨。如果要修改某個形狀或方向的行為,只需要在一個地方做出改動,不需要修改所有的方法。

夜已深,我把改好的代碼提交到master分支,然後上床睡覺。因為幫同事把雜亂的代碼清理乾淨了,我心裡還引以為豪。

第二天

事情並沒有像我期待的那樣發生。

老闆找我談話,他們希望我把代碼回滾回去。我感到很驚訝,畢竟原先的代碼簡直就是一團亂麻,而我改得很乾淨啊!

我很不情願地答應了,但幾年之後,我才意識到他們其實是對的。

必經之路

痴迷於“乾淨代碼”和刪除重複代碼是我們很多人都會經歷的一個階段。當我們對自己的代碼不是很自信時,就很容易將自我價值感和職業自豪感與一些可以被衡量的東西聯繫在一起,比如嚴格的lint規則、命名模式、文件結構、不重複代碼實踐。

我們沒辦法自動去除重複代碼,但可以自己動手做。每次修改代碼之後,我們可以很容易地知道重複代碼是少了還是多了。所以,去除重複代碼感覺就像是在改進代碼質量。更糟糕的是,它擾亂了人們的認同感,讓他們覺得“我是那種編寫乾淨代碼的人”,但這其實無異於自我欺騙。

一旦學會了抽象,我們就很容易對這種能力產生很高的期望,每當看到有重複代碼就會想要對它們進行抽象。在寫了幾年代碼之後,我們發現重複代碼到處都是,而抽象成了我們獲得的一項超級能力。如果有人告訴我們說抽像是一種美德,那我們肯定會深信不疑,並且會因為別人不崇尚“乾淨代碼”而對他們品頭論足。

現在,我知道之前的代碼重構就是一個災難,原因如下。

  • 首先,我沒有事先和寫代碼的人溝通。我直接修改了他們的代碼並提交,沒有和他們討論。即使這是一種改進(但我現在不這麼認為了),但我這樣的行事方式並不值得稱道。一個健康的工程團隊應該以信任為基礎,不經過討論就修改他人的代碼會對團隊協作造成沉重的打擊。

  • 其次,天下沒有免費的午餐。我以犧牲靈活性為代價,以此來減少重複代碼,這算不上是一個好的權衡。例如,後來我們要求不同形狀的不同手柄具備一些特殊的行為,被我重構過的代碼需要修改多次才能滿足需求,而原先“雜亂”的代碼卻可以很容易實現這些需求。

那麼,我的意思是我們應該盡量寫“臟”代碼嗎?當然不是。我只是建議大家在考慮什麼是“乾淨”或“臟”代碼時進行深度思考。你當時有什麼樣的感覺?厭惡?正義?美麗?優雅?你可以肯定這些品質會帶來實質性的工程成果嗎?它們又是如何影響代碼的編寫和修改方式的?

我確實沒有深入思考過這些事情。我只考慮到代碼本身,但從來沒有想過代碼與團隊之間的演化關係。

編碼就像是一段旅程,想想你從寫第一行代碼到現在走了多遠。當第一次通過提取函數或重構類讓複雜的代碼變簡單,我覺得那是一種樂趣。如果你對自己的“傑作”感到自豪,那麼就很容易掉入追求乾淨代碼的旋渦。

但請不要止步於此,不要只做一個乾淨代碼狂熱者。寫出乾淨的代碼並不是我們的終極目標,我們只是通過這種方式嘗試找到處理系統複雜性的方法。當你不確定代碼改動會對代碼庫造成怎樣的影響,在未知的海洋中需要燈塔的指引,那麼這不失為一種防禦機制。

寫出乾淨的代碼可以作為一種指引,但後面的路還是要自己走。

原文鏈接

Goodbye clean code!