Ruby 物件導向程式實踐心得

物件導向程式設計

  1. 設計的本質為何?
    1. 設計的本質是為了應付未來的變化
  2. 為何選擇物件導向程式語言?
    1. 物件導向程式的信奉者相信藉由程式描述現實中的物件(Object)與其之間的訊息(Message),可以有效的管理程式碼的依賴關係,從而應付未來的變化
  3. 何時該做設計?
    1. 取決於當下的能力和時間表並做出馬上能得到效益(享受到易於修改)的設計
  4. 如何評估設計的好壞
    1. 透明性:程式碼的修改結果是顯而易見,容易了解有哪些地方會受到影響
    2. 合理性:修改的效益與成本
    3. 可用性:re-usable,不依賴於上下文
    4. 典範性:作為典範讓他人擴充
  5. 設計失敗的原因
    1. 不知道怎麼設計
    2. 一知半解的套用各種 Design pattern,導致設計和實作完全分離時(改不動了)

單一職責的類別

  1. 為何單一職責是屬於物件導向語言的 Principle?
    1. 物件導向語言和程序性語言最大的不同在於資料和行為之間息息相關,為此我們會將相關的資料和行為放在同一個類別中,提高「內聚」。
  2. 為何要追求單一職責?
    1. 一個類別知道得越多,代表依賴的越多,那它就越難重複使用,因為很容易牽一髮動全身,遵守單一職責的類別容易修改且不用擔心後果
  3. 如何判斷物件是否遵守單一職責?
    1. 「高內聚」,這個物件所做的所有事跟與其目標非常相關

    2. 藉由把類別當成一個人來提問

      想像有一個書店 BookStore 類別,並向他提問……

      書店先生,請問你有賣 Ruby 物件導向程式實踐 這本書嗎? : 有

      書店先生,請問你的推薦的書籍? : Ruby 物件導向程式實踐

      書店先生,請問你有提供什麼食物? : ???

  4. 無法切分準確的職責?
    1. 等待更多的資訊,別畫蛇添足

撰寫擁抱變化的程式碼(For Ruby)

  1. 依賴於行為而非資料

    # bad puts @var ----- # good attr_reader :var puts var
  2. 隱藏資料結構

    # bad book = data # array book[0] # Name book[1] # author book[2] # publish year ----- # good BookModel = Struct.new(:name, :author, :publish_year) <-- 當 data 順序有變時,只需調整這裡 book = BookModel.new(data) book.name book.author book.publish_year

管理依賴關係

  1. 對於任何訊息,物件會有三種回應方式
    1. 自己本身知道如何回應
    2. 經由繼承回應
    3. 藉由別的物件回應
  2. 依賴重會發生什麼事?
    1. 知道越多,代表依賴越重,當需求出現變化時,修改程式碼的成本會變高
  3. 減少依賴的一些方法?
    1. 使用 keyword argument 取代參數順序

      1. 當 keyword argument 太多的時候,可以用 **arg

        def calculate(**arg) params = default_arg.merge(arg) ... end private def default_arg # 提供參考 { unit_price: 0, quantity: 0, } end
      2. 遇到不可修改的 class 時,可以用 factory

        class Calculator def initialize(unit_price, quantity, ...) # 改不動 ... end end module CalculatorWrapper def self.build(hash) Calculator.new(hash[:unit_price], hash[:quantity]) end end CalculatorWrapper.build({ unit_price: 10, quantity: 10 })
    2. 依賴注入

      1. 使用依賴注入避免相依於具體的類別,因為我們關心的只有這個物件能不能回應某個行為

      2. 依賴注入可以延伸到另一個觀念「反轉依賴」,導致依賴的方向是可以自由決定的,我們可以得知幾個原則:

        1. 依賴於不容易變化的類別
        2. 具體比抽象更容易變化
        3. 減少依賴將能夠降低修改的成本
        # scenario: 記者報導商家 # V1 class Reporter # 永久不變 def report(store) ... end end reporter.publish(store) # V2 class Store # 每天都在變的,method def is_reported_by(reporter) ... end end store.is_reported_by(reporter) # 假如 Reporter 比 Store 更不易變化(method name 不會變來變去) # 該選擇 V2

建立靈活的介面

  1. Domain model 很容易發現,例如:使用者、商家,但之間傳遞的訊息才是構成整個 Application 的核心,而訊息藉由介面在物件中傳遞
    1. UberEat 和 CYBERBIZ 都有 shop 和 customer 類別,但這兩者傳遞的訊息卻不同
  2. 可以藉由順序圖了解訊息的傳遞,可以發現隱藏的物件和設計公共介面
  3. 用「要什麼」取代「如何要」,專注於結果,並且可以使用依賴注入來達成上下文獨立
  4. 當出現 chaining method 時很可能違反「 迪米特法則」,「 迪米特法則」是一套能帶來鬆耦合的守則,表象解是用 delegation,但本質上應該是目前程式碼處於「如何要」的狀態

使用鴨子類型技巧降低成本

  1. 當出現「我知道你是誰,並且我知道你能做什麼」時,通常都可以用鴨子型別變成「我知道你能做什麼」
  2. 當出現以下架構時,通常可以使用鴨子型別降低修改成本
    1. case…when
    2. is_a?
  3. 具體容易釐清但是修改成本高,抽象難懂但是可以輕易擴充
    1. 具體 → prepare_food, prepare_cars
    2. 抽象 → prepare
  4. 基本資料類別也能建立新的抽象方法,這稱為「Monkey patch」

藉由繼承取得行為

  1. 提升抽象而並非下放具體,白話文來說應把子類別的程式碼抽取共同的部分放到父類別

    class Bike ... def common_method end end # 新增 MountainBike class MountainBike < Bike def specific_method ... end end class RoadBike < Bike end # code 該怎麼移動?
  2. 可以使用 raise NotImplementedError 建立 Template 類別

  3. 使用 hook 使子類別和父類別解耦,避免使用 super

    class A def initialize(arg) @name = arg[:name] @age = arg[:arg] end end class B < A def initialize(arg) @weight = arg[:weight] super(arg) # 少這一步就會炸掉 end end # V2 class A def initialize(arg) @name = arg[:name] @age = arg[:arg] local_assign(arg) end private def local_assign(arg) end end class B < A def local_assign(arg) @weight = arg[:weight] end end

使用模組共用角色行為

  1. 里氏替換原則的進一步解釋,包含何時該用繼承
    1. 繼承條件嚴苛到根本沒有應用的機會

組合物件

  1. 組合(composition)和聚合(aggregate)的差別

    1. 組合代表的是什麼含有什麼
    2. 聚合是一種特殊的組合,被含有的東西含有其自己的獨立生命
  2. 組合、模組和繼承的比較

    1. 繼承:代表的是「is-a」,自動委派(delegation)訊息是繼承最大的特點,若使用得宜,在程式碼的可用性、合理性和典範性的表現上都很突出,因為繼承導致非常強的依賴關係,在大部分的情況下應首先選擇組合。
    2. 模組:代表的是「behavior likes a」,專注的是角色共用的這件事,如:可調度、可列印等等的角色行為
    3. 組合:代表的是「has-a」顧名思義,組合就是將不同功能的小物件組合起來,藉由定義相同的介面,可以自由的抽換其中的小物件,適應各種不同的變化
  3. aggregate 會承擔其 entities 的職責給外部使用, delegrate for enetities ( 自己補充

    1. 跟蒼時的討論的筆記
    2. tree vs graph
    order.update_amount(product_id, amount) class Order has_many :items def update_amount(product_id, amount) items.select { |i| i.pid == product_id }.update_amount(amount) end end class Item def update_amount ... end end

設計節省成本的測試

  1. A 物件的輸出等於 B 物件的輸入,A 物件的測試只需測到 A 物件的輸出即可
    1. 避免測到重複的東西
  2. 測試的輸出分為兩種,查詢和命令
    1. 查詢是資料
    2. 命令是 call method
  3. 減少重複或重疊的測試
  4. 測試之所以難寫,很大程度跟主程式的寫法有關係,如依賴注入與替身的關係
  5. BDD 和 TDD 都是先寫測試再實作,差別在於著重點:
    1. BDD: 從外而內
    2. TDD: 從內而外
  6. 使用 shared_test 共用角色行為
  7. 永遠不需要測試私有方法,若私有方法過於龐大,可以考慮抽取成一個新的物件,並增加該物件的測試
  8. 測試繼承時,若父類別為抽象,可以建立一個專用於測試的子類別
  9. 避免測試與主程式脫離,如某物件的 method 已經更名了,但由於 test 中是用 Stub 導致測試依然通過,此時如果該物件已有 expect_response_to(:method) 就可以即時發現原因
  10. 先寫測試的好處在於可以強迫自己使用可重複使用性的程式碼
  11. 如果建立真物件不會太麻煩的話,可以考慮直接注入真物件,而並非建立一個偽物件