重構:幹掉有壞味道的程式碼

xybaby發表於2020-05-19

第一次讀重構 - 改善既有程式碼的設計(Refactoring: Improving the Design of Existing Code)這本書還是在學校的時候,那個時候剛開始寫Java程式碼,師兄推薦了兩本書《重構》、《設計模式》。在今日看來,這兩本書都是經典好書,得謝謝我的師兄。

最近,打算重新讀一下這兩本書,先讀了重構,感覺還是收穫頗多。想來這兩本書都是比較偏向實踐的,並不是讀一遍就可以束之高閣了,而是應該常讀常新。

本文地址:https://www.cnblogs.com/xybaby/p/12894470.html



去年重讀了程式碼整潔之道這本書,也寫了一篇筆記 什麼是整潔的程式碼。今年重讀《重構》的時候,發現書中很多內容都是相同的,作者好像都是叫 Martin 什麼的,我還想難道是同一個人?

查了一下,並不是,重構的作者是 Martin Fowler;而clean code的作者是 Robert C. Martin ,江湖人稱 "Uncle Bob"。

不過好像兩位都在物件導向、敏捷領域有所建樹。By the way,重構的第一版寫於1999年,(本文也基於第一版的譯文),而clean code的第一版寫於2009年,且clean code是有參考 “refactoring: Improving the Design of Existing Code”的。

在我看來,重構這本書的核心價值有三部分:

  • 指出有“壞味道”的程式碼
  • 對這種程式碼給出重構的詳細步驟,這些步驟保證重構過程是安全的
  • 關於引入新技術、新思想的一些思考,如重構、程式碼複用、TDD

當然,第二部 -- 針對各種有問題的程式碼的重構步驟 -- 是本書的重點,不過現在的IDE都提供了對重構的支援,大大提升了重構的效率和安全性。

認清重構的事實

書名叫 Refactoring: Improving the Design of Existing Code ,作者也著重強調:重構是在不改變軟體可觀察行為的前提下改善其內部結構。也就是說,對外的API,以及API的行為不應該被改變,不要在重構的同時修bug,或者新增新功能。

重構是為了改善程式的內部結構,而改善的目的在於增加程式碼的可讀性,讓程式碼更容易維護和修改。

我們也常常為了提升效能而修改程式碼,不幸的是,為了效能而實施的修改通常讓程式碼變得難以維護,標誌就是得加註釋說明為什麼要這麼修改。

重構的前提

不管怎麼樣,手動還是藉助工具,重構還是會修改程式碼,只要修改程式碼,就可能引入錯誤。那麼重構給出了就是一套經過驗證的、有條不紊整理程式碼的方法,通過逐步改進、及時測試、出錯則回滾的方法來最小化引入bug的概率。

上面提到了逐步驗證,這就需要在重構的時候需要有可靠的、自動的測試環境,如果沒有靠譜的測試方案,那麼最好還是不要重構。

什麼時候重構

程式設計師新學得一個技能, 比如重構,就很容易認為這是解決程式設計問題的屠龍技,迫不及待想找個環境用起來,但只有在合適的時機使用才能發揮其效用。

  • 增加新的功能前
  • 修改bug前
  • code review時

成功的軟體都需要長時間的維護、迭代,那麼我們程式設計師難免就會接受其他程式設計師的遺產:程式碼以及bug。如果需要在舊程式碼上加新功能,但舊程式碼的混亂程度又讓人無從下手,該怎麼辦呢?

  • 重寫:既然之前的程式碼很SB,那我就重新寫點NB的程式碼。但現實很殘酷,重寫的時間、人力成本是多少?引入的新BUG怎麼算?況且,如果貿然動手,新造的輪子還可能不如原來的輪子。
  • 複製、修改:看看系統中有沒有類似的功能模組,複製過來,改一改,如果恰好能工作,那就萬事大吉。但我們知道,重複的程式碼是不好的,bug、“有壞味道”的程式碼也被複制和傳播。
  • 重構:處於重寫與複製的中間狀態,在不修改程式的外在表現的情況下,改善程式碼的質量,也讓新功能的新增更加容易。這也符合clean code中提到的童子軍軍規:

