這篇文只能舉小例子,不過
data class CoordinatePlace(
val coordinate: Coordinate,
val place: String)
我到這邊就會停了,不會再 refactor 下去。因為像是 place.coordinate.lng
多一層存取對我來說不會是個問題,(多到兩層以上我才會開始煩惱)。
kotlin 可以用 with
, apply
等等工具減少很多不必要的 code,為了少一層開始抽 ICoordinate
不值得啊。(當然也可能是因為只能選小的範例)
-------
然後,有關繼承。OOP 寫久了之後,我的經驗是可以被設計有繼承的關係,通常是有類似的 行為
,而不是有類似的資料。
也就是說,我不傾向因為它們有很多共同的欄位,所以就用繼承來解決程式碼重用。我最少得觀察到一個以上的行為、動作,才會開始思考繼承,而且通常會變成 interface (因為只有行為,沒有資料)
為什麼會有這種結論?因為共同欄位用繼承來重用程式經不起時間的考驗,只有設計初時可以用,等到加需求後就… 真的就是補丁硬改,改不動的就 workaround,那程式碼真是令人痛心。
-------
繼續以 Coordinate
為例, 假設我們有求兩個座標距離的需求:
data class Coordinate(... omit) {
fun distanceTo(other:Coordinate): Double
}
然後有名稱的地點 CoordinatePlace
也要算距離,方法一是直接拿裡面的 coordinate
來求距離,方法二就是加上同樣的 method:
data class CoordinatePlace(val coordinate:Coordinate) {
fun distanceTo(other:Coordinate): Double {
return coordinate.distanceTo(other)
}
}
方法二可以封裝 CoordinatePlace
的 coordinate
欄位 (欄位能少曝露就少曝露),到此,distanceTo
這個 method 就會有機會抽成一個 interface,因為它是 行為 ,然後有求距離需求的 class 實作它就行了。
這種不依賴 coordinate.lng
等等內部資料的高階行為才會有資格被抽出去,也因為 method signature 沒有牽扯到太多的資料欄位,所以未來擴充時有很大的彈性。
在設計 model 之間的互動時,應該要多依賴上面 distanceTo
這種高階的行為,而不是直接開內部欄位 .lng
, .lat
給別的 model 算,沒有高階行為的 model,才是真正的 Anemic Domain Model。
ps. data class 的欄位都是 public 的,所以這個例子有點封裝失敗。不過上面的重點是行為。
另一個有名的例子是 java8 的 java.time 的設計。
LocalDate 和 LocalDateTime 並沒有繼承關係,明明 LocalDateTime 也有 year, month, day 等等欄位,意思也很像