在OO(面向對象)時代長大的小伙伴們一定記得:
面向對象的基石:把數據和依賴該數據的行為封裝在一起。
但我們經常遇到一個類依賴其它類的數據的情況。不多的話,正常,對象間勢必存在交互,畢竟完全獨立的類無法構建出復雜的業務系統。
太多依賴外部數據的話,可能是問題,也可能不是問題,而是故意為之。嗯?這不是反OO嗎?莫急,先來看看兩個例子,然后分析隱藏在后面的東西。
特性依戀
先看太多外部數據依賴是問題的情況,重構里面管這叫 特性依戀 。顧名思義,太過迷戀別人的東西。
case class Product(name: String, price: Float)
case class OrderItem(count: Int, product: Product)
case class Order(items: List[OrderItem]) {
def cost: Float = {
items.sum(item => item.count * item.product.price)
}
}
每個訂單項的花銷之和,就是訂單的花銷。問題異常明顯,訂單項的花銷是在訂單層次計算的,導致訂單過度依賴訂單項的數據。
case class OrderItem(count: Int, product: Product) {
def cost = count * product.price
}
case class Order(items: List[OrderItem]) {
def cost = items.sum(_.cost)
}
訂單項的花銷,訂單項自己計算,訂單的花銷是所有訂單項花銷之和。代碼比說明書清楚多了,OK。
行為構建在數據之上,對象作為載體封裝二者。從上面的例子可以看出,不能錯位,屬於訂單項的行為就不要放在訂單里面,如此才能提高代碼的可維護性和可重用性。
到目前為止,OO的世界依然和諧美好。
如此熟悉的反OO:訪問者模式
再來一例。
case class Car(engine: Engine, body: Body, wheels: List[Wheel]) {
def engineerCheck() {
check(enigne)
check(body)
wheels.foreach(check(_))
}
def washerWash() {
wash(body)
wheels.foreach(wash(_))
}
}
一輛車有一個引擎,一個車身,幾個輪子。出廠/維修/保養的時候都需要找工程師檢查,洗車的時候需要找洗車工清洗。工程師檢查的行為一定是針對汽車的各組件,洗車工也是清洗的各汽車組件,行為和數據在一起組成對象,從OO的角度看,沒啥問題。
如果來了一個外星人,以前沒見過地球的汽車,覺得新奇,准備自己反向工程一輛,那簡單:
case class Car(engine: Engine, body: Body, wheels: List[Wheel]) {
...
def alienReverseEngineering() {
reverseEngineering(enigne)
reverseEngineering(body)
wheels.foreach(reverseEngineering(_))
}
}
小伙伴們發現沒?汽車已經無辜到要關心外星人,職責太特么不單一了,即使它沒有違反OO。重構的解決方案就是 訪問者模式 ,把工程師/洗車工/外星人干的事情從汽車里面剝離出來。
trait Element {
def accept(v: Visitor)
}
class Engine extends Element {
def accept(v: Visitor) {
v.visit(this)
}
}
class Body extends Element {
def accept(v: Visitor) {
v.visit(this)
}
}
class Wheel extends Element {
def accept(v: Visitor) {
v.visit(this)
}
}
case class Car(engine: Engine, body: Body,
wheels: List[Wheel]) {
def accept(v: Visitor) {
engine.accept(v)
body.accept(v)
wheels.foreach(accept)
}
}
Elment代表的是需要被訪問的元素,本例中就是汽車的各組件。Car容納了所有組件,並隱藏組件間的結構。
trait Visitor {
def visit(engine: Engine)
def visit(body: Body)
def visit(wheel: Wheel)
}
class Engineer extends Visitor {
def visit(engine: Engine) = { ... }
def visit(body: Body) = { ... }
def visit(wheel: Wheel) = { ... }
}
class Washer extends Visitor {
def visit(engine: Engine) = { ... }
def visit(body: Body) = { ... }
def visit(wheel: Wheel) = { ... }
}
class Alien extends Visitor {
def visit(engine: Engine) = { ... }
def visit(body: Body) = { ... }
def visit(wheel: Wheel) = { ... }
}
Visitor是所有對Car感興趣的人,以及他們會對Car發生的行為。
Element/Car是數據,而Visitor是行為,訪問者模式使得你可以在不修改Car的組件及結構的情況下,通過Visitor的方式定義新的行為。
細心的小伙伴們已經發現了,其實訪問者模式分離了數據和行為,反OO了。
反不反OO呢?
一會支持OO,一會反OO,以后咋做設計呢?
如果一碼說設計是門藝術,需要根據實際情況仔細權衡,小伙伴們一定會在心里使勁罵,說了句廢話。
那一碼不說虛的,來分析點實在的東西。既然兩個例子無法在OO上達成一致,那咱往后退一層,來看看更基礎的原則 單一職責 和 不要重復 。
對於訂單一例,只有把訂單項的數據和行為(開銷)放在一起,才算系統里面對一個概念的解釋只在一處存在,滿足 不要重復 的原則。對於汽車一例,只有把易於變化的行為和穩定的數據結構分離,才能做到一個個獨立的職責 汽車/工程師/洗車工/外星人,才能做到易於維護和擴展。
能夠把上面這一點想通,其實只是個開始而已。一碼個人覺得,對於代碼層面的設計而言:
- 軟件設計的基本原則是道,如:單一職責,不要重復,依賴倒置等
- 范式及其背后的模式是術,如:面向對象及設計模式,函數式編程及Monads,泛型編程,元編程等
從代碼設計的角度看,如果你會C#,那么不要再去學Java(反之亦然),而應該去學學Scheme的函數式編程,Ruby的元編程。只有掌握不同的術,才能讓道逐漸豐滿,也才能為具體問題找到最合適的設計方案。