讓程式碼越來越好,而不是越來越壞

重構與設計

不管是瀑布流模型開發,還是敏捷開發,都是需要有設計的。過度設計和不做設計都是有問題的,而重構簡化了設計:無需過度追求靈活些,合理即可。所謂靈活些,即可應對各種需求變化,但靈活的系統比簡單的系統複雜得多,且難以維護。

重構使得修改物件導向的程式設計變的很容易,因為可以重構繼承體系,將field、method移動到不同的類中,通過多型移除各種複雜的條件判斷。某種程度上,重構可以簡化詳細設計,但不能替代架構設計,或者說概要設計。

值得注意的是:

  • 本書的重構手法只適合單程式單執行緒程式,而不一定適合多執行緒、分散式。對於多執行緒,一個簡單的inline就可能導致各種問題。而對於分散式系統的重構,更多的是架構層面的設計。
  • 越難重構的地方,越需要精心設計,比如資料庫欄位,通訊協議,對外介面。保持對舊協議的相容是一件非常麻煩的事情。

有“壞味道”的程式碼

需要重構的程式碼往往都散發著“壞味道”,讓專業的程式設計師感受到不舒服。這一部分,羅列了作者總結的“壞味道”。

需要注意的是,本書羅列的壞味道不一定很全面,比如一個變數命名為temp,大概率就是一個壞味道,但本書中就未提及這種情況。因此,非常建議配合clean code一起閱讀

另外,個人覺得本節還有一個嚴重問題:那就是缺乏例子。“壞味道”是我們為什麼要重構,而後面的具體手法是如何重構,why 比 how 更重要些,所以個人感覺應該在描述"壞味道"的時候給出程式碼示例。

重複的程式碼 -- duplicated code

最簡單的情況,就是兩段程式碼有相同的表示式語句,處理方法也很明確,那就是extract method,然後應用這個新的方法。另外一種常見情況,就是這兩段相同的程式碼位於不同的子類 -- 往往是新增子類的時候部分複製了其他子類的程式碼,這個時候就應該使用pull up method將公共程式碼抽取到基類去。

當然,兩段程式碼也可能是相似但不完全相同,那麼可以考慮將差異部分子類化,即使用form template method。或者將差異部分引數化,即通過引數控制不同的邏輯,但需要注意的是,引數會不會導致兩種截然不同的行為,即parameterize methodreplace parameter with explicit methods的區別。

最後,也經常發現兩個類之間有相同的重複程式碼,但是二者之間並沒有繼承關係(並不是is-a關係),那麼可以extract class將公共部分提取出來,以組合的方式使用,或者使用多繼承--Mixin 繼承其實現。

過長的函式 -- long method

過長的函式往往冗雜著過多的細節,在什麼是整潔的程式碼一文就曾經中, 程式碼的組織應該像金字塔一樣,“每個函式一個抽象層次,函式中的語句都要在同一個抽象層級,不同的抽象層級不能放在一起”。

對於過長的函式,負責任的程式碼作者往往會給出一些註釋:解釋某一小段程式碼的作用,這其實就暗示著我們可以把這段程式碼移到一個獨立的函式,然後取一個恰當的名字來展現其意圖。這個新函式的名字應該體現做什麼,而不是怎麼做,這樣,新函式的名字就可以取代原來的註釋。

如果新抽取出來的子函式需要用到原函式中的引數或者臨時變數,那麼這些都需要引數化到子函式,這可能導致子函式引數列表過長的問題,這個問題及其解決辦法在後面闡述。

除了註釋,還有什麼“味道”暗示應該提取子函式呢,比如 if then else中有大段的程式碼,這個時候可以使用Decompose conditional處理條件表示式。

過大類 -- large class

單個類有太多的例項屬性,而且其中某些屬性經常獨立於其他屬性一起使用,那麼可以使用extract class

比如一個課程資訊類 Course,裡面包含了 CourseId、CourseName、TeacherId、TeacherName、TeacherSex 等屬性,那麼壞味道就是:很多屬性名擁有相同的字首。因此可以通過extrace classCTeacherId、TeacherName、TeacherSex 抽取到新的類 Teacher。然後就可以去掉這些屬性名的字首,同時Course類持有 Teacher即可。

或者一些屬性只在某些特殊狀態下使用,那麼可以考慮extrace subclass

過長引數列表 -- long parameter list

過長的引數列表讓程式碼變得難以閱讀和理解,要搞清楚每個引數的意義就需要大費周折。

如果某個引數可以從函式內可訪問的物件(類屬性或者其他引數)獲得,那麼這個引數就是冗餘的,就可以 replace parameter with method

另外,傳遞的若干個引數可能只是某個物件的一堆屬性,那麼就可以考慮直接傳遞該物件 preserve whole object,不過需要注意,preserve whole object可能會導致非預期的依賴關係,這在靜態型別語言(如C++)中又是一個複雜問題。

發散式變化 -- Divergent change

某個類由於不同的原因要在不同的地方進行修改,事實上,這違背了類的單一職責原則(SRP),通常也是過大類。解決的辦法就是拆分成不同的類(子類)。extract class or extract subclass

散彈式修改 -- shotgun surgery

Divergent change 恰好相反,為了需要響應一個變化而修改大量的類

依戀情結 -- feature envy

函式對某個類的興趣高於自己所在的類。如大量使用其它類的資料,常見的是取出其他物件的屬性,然後一通計算後再賦值。解決辦法,將總是一塊兒變化的東西放在一起:資料與對資料的操作。

資料泥團 -- Data clumps

如果某些資料經常一起變化,那麼應該將這些資料提取到某個類中,正如之前過大類中的例子。提取出單獨的類,減少了屬性和引數的個數,而且接下來就可以找出 feature envy,進一步重構。

基本型別偏執 -- primitive obsession

類似於上一條“資料泥團”,不過更強調基本資料的封裝

使用基本型別,比如用兩個欄位 begin, end 來表示區域[begin, end),僅從可讀性上來說肯定不如封裝成一個類 range

switch

switch 的問題在於重複,這裡需要switch case,那麼很可能其他地方也要switch case。如果增加一種case,那就得到處修改,違背OCP原則。

使用多型是常用的解決辦法,replace condition with polymorphrsim,過程是這樣子的:

  1. extract_method
  2. move method
  3. replace type code with subclass(strategy、state)
  4. replace condition with polymorphrsim

平行繼承體系 -- parallel inheritance hiearachies

這是shotgun surgery的一種特化,某各類增加了一個子類導致另外一個類也必須增加一個子類,雖然設計模式中可能出現這樣的情況,但壞味道可以幫助我們加以區分:某個繼承體系的類名字首和另一個繼承體系的類名字首完全相同

冗餘類 -- Lazy class

沒有什麼價值的類。類中不在有什麼實質性工作,可能是因為邏輯變化,可能是因為重構,這個時候可用通過collapse hierarchy 或者 inline class去掉這樣的類。

誇誇其談未來 -- speculative generality

過度的設計、抽象、泛化,各式各樣的鉤子和特殊情況處理,越靈活越複雜,越是難以維護。壞味道:函式或類的唯一使用者是測試用例

令人迷惑的暫時欄位 -- Temporary Field

某個成員變數只是在某些特殊情況才會用到,不用到的時候會導致迷惑,或者某個成員變數的賦值只是為了後續方便某個成員方法的呼叫,根據不同的情況可以參考一下重構手法:

  • extract class將這些特殊的field移到新的類
  • 使用 null object避免寫出條件分支
  • 函式呼叫時傳入這些特殊變數

過度耦合的訊息鏈 -- message chain

對某一個物件不停索求另一個物件,壞味道就是 A.getB().getC().dosth(),這就是 clean code 中提到的火車失事,違背了德墨忒爾律(The Law of Demeter):模組不應瞭解他所操作的物件的內部情況

解決的辦法是Hide delegate, 但這樣的重構又可能導致下一個問題:middle man

中間人 -- middle man

過分使用委託,如果一個類的多半介面都是委託給其他類,那麼可以考慮remove middle man。這有點類似軟體架構模式中提到的汙水池反模式(architecture sinkhole anti pattern)

如果middle man也有一些職責,可以考慮 replace delagate with inheritance 讓其變成最終物件的子類。

狎暱關係 -- inappropriate intimacy

兩個class過於親密,使用彼此的private。抽取出新的類,或者move filed

不完美的類庫 -- incomplete library class

類庫是程式碼複用的絕佳體現,但是類庫的作者不可能預料到所有的需求,因此怎麼在不改原始碼的基礎上完成想要的工作:

  • introduce foreign method
  • introduce local extension

被拒絕的饋贈 -- Refused Bequest

壞味道:子類複用了基類的行為(實現),但卻不想支援基類的介面,這違背了LSP原則:子型別必須能夠替換它們的基型別。

C++中public繼承的其實就是介面,而private繼承的則是實現,通過private繼承,基類中的所有方法都變成private。更通用的重構手法: replace inheritance with delagate

過多的註釋 -- comments

註釋是好東西,散發著香味,但你不應該用它來掩蓋臭味

使用extract method或者rename method來解釋註釋的行為。對於引數的命令也應該能望文知義

具體的重構手法

找到壞味道之後,就是如何安全的進行重構,書中羅列了各種重構手法的具體的實施步驟,按照這種逐步推進、逐步測試的方法,保證重構沒有影響到程式碼的外在表現。當然,IDE提供的重構工具讓部分重構變得更加容易和安全。

重新組織函式

函式總是過長,尤其是在漫長的維護過程中,函式內的程式碼數量會逐漸膨脹。

Extract method

需要注意:

  • 保證函式名稱與函式本體之間的語義距離 -- 一個好的函式名
  • 對於區域性變數和引數的處理:引數

Inline Method

難點:

  • 是否是多型
  • 得找出所有引用點

Inline temp

臨時變數只是被一個簡單表示式賦值一次。有助於後續的Extract method,也可以作為replace temp with query的一部分使用。

注意:

  • 如果表示式較為複雜不應內聯,影響可讀性與效率
  • 多次賦值的話也不能內聯

replace temp with query

將一個表示式提取為一個單獨的函式,新函式可以被其它函式呼叫。之中有一段例項程式碼,用python改寫如下:

def calc_price(self):
	base_price = self._quality * self._item_price
	if base_price > 1000:
		return base_price * 0.95
	else:
		return base_price * 0.98

重構後是這樣的

def calc_price(self):
	if self.base_price() > 1000:
		return self.base_price() * 0.95
	else:
		return self.base_price() * 0.98

def base_price(self):
	return self._quality * self._item_price

個人覺得這個例子並不是很恰當

  • 在沒有改善可讀性的情況下,引入了重複呼叫帶來的開銷
  • 有時也會有問題,原始的程式碼base_price一旦計算後是不會發生變化的,都提取成query之後就不能保證了

個人認為,即使為了解決temp只在函式內部生效而無法複用的問題,也應該改成:

def base_price(self):
	return self._quality * self._item_price

def calc_price(self):
	base_price = self.base_price()
	if base_price > 1000:
		return base_price * 0.95
	else:
		return base_price * 0.98

對於python,query還可以實現為property的形式,如果確定query的結果是固定的,還可以使用cached_porperty優化。

introduce explaining variable

將複雜表示式變成一個解釋性的區域性變數,解決可讀性問題

split temporary variable

一段程式碼中,一個臨時變數只能代表一個意思,否則應使用不同的臨時變數。

remove assignment to parameter

