rails 讀書會 - POODR - Ch7 使用module來分享角色的行為
2015-10-18 18:42:34
  • ruby的繼承無法處理多重繼承。
  • 使用分享角色的方式取代繼承。

Understanding Roles

角色的定義:多個不相關的class擁有相同的行為,我們把它定義成這些class在扮演分享同一個角色。這些class就會有一個扮演相同角色的相依性,這種相依性在設計的時候就要考慮進來。

Finding Roles

duck typing就是一種角色的概念。與第5章的duck typing不同的是我們想組織分享的行為,通常我們會將分享的行為定義在某個檔案中方便使用與參照,ruby的module就非常適合用來定義共用的method。

一旦在class使用了module來共用method,class可以呼叫的method就會擴大,基本上包含了:

  • 1. class自己實作的method。
  • 2. class由繼承體系得知的method。
  • 3. class由include module共用的method。
  • 4. class由繼承體系中的class,這些class裡include module共用的method。

Organizing Responsibilities

  • 選擇哪一些method需要共用的方法,就如同之前規劃承繼架構中所提到的方法。

Removing Unnecessary Dependencies

Writing the Concrete Code

Bicycle中有一系列有關schedule的method用來處理排程的問題。

class Bicycle
  attr_reader :schedule, :size, :chain, :tire_size
  # Inject the Schedule and provide a default
  def initialize(args={})
    @schedule = args[:schedule] || Schedule.new
    # ...
  end
  # Return true if this bicycle is available
  # during this (now Bicycle specific) interval.
  def schedulable?(start_date, end_date)
    !scheduled?(start_date - lead_days, end_date)
  end
  # Return the schedule's answer
  def scheduled?(start_date, end_date)
    schedule.scheduled?(self, start_date, end_date)
  end
  # Return the number of lead_days before a bicycle
  # can be scheduled.
  def lead_days
    1
  end
  # ...
end

require 'date'
starting = Date.parse("2015/09/04")
ending = Date.parse("2015/09/10")
b = Bicycle.new b.schedulable?(starting, ending)
# This Bicycle is not scheduled
# between 2015-09-03 and 2015-09-10
# => true

但我們發現除了Bicycle外,還有Vehicle與Mechanic也有相同schedule的需求,除了lead_days不同之外,schedulable?scheduled?的method都一樣。

Extracting the Abstraction

為了要共用method,我們將schedule相關的method從Bicycle抽出來放到schedulable這個module中。

module Schedulable
  attr_writer :schedule
  def schedule
    @schedule ||= ::Schedule.new
  end

  def schedulable?(start_date, end_date)
    !scheduled?(start_date - lead_days, end_date)
  end

  def scheduled?(start_date, end_date)
    schedule.scheduled?(self, start_date, end_date)
  end

  # includers may override
  def lead_days
    0
  end
end

這時候Bicycle就可以改寫成:

class Bicycle
  include Schedulable
  def lead_days
    1
  end
  # ...
end

require 'date'
starting = Date.parse("2015/09/04")
ending = Date.parse("2015/09/10")
b = Bicycle.new b.schedulable?(starting, ending)
# This Bicycle is not scheduled
# between 2015-09-03 and 2015-09-10
# => true

改成這樣的好處是Vehicle與Mechanic就採用同樣的方法就可以直接擁有schedule相關的method。

class Vehicle
  include Schedulable
  def lead_days
    3
  end
  # ...
end

class Mechanic
  include Schedulable
  def lead_days
    4
  end
  # ...
end

v = Vehicle.new v.schedulable?(starting, ending)
# This Vehicle is not scheduled
# between 2015-09-01 and 2015-09-10
# => true
m = Mechanic.new m.schedulable?(starting, ending)
# This Mechanic is not scheduled
# between 2015-02-29 and 2015-09-10
# => true

注意schedulable?scheduled?這兩個method因為在Bicycle等class中並沒有實作,所以這些method會直接從schedulable module中delegate(委派),也就是採用module中method的實作。但因為三個class都各自重新定義了lead_days,所以lead_days會使用class自己定義的天數。

  • 繼承與分享角色的差別,其實就是is-a(是一個xxx)與behaviors-like-a(行為像是一個xxx)的差別。

Looking Up Methods

尋找method的順序:

  • 1. 先找class中有沒有這個method的實作。
  • 2. 如果上面找不到,找class include的module中有沒有這個method的實作。
  • 3. 如果上面都找不到,呼叫method_missing並將method的名字當參數傳進去,如果沒有做什麼,最後往父類別繼續找method的實作。

Inheriting Role Behavior

module中還可以include其它的module,使得module變成有繼承體系的感覺,但同時也會增加系統的複雜度,也變的更難理解與維護,就看適不適合這樣子用了。

Writing Inheritable Code

Recognize the Antipatterns

只要出現下面兩種程式碼,表示有可能已經違反了設計的原則:

  • 一個物件必須根據自己是哪一種類別或資料型態來決定method要怎麼運作。
  • 一個method必須檢查傳進來的物件是哪一種類別或資料型態來做不同的事情。

基本上,在大多數的情況下,「檢查是哪一種類別或資料型態」都會是一個錯誤。

Insist on the Abstraction

  • 任何可以使用在父類別的程式碼,繼承父類別的子類別也應該可以運作。父類別不應該包含任何一個只能讓某些子類別才能用的method。這個原則同樣用於module。
  • 如果有一個子類別得到了來自於繼承或是include的method,卻將它實作成raise一個 "Dose not implement" 的exception。這就要重新思考這個子類別到底是不是真的符合繼承父類別的條件。
  • 如果你沒辦法抽象化出一個父類別,這表示繼承並不適用於你目前的系統。

Honor the Contract

簡言之就是Liskov Substitution Principle (LSP),一個子類別應該可以在任何可以使用父類別的地方取待父類別。

Use the Template Method Pattern

抽象的父類別定義method的界面,而繼承的子類別則複寫method的實作。

Preemptively Decouple Classes

盡量避免在子類別中呼叫super,取而待之是使用hook之類的作法,因為呼叫super表示你在父類別與子類別之間增加了相依性。例如:

不好的寫法:

class Car
  def initialize(move_action: nil, carry_action: nil)
    @move_action = move_action || Run.new
    @carry_action = carry_action || Carry.new
  end
end

class SportCar < Car
  def initialize(move_action: nil, carry_action: nil)
    super(move_action, carry_action)
    @move_action = RunFaster.new
  end
end

比較好的寫法:

class Car
  def initialize(move_action: nil, carry_action: nil)
    @move_action = move_action || Run.new
    @carry_action = carry_action || Carry.new
    init_action
  end

  private

  def init_action
  end
end

class SportCar < Car
  private

  def init_action
    @move_action = RunFaster.new
  end
end

在上面的例子中,init_action就是一個hook。不過使用hook的限制就是只能用在直接繼承的父子類別,在更多層的繼承關係反而會限制class的使用。例如:

class SportCar < Car
  private

  def init_action
    @move_action = RunFaster.new
  end
end

class CoolSportCar < Car
  private

  def init_action
    super
    @carry_action = CarryCool.new
  end
end

在上面的例子中,CoolSportCar為了要維持SportCar原本的行為,必須在init_action呼叫super。

Create Shallow Hierarchies

盡量避免過深的繼承階層關係,繼承階層越多,表示要找到對應的method定義或實作就越困難,同時在同一個階層體系下的class表示他們彼此之間都有相依性,這也會造成如果改了繼承階層中的class,會大大增加出包的機會。

Summary