Nic Lin's Blog

喜歡在地上打滾的 Rails Developer

物件導向基本原則 SOLID (Ruby Sample)

前幾天面試被問到這個,對這些掌握度是一知半解實在受不了,必須做個筆記瞭解一下

SOLID 是 OOP 以下幾個原則的縮寫

  • S : Single Responsibility Principle (單一職責原則)
  • O : Open Closed Principle (開放封閉原則)
  • L : Liskov Substitution Principle (里氏替換原則)
  • I : Interface Segregation Principle (介面隔離原則)
  • D : Dependency Inversion Principle (依賴反轉原則)

Single Responsibility Principle (單一職責原則)

『你只有一個理由需要更改這個class,如果有一個以上的理由就表示:這個class負責超過一個以上的責任。』

先看以下違反 SRP 的例子:

class DealProcessor
  def initialize(deals)
    @deals = deals
  end

  def process
    @deals.each do |deal|
      Commission.create(deal: deal, amount: calculate_commission)
      mark_deal_processed
    end
  end

  private

  def mark_deal_processed
    @deal.processed = true
    @deal.save!
  end

  def calculate_commission
    @deal.dollar_amount * 0.05
  end
end

透過這個 class 我們可以處理交易的佣金支付,但如果要不斷增加複雜的策略或規則,都必須進到這個 class 裡面來做修改。

所以更好的作法應該是把計算佣金的部分單獨抽出來做一個 class,這樣日後修改維護或寫測試就能有更佳的彈性。

class DealProcessor
  def initialize(deals)
    @deals = deals
  end

  def process
    @deals.each do |deal|
      mark_deal_processed
      CommissionCalculator.new.create_commission(deal)
    end
  end

  private

  def mark_deal_processed
    @deal.processed = true
    @deal.save!
  end
end

class CommissionCalculator
  def create_commission(deal)
    Commission.create(deal: deal, amount: deal.dollar_amount * 0.05)
  end
end

這樣一來,負責標記交易和處理佣金計算的責任就拆分開來了,就是解耦的部分。

Open Closed Principle (開放封閉原則)

概念:讓程式「對擴充開放」「對修改封閉」

看一下以下這個違反 OCP 原則的範例:

class UsageFileParser
  def initialize(client, usage_file)
    @client = client
    @usage_file = usage_file
  end

  def parse
    case @client.usage_file_format
      when :xml
        parse_xml
      when :csv
        parse_csv
    end

    @client.last_parse = Time.now
    @client.save!
  end

  private

  def parse_xml
    # parse xml
  end

  def parse_csv
    # parse csv
  end
end

透過這個範例可以發現,如果今天我想要加入一個 yaml 的解析,那就會直接改動 UsageFileParser,在下面的 private 加入一個 parse_yaml 方法,並且在 public 的 parse 中還要多一個 case。

我們應該希望在不變動原本的內容下新增新的解析器,這樣一來可以直接讓原本的 class 有更佳的覆用性,並且也同時遵守 SRP 原則,每個 class 只負責他該負責的。

所以我們應該把每一種解析器單獨抽出來,並且改寫如下

class UsageFileParser
  def initialize(client, parser)
    @client = client
    @parser = parser
  end

  def parse(usage_file)
    parser.parse(usage_file)
    @client.last_parse = Time.now
    @client.save!
  end
end

class XmlParser
  def parse(usage_file)
    # parse xml
  end
end

class CsvParser
  def parse(usage_file)
    # parse csv
  end
end

這樣一來,我就可以透過

UsageFileParser.new(a_client, XmlParser) # 解析 xml
UsageFileParser.new(b_client, CsvParser) # 解析 csv

如果還要加入 yaml, 也只需要再做一個 YamlParser 的 class 就可以使用。

這樣一來就達到「對擴充開放」「對修改封閉」了。

Liskov Substitution Principle (里氏替換原則)

Liskov 替換原則的定義是:『子類別必須能夠替代基礎類別』

我覺得這是比較難搞懂得一項原則,我自己的理解有兩種(不衝突)

  1. 不要繼承不必要的遺產,沒用到而去繼承反而是種累贅甚至會搞壞了整個系統也不一定。
  2. 確保底層的實做會遵守上層介面所定義的行為。

看下面的例子,有一個 Person 類別,而 Student 和 Women 分別繼承其 class

class Person
  def talk
   ''
  end
  
  def height
   ''
  end
end


class Student < Person
  def talk
    '我是一年一班 Nic'
  end
  
  def height
    '155cm'
  end
end

class Women < Person
  def talk
    { say: "我是蒼井空,來自日本" }
  end
  
  def height
   '160cm'
  end