移除對引數的賦值,防止誤改、不小心的覆蓋,可讀性更好

  • 不要對引數進行賦值,以一個臨時變數取代引數的位置
  • java只採用pass by value傳遞方式。對於基本型別,同C++一樣;對於引用型別,可以改變引數內部的狀態(呼叫者實參的內部狀態隨之改變),但對引數重新賦值沒有任何意義。
  • 可以給引數強制加上final修飾符,保證引數不被賦值

在物件之間搬移特性

move method

遷移的過程中可能需要用到source class的特性(成員變數或者成員方法)。處理方式:

  1. 將這個特性移到target class中;
  2. 在target class中建立一個對source class的引用;
  3. 將source object作為一個引數傳遞給target method(eclipse中的move就是該方法);
  4. 將特性作為引數傳遞給target method

movie field

常常是extract class的一部分,先移動field,在移動method

extract class

先 move field,再move 必要的 method

需要考慮的是,新的類要不要對外公佈

inline class

Hide delegate

eg:

value = AObject.getBObject().getVlaue()

public int AObject::getValue(){ return bObject.getgetVlaue()}
value = AObject.getVlaue()

而且應該考慮要不要幹掉 AObject.getBObject

remove middle man

Hide delegate相反,如果一個server全是各種簡單委託

introduce foreign method

需要呼叫的類缺少一個你需要的方法

良好的建議在於:這個方法應該屬於服務類,因此只需將類的物件作為第一個引數就行,(其他引數應該是服務類 “新方法”的引數)

introduce local extension

  • 已有且不能修改的類無法完成需求
  • 使用繼承或者組合解決

重新組織資料

self encapsulate field

對屬性的訪問通過getter和setter實現

適用情況:

  • 可能對屬性訪問做控制
  • 可能會有subclass,且subclass的getter、setter方法不同於superclass

replace array with object

一個陣列,其中的元素表示不同的東西

duplicated observed date

有一些domain data(業務處理邏輯相關的)置身於GUI控制元件中,而domain method需要訪問之。

domain class 和GUI呈現分離,共享的資料通過觀察者模式實現同步控制

replace magic number with symbolic constant

magic number 真的是人見人恨

encapsulate collection

如果函式返回一個集合,那麼這個返回值應該是隻讀的,而且不應該提供群集合的 setter 方法,而應提供加入、刪除集合元素的方法

Java中的unmodifiable系列就是返回只讀集合

replace record with data class

record 比如來自資料庫,用一個 dataclass 將所有 field 宣告為 private ,提供對應的訪問函式

replace type code with class

型別編碼(type code)是一些常量或變數,一般有多個可能的值。普通常量使用的時候缺乏型別檢查,類似C++中的define,而class強加型別檢查。

比如血型如果用4個整數(c語言中的enum)表示,那麼是傳參的時候無法限制類型別,可讀性也差。C++11中enum class就解決了這個問題

前提是型別碼不會用於switch中,否則就得使用下面的重構手法

replace type code with subclass

如圖所示:

type code影響到了其所在類的行為, 那麼就得使用多型,該方法為replace conditional with polymorphism做準備。

前提是type code在物件建立的時候就確定,且宣告週期內不可變。如果 type code可能是變化的,只能使用replace type code with state/strategy

replace type code with state/strategy

replace type code with subclass一樣,都是為replace conditional with polymorphism做準備

簡化條件表示式

Decompose conditional

從if,then,else三個段落中提煉出獨立函式,使程式碼更加清晰

consolidate conditional expression

一系列條件測試如果得到的是相同的結果,那麼將這些條件合併為一個表示式,並將這個表示式提煉為一個獨立函式。extract method也更好體現了做什麼,而不是怎麼做。

如果這些條件邏輯上本來是彼此獨立的,那麼不應該使用本項重構

consolidate duplicated conditional Fragments

在條件分支上有相同的一段程式碼,那麼應該將這一段程式碼移到條件式之外,這是經常遇到的情況。

關鍵是這樣更好體現了哪些是隨條件變化而變化的,同時避免 duplicated code。

remove control flag

