大話重構 之 原來反OO天天見


在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的元編程。只有掌握不同的術,才能讓道逐漸豐滿,也才能為具體問題找到最合適的設計方案。

推薦

消除過長方法

消除過長類

消除重復代碼

答粉絲問

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

職責單一原則真的簡單嗎

防止“加個需求,到處改代碼”

圖片二維碼


免責聲明!

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



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