end
women = Women.new
student = Student.new

def introduce_by_person(person)
  puts "Hi, I'm #{person.height} height and I say #{person.talk}"
end

introduce_by_person(women) # Hi, I'm {:cm=>"160"} height and I say {:say=>"我是蒼井空,來自日本"}
introduce_by_person(student) # Hi, I'm 155cm height and I say 我是一年一班 Nic

因為遵循了父類別設定 talkheight,所以呼叫 introduce_by_person 方法時,也都能正確輸出。

不過這邊可以注意到的是 Women 中並沒有遵循 LSP 原則,他沒有按照原本 weight 的定義修改,原本應該是輸出 String,但卻改為 Hash 這將造成程式會出現「不可預測」性,換句話說就是可能產生不可預知或是不容易察覺的 bugs

Interface Segregation Principle (介面隔離原則)

用戶不應該被迫相依於他們用不到的函示

如果有用不到的函示,應該做成不同的 interface 或是 protocal

有要用到再裝上去就好

舉個例子,有個計算 Fee 的 class

class FeeCalculator
  def calculate(product, user, vat)
    # calculation
  end
end

會用在 ProductController 中

class ProductController
  def show
    @fee = FeeCalculator.new.calculate(product, user, vat)
  end
end

但如果我今天對 Fee 的 class 新增了一個行為,必須將結果存進 database

class FeeCalculator
  def calculate(product, user, vat, save_result)
    # calculation

    if save_result
      # storing result into datebase
    end
  end
end

這時候就會發現,我們只有在 create 時要觸發 save_result,但 Show 不需要啊

那就會變成,每一次使用都還要去新增一個參數傳遞

class ProductController
  def show
    @fee = FeeCalculator.new.calculate(product, user, vat, false)
  end
end

class OrderController
  def create
    @fee = FeeCalculator.new.calculate(product, user, vat, true)
  end
end

但其實 Show 行為他根本不需要瞭解要不要存進 database,他只是想計算 Fee 啊!

所以 save_result 這個行為應該單獨在抽出來變成一個方法或是介面

class FeeCalculator
  def calculate(product, user, vat)
    # calculation
  end

  def save(fee)
    # storing result into db
  end
end

這樣看起來就俐落多了,也遵循 ISP 原則了

class ProductController
  def show
    @fee = FeeCalculator.new.calculate(product, user, vat)
  end
end

class OrderController
  def create
    fee_calculator = FeeCalculator.new
    
    fee = fee_calculator.calculate(product, user, vat)
    fee_calculator.save(fee)
  end
end

Dependency Inversion Principle (依賴反轉原則)

program to an interface, not an implementation

原本上層的類別會依賴下層的類別,就如同要蓋二樓就必須蓋好一樓 但 Dependency-Inversion Principle 的意思就是應該要讓上層和下層都依賴於抽象層,也就是上面重點說的 interface

A 物件程式內部需要使用 B 物件 A, B物件中有依賴的成份

依賴反轉把原本 A 對 B 直接控制權移交給由第三方容器

降低 A 對 B 物件的耦合程度,並讓雙方都倚賴抽象。

來看以下的例子

class Newsperson
  def broadcast(news)
    Newspaper.new.print(news)
  end
end

class Newspaper
  def print(news)
    puts news
  end
end

laura = Newsperson.new
laura.broadcast("Some Breaking News!") # => "Some Breaking News!"

可以看的出來 Newsperson 依賴較低層級的 Newspaper

現在廣播與報紙的內容相關連了,如果我們更改報紙的名稱或是添加更多廣播平台,這兩件事都會導致我們不得不改變 Newsperson,儘管看起來依賴程度非常小。

現在試著重構來遵循 DIP 原則

class Newsperson
  def broadcast(news, platform = Newspaper)
    platform.new.broadcast(news)
  end
end 

class Newspaper
  def broadcast(news)
    do_something_with news
  end
end

class Twitter
  def broadcast(news)
    tweets news
  end
end

class Television
  def broadcast(news)
    live_coverage news
  end
end 

laura = Newsperson.new
laura.broadcast("Breaking news!") #do_something_with "Breaking news!"
laura.broadcast("Breaking news!", Twitter) #tweets "Breaking news!"

現在高級別的 Newsperson 不再依賴低級別的 class,而且對各平台的 class 更容易測試了

重點:系統中模組建議依賴抽象,因為各個模組間不需要知道對方太多細節(實作),知道越多耦合越強。

小結

其實這些 principle 都會互相依賴,通常遵照一兩項規範下去做,也會連帶其他的效果,其實目的都是讓程式更好維護及擴充。

參考

comments powered by Disqus