在迴圈的布林表示式中,某個變數起控制標記,如 while(exit) ,以break語句或者return語句代替控制語句

replace nested conditional with guard clause

衛語句(guard clause):如果某一條件極其罕見,就應該單獨檢查該條件,並在該條件為真時立刻返回,這樣的單獨檢查成為衛語句。當然,我更喜歡稱之為early return,往往能減少巢狀的深度,讓程式碼可讀性更好。

本質:給予某一條件特別的重視(if then else表示對分支的重視是相同的),衛語句表示:一旦這種情況發生,應該做一些必要的清理工作,然後退出。

replace conditional with polymorphism

將一個條件表示式的分支放進一個subclass的覆寫函式內,並將原始函式宣告為抽象函式

關於對replace type code with state/strategyreplace type code with subclass的選擇:核心在於 type code 是否可能會在物件的生命週期內改變。

introduce null object

如果需要再三檢查一個物件是不是null,那麼以一個null object替換為null時的情況。null object 一般是常量,可以用 singleton 封裝,其屬性不會發生改變

需要注意的是:

  • 只在有大多數的客戶程式碼需要 null object 做出相應相應時,才有必要使用 null object。當然,如果少數地方需要做出不同響應,那麼也可以用object.isNull區分
  • null object 是邏輯上可能出現的,是一種特殊情況,並不是異常。比如書中的例子:一個出租房確實可能暫時沒有租客。

introduce assertion

assertion 應該是一個永遠為真的表示式,如果失敗,表示程式出了錯誤。assert既可以幫助排查bug,也可以幫助讀者理解程式碼作者的假設、約束

簡化函式呼叫

rename method

add parameter

需要考慮增加引數是否會導致壞味道,long parameter list。如果可以通過已有的引數、屬性獲得新引數的值,那麼就不應該增加。

remove parameter

重構的時候要注意多型(繼承)的情況,不要遺漏。上同

separate query from modifier

將查詢操作和修改操作分開

parameterize method

若干函式做了類似的工作,只是函式本體中包含了不同的值。將導致函式差異的值作為引數傳入,如下面的程式碼:

    def tenPercentRaise(self):
        self._salary *= 1.1

    def fivePercentRaise(self):
        self._salary *= 1.05

    # 重構後的程式碼
    def raiseWithFactor(self, factor):
        self._salary *= (1 + factor)

重構後,只保留一個方法raiseWithFactor,但新函式應該加上引數合法性的檢查。個人認為,如果factor的取值固定為少數的幾個值,那麼提供不同的介面也是可以的,只不過對外介面統一呼叫同一個私有介面。

replace parameter with explicit methods

函式的操作完全取決於引數值,則針對引數的每個引數值,建立一個獨立的函式

壞味道很明顯,引數是離散的,函式內以條件式檢查這些引數值,並根據不同引數值做出不同反應。比如下面這種型別的程式碼

    def setSwitch(self, is_on):
        if is_on:
            # do a lot of thing let switch on 
        else:
            # do a lot of thing let switch off 

preserve whole object

解決long parameter list的一種重構手法

replace parameter with methods

物件呼叫某個函式,將其返回值作為引數傳遞給另一個函式,而後面一個函式也可以呼叫前一個函式 。那麼在後一個函式中取出該項引數,並直接呼叫前面一個函式。動機在於如果可以通過非引數列表的方式獲得引數值,那麼就不要使用引數列表

使用前提

  • 引數計算過程(即前一個函式)不會依賴呼叫端的某個引數
  • 引數的存在可能是為了將來的彈性時也不能使用本項重構

introduce parameter object

某些引數總是很自然地同時出現----把這些引數抽象為一個物件,比如經常遇到的是以一對值代表一個範圍,如(start、end)、(lower、upper),用Range取代之

好處:

  • 縮減引數列表長度;
  • 更易理解和修改;
  • 可以把一些反覆進行的計算移到物件裡面(比如計算range差值)

remove setting method

