大話重構 之 消除巨無霸類


當你看到別人寫的超過千行的巨無霸類,以及隨着時間的累積,自己寫的類也穩步邁向巨無霸的時候,是不是既恐懼又無奈?一碼今天就帶小伙伴們征服巨無霸,打造屬於自己的成就感。

過長類的緣由

當業務邏輯隨着時間累積,並且越來越復雜時,這個類由本來的清秀怡人非常容易變得滿臉橫肉。

一個類中業務邏輯越來越多,首先它的職責就不再單一,一說這,小伙伴們都明白。

其次邏輯越多,涉及的狀態越多,即實例變量越多,實例變量一多,重復代碼也就隨之而來。為啥?實例變量關系有遠有近,關系較近的實例變量沒有被清晰的打包出來,直接后果就是類中存在多處操作這些實例變量的重復代碼片段。

“實例變量沒有被清晰的打包”,實際上就是職責不單一。是否對職責不單一又有了一點新的認識呢?

另外說一點,能把類寫成巨無霸的同學,通常都不修邊幅,語言的表達層次很低。比如,找一組元素中的最小值,一定會親手寫個循環遍歷一遍,4行代碼沒了。

有了前面的說明,我們就可以開始征服之旅啦。由淺入深,分三個方面來講:提升語言表達層次,消除類內的重復,明確概念分離職責。

風景

提升語言表達層次

語言表達層次怎么理解,一碼給你看兩段在集合里面找最小值的代碼:

表達層次低

val elements = ...
var min = Int.MaxValue
for (element <- elements)
    if (element < min)
        min = element

正常

val elments = ...
val min = elements.min()

“正常”版本說明了意圖,表達層次高,明顯獲勝。Scala語言里面寫出“層次低”版本的較少見,但是其它語言里就不一定了。碰到這種情況記得找下Apache/Guaua等庫,沒有的話自己封裝一個庫也可以,千萬不要再寫“層次低”這種吃力不討好的代碼了。如果自己封裝庫,重構的方法參考《消除重復代碼》一文。

經過上面這一折騰,巨無霸應該瘦了十之有一了,接下來要面對的敵人是“很多實例變量”。

風景

消除類內的重復

類內部關系相近的實例變量,容易導致重復的代碼片段,這給我們一個很好發現代碼重復的提示。

那哪些實例變量的關系比較近呢?

  • 同樣的前綴或后綴
  • 由空行隔開的實例變量組
  • 名字上的業務含義可以看出來的

有了上面的提示,接下來就是到類里面去掃描重復代碼了,人肉掃描哈,有更好辦法的小伙伴一定告訴我。找到后的具體重構方法,萬變不離其宗,依然是 提取方法 ,請小伙伴們參考《消除重復代碼》一文。

典型的效果是,原本5個上百行的方法,重構成5個幾十行的方法(通常不超過40行)和10多個三四行的方法。

另外不得不提的是一些不咋個依賴實例變量的超大方法,如何重構涉及的內容較多也較獨立,請參考《消除過長方法》一文。

噼里啪啦一陣,巨無霸這次瘦身比較明顯,去了十之三四。如果達不到效果,微信上找我,一碼就喜歡干這個事情,跟你一起重構。

到了這一步,還有500行的類,終於要用到今天的主角了:明確概念,分離職責。

風景

明確概念分離職責

關系較近的實例變量,以及使用它們的方法,需被冠以明確的概念,並獨立成類。

這樣做的好處:

  • 保持職責單一
  • 明確的概念更容易抽取出易用的接口,如此方便重用和擴展

這里有兩種手法,一是 提取類 ,一是 提取子類 ,對應了組合和繼承。后者不太直觀,莫急,下面逐一說明。

提取類

class Person(val name: String,
             val officeAreaCode: String,
             val officeNumber: String) {
	def officePhone(): String = {
		officeAreaCode + "-" + officeNumber
	}
}

officeAreaCode和officeNumber有相同的前綴,而且officePhone方法中也一起使用,非常緊密。概念也很明確,完整的電話號碼嘛。好了,提取一個獨立的類TelephoneNumber。

class Person(val name: String,
             val officeTelphone: TelephoneNumber) {
	def officePhone(): String = {
		officeTelephone.telephoneNumber
	}
}

class TelephoneNumber(val areaCode: String,
                      val number: String) {
	def telephoneNumber(): String = {
		areaCode + "-" + number
	}
}

例子很簡單,但已足以說明問題。

提取子類

為啥會有子類呢?不太直觀哈。一般是在代碼中出現不同的條件,有不同的處理時,可以用 提取子類 ,來個例子。

class Ojbect(val name: String,
             val type: String) {
    def doSomething() {
        type match {
            case "A" => doSomethingForA()
            case "B" => doSomethingForB()
            case "C" => doSomethingForC()
        }
    }
    
    def doSomethingForA() = { ..A.. }
    def doSomethingForB() = { ... }
    def doSomethingForC() = { ... }
}

其中的match是Scala特有的,和Java的switch類似,當然它有更強大的內涵,請小伙伴們自行GFSOSO。

type這個實例變量,是要抽取子類的典型特征,重構如下:

class Object(val name: String) {
    def doSomething(): Unit // 沒有方法體
}

class A(val name: String)
    extends Object(name) {
    
    @override def doSomething() = { ..A.. }
}

class B(val name: String)
    extends Object(name) {
    
    @override def doSomething() = { ..B.. }
}

class C(val name: String)
    extends Object(name) {
    
    @override def doSomething() = { ..C.. }
}

相信一看代碼,小伙伴們就懂了。

小技巧

當從內部無法明顯看出哪些變量關系緊密時,可以轉到外部觀察,看哪些方法經常被一起使用,這有助於你找到明確的概念,進而用上面的重構手法分離職責。

好了,打完收工,這些類都個個清秀了吧。小伙伴們趕緊動動手,找到屬於自己的成就感吧。

下期再見。

推薦

解決萬惡之首“重復代碼”

消除過長方法

消除巨無霸類

答讀者問

你的參數列表像蚯蚓一樣讓人厭惡嗎

職責單一原則真的簡單嗎

查看《大話重構》系列文章,請進入YoyaProgrammer公眾號,點擊 核心技術,點擊 大話重構。

分類 大話重構

優雅程序員 原創 轉載請注明出處

圖片二維碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM