惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 三生石上(FineUI控件)
S
SegmentFault 最新的问题
Jina AI
Jina AI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
IT之家
IT之家
美团技术团队
V
Visual Studio Blog
人人都是产品经理
人人都是产品经理
博客园 - 叶小钗
有赞技术团队
有赞技术团队
GbyAI
GbyAI
宝玉的分享
宝玉的分享
腾讯CDC
M
MIT News - Artificial intelligence
博客园 - 【当耐特】
Google DeepMind News
Google DeepMind News
月光博客
月光博客
MyScale Blog
MyScale Blog
Last Week in AI
Last Week in AI
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 司徒正美
Recent Announcements
Recent Announcements
MongoDB | Blog
MongoDB | Blog

Nic Lin's Blog

謝明真 - 高效領導力的課後筆記 NFT 開發實戰!基礎智能合約入門 (3) NFT 開發實戰!基礎智能合約入門 (2) NFT 開發實戰!基礎智能合約入門 (1) 如何自我檢測 log4j CVE 漏洞 Rails 如何在資料寫入時記錄來源 IP 位置 如何經營工程師 Youtube 頻道 - Part 8 營收篇 如何經營工程師 Youtube 頻道 - Part 7 酸民文化篇 如何經營工程師 Youtube 頻道 - Part 5 設備器材篇 如何經營工程師 Youtube 頻道 - Part 4 後製剪輯篇 如何經營工程師 Youtube 頻道 - Part 3 文案企劃篇 如何經營工程師 Youtube 頻道 - Part 2 設備器材篇 如何經營工程師 Youtube 頻道 - Part 1 制訂頻道方向篇 如何經營工程師 Youtube 頻道 - Part 0 Rails 中避免 race condition 的最佳實踐(二) Rails 中避免 race condition 的最佳實踐(一) 10 分鐘整合 google sheet 做自動化開發功能週報 經營 Side Project 300 天所帶來的收穫及挑戰 我的 Youtube 影片製作流程 API 設計時必須注意的 HTTP header 底線問題 如何提升你的程式可讀性之實務技巧(三) 如何提升你的程式可讀性之實務技巧(二) 如何提升你的程式可讀性之實務技巧(一) Ruby 中使用 freeze 優化效能的時機 避免 React 中的 useEffect 無限 render 在 Rails 內輕量使用 Vue Component 的最佳實踐 如何在區域網路用 Docker 架設有 SSL 的 Gitlab 從被問到問人,那些我常問的面試問題 [Rails] 如何漂亮寫出可維護的 query (Maintainable Rails Query) 在已知長度情況下優化 slice 的性能
物件導向基本原則 SOLID (Ruby Sample)
Nic Lin · 2018-11-18 · via Nic Lin's Blog

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

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 都會互相依賴,通常遵照一兩項規範下去做,也會連帶其他的效果,其實目的都是讓程式更好維護及擴充。

參考