前幾天面試被問到這個,對這些掌握度是一知半解實在受不了,必須做個筆記瞭解一下
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 替換原則的定義是:『子類別必須能夠替代基礎類別』
我覺得這是比較難搞懂得一項原則,我自己的理解有兩種(不衝突)
- 不要繼承不必要的遺產,沒用到而去繼承反而是種累贅甚至會搞壞了整個系統也不一定。
- 確保底層的實做會遵守上層介面所定義的行為。
看下面的例子,有一個 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
因為遵循了父類別設定 talk
和 height
,所以呼叫 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 都會互相依賴,通常遵照一兩項規範下去做,也會連帶其他的效果,其實目的都是讓程式更好維護及擴充。