在面向對象程序中活的最好最長久的是短方法。對於新手而言,很困惱面向對象的程序中完全找不到計算邏輯,反而是無窮無盡的方法調用,但是當你習慣面向對象后就會了解到短方法的價值所在。
短方法的價值
從較早的時候,程序員們就發現方法越長就越難以理解,但由於老的編程語言在方法調用上的開銷使得很多人放棄了用短方法。在現代的面向對象語言中,這一開銷已經非常小,這不應該再是我們使用長方法的理由。
短方法可以極大地增強代碼的可讀性。
- 一個好的方法命名可以使你不看方法實現就能知道它的功能,重點就在於一個好的方法命名;
- 使用短方法,可以給業務邏輯提供合適的抽象層級。
方法的抽象層級可能不太容易理解,舉些例子。比如部門的組織架構,主管下面有多個項目經理,項目經理下面有多個項目組長,項目組長下面有多個小兵,一個好的主管會把目光聚焦在幾個項目經理身上,而不是給每個小兵派具體的任務。再如網絡通訊的協議棧,從應用層到物理鏈路層,每層都構建在下一層之上,都有自己清晰的界限。方法的抽象層級也一樣,每個方法做一個級別的事情,而不是把所有細節堆到一個方法里面,跟一鍋粥一樣。
什么場景下用短方法
很簡單的一點就是看方法體內的注釋。方法體內的注釋通常意味着語義上的背離,即看代碼塊實現很難理解它的意圖。下面是幾種常見需要短方法的場景:
- 如果一個方法里面,有多個代碼塊,每個代碼快都有注釋,這是使用短方法的絕佳時機;
- 如果想給方法里面的某個代碼塊寫注釋,也需要提取一個短方法;
- 如果發現不同的地方有重復,提取短方法就是第一步。請參考消除重復代碼
比較極端的情況下,甚至只有一行代碼,也值得提取一個短方法。
再次提醒下各位看官,問題的關鍵點在於語義上的差別:方法名可以說明意圖,而代碼塊自身只能說明是如何實現的。
提示:代碼是給人讀的,要說明意圖,而不僅僅是如何實現。
如何消除過長方法
如下面的代碼:
def process {
try {
// 初始化
openA()
openB()
// 執行業務邏輯
...
...
} finally {
// 清理
closeB()
closeA()
}
}
一個方法體內有注釋說明的多個代碼快,是典型特征。解決的辦法簡單明了,多次 提取方法。
def process {
try {
initialize()
execute()
} finally {
clean()
}
}
def initialize {
openA()
openB()
}
def execute = { ... }
def clean {
closeB()
closeA()
}
重構后的四個方法都只做了一個抽象級別的事情,賞心悅目。
上面的例子非常簡單,但是只要你開始實施 消除過長方法,不久后一定會出現一個絆腳石,方法內有大量的局部變量。如果任由這些局部變量存在,而實施 提取方法 進行重構,那重構出來的多個短方法會有很多參數,MLGB,還不如不重構。
咋辦呢?莫急。
如何應對諸多局部變量
應對局部變量根據場景不同,可分為如下四種。
▶ 用查詢替代變量
val basePrice = this.quality * this.itemPrice
if (basePrice > 1000)
basePrice * 0.95
else
basePrice * 0.98
把局部變量換成方法,隨時可以調用。
if (basePrice > 1000)
basePrice * 0.95
else
basePrice * 0.98
def basePrice = this.quality * this.itemPrice
有人會說了,重構后的代碼性能會降低。嗯,確實降低了,有些情況下,可讀性和性能是沖突的,但是如果小小的性能犧牲可以換來更高的可讀性,我會毫不猶豫地選擇可讀性。當然在性能消耗特別大的情況下。。。依然可以用這種方式,先 提取方法,然后在提取出來的方法里面使用緩存,兩全其美。
▶ 引入參數對象
val x1 = ...
val y1 = ...
val x2 = ...
val y2 = ...
calcDistance(x1, y1, x2, y2)
引入一個代表點的參數類Point,然后重載一個calcDistance方法。
val point1 = new Point(..., ...)
val point2 = new Point(..., ...)
calcDistance(point1, point2)
case class(x: Double, y: Double)
case class是scala的一種特性,相當於C++里面的struct,java里面的數據類,有興趣的同學可以google下。哦,google不能用了,用www.gfsoso.net吧。當然case class的魔法不是這個,是和構造函數對應的解構函數,專業點叫 模式匹配。額,繼續gfsoso下吧。
▶ 保持對象完整
val begin = dateRange.begin
val end = dateRange.end
withinPlan = plan.withinRange(begin, end)
重載個withinRange方法,支持直接傳入完整的DateRange對象啊。
withinPlan = plan.withinRange(dateRange)
▶ 用方法對象替代方法
class Account {
def gamma(value: Int, quantity: Int, date: Int) = {
val value1 = value * quantity + detla()
var value2 = value * date + 100
if ((date - value1) > 100)
value2 -= 20
val value3 = value2 * 7
value3 - 2 * value1
}
}
我要把if語句那段提取出來,因為它太重要(不是真的太重要,是為了說明 用方法對象替代方法)。
class Account {
def gamma(value: Int, quantity: Int, date: Int) = {
new Gamma(value, quantity, date).compute()
}
}
class Gamma(value: Int, quantity: Int, date: Int) {
var value1 = _
var value2 = _
var value3 = _
def compute() {
value1 = value * quantity + detla()
value2 = value * date + 100
importantThing()
value3 = value2 * 7
value3 - 2 * value1
}
def importantThing() {
if ((date - value1) > 100)
value2 -= 20
}
}
有點累哇?好好休息下,准備迎接下一篇《消除壞味道》系列的文章吧。
提示匯總
- 代碼是給人讀的,要說明意圖,而不僅僅是如何實現。
推薦
查看《大話重構》系列文章,請進入YoyaProgrammer公眾號,點擊 核心技術,點擊 大話重構。
分類 大話重構
優雅程序員 原創 轉載請注明出處