如果class中某個屬性在初始化的時候就設定,以後就不在改變了,那麼應該去掉改屬性的setter,還可以將屬性設定為final(or const)。

hide method

一個函式,從來沒有被其它class使用過,那麼它應該為private。最小化對外介面,需要的時候再開放。

replace constructor with factory method

常常在多型 或者replace type code with subclass中使用。可以用來實現change value to reference,或者單例。同時,工廠方法也會比建構函式過載可讀性更好

encapsulate downcast

不要讓使用者對你的函式返回值進行downcast,返回使用者需要的型別

這是低版本Java的問題(沒有模板),Java5.0之後沒問題了。這也是強型別OO語言的問題,python就沒有這個問題。

replace error code with exception

錯誤,異常與自定義異常 這篇文章對error code 和 exception 有較多討論

replace exception with test

exception 不應該作為流程控制的手段,如果某種情況的出現不是意料之外的,那麼就不應該丟擲異常

處理繼承體系

pull up field

把子類重複的屬性移動到基類

pull up method

把子類重複的函式移動到基類,如果兩個子函式相似但不盡相同,可以考慮使用form template method

pull up constructor body

各個子類的建構函式程式碼有一樣的部分,則在基類中建立建構函式,在子類中呼叫

push down method

push down field

extract subclass

class中一些特性只被某些實體使用,那麼新建一個subclass中,將這些特性轉移到subclass

需要考慮到extract class與extract subclass的區別(委託與繼承)

extract superclass

兩個類有相似特性,把相似的部分移動到superclass,有時候為了程式碼複用也可以這麼做

collapse hierarchy

基類與子類沒有太大區別----合併之

form template method

replace inheritance with delegation

某個subclass只使用superclass的一部分,或是根本不需要繼承而來的資料,則可以改繼承為委託。

繼承和委託也是adapter模式的兩種實現方式,本人也傾向於delegation

replace Delegation with inheritance

太多的簡單委託關係

關於引入新技術、新思想的思考

即使一個新技術(新思想)已經經過社群的驗證,要引入到開發團隊來也不是一件容易的事情,也會遇到重重阻力:

  • 大多數人還不知道如何使用這項新技術
  • 引入新技術的收益要長期才能看出來,那麼何必現在去付出呢?如果回報週期過長,那麼在收穫的時候可能已經不再當前的位置了
  • 新技術的引入並不是非用不可,還需要花掉一些時間,老闆(專案經理)願意嗎?
  • 線上上專案使用新技術,反而可能引入BUG,冒險是否值得?

如果你本身就是老闆(技術Leader),且能夠頂住來自產品的壓力,那麼可以強推一項新技術,雖然強推效果也不一定好。但如果是作為平級,怎麼推廣呢?如何解決這些障礙?

第一:培訓與工具,通過培訓、分享讓團隊快速掌握新技術,使用工具讓成員掌握新思想。比如,想要遵守同一套程式碼規範,那麼最好配上相應的程式碼檢查。

第二:展現短期、肉眼可見的利益,新技術不僅要有長期收益,還得在短期內就展現出其優點。如果短期內就能看到好處,大家就願意去積極嘗試。

第三:降低開銷,降低上手難度,技術的投入也是講究投入產出比的,使用成本越低,大家就不會排斥。

第四:安全過渡,逐步進行,如果是線上專案,最好能有健全的回滾機制。

本質上,都是通過向你的Leader或者小夥伴展示,這個新東西又好又不貴,使用起來還很方便。

更有意思的是,書中提到Geoffrey Moore提出來的技術接納曲線:

一個思想、技術、產品即使有先行者、嚐鮮者的支援,但想要大眾市場接受,還要跨過一條鴻溝。鴻溝的存在源於不同人的不同訴求:先行者關注的是新技術本身,而普羅大眾關注的是成熟度、引入(使用)成本。

在構建之法也有很詳細的分析。

Reference

重構 - 改善既有程式碼的設計
程式碼整潔之道
什麼是整潔的程式碼
軟體架構模式
錯誤,異常與自定義異常

相關文章