原文鏈接: SOLID Principles in Ruby
轉(zhuǎn)載請注明出處:http://www.tedyin.me/2016/02/27/solid-principles-in-ruby/
作為一名程序員無論你的水平高低润匙,你都會想寫出一手優(yōu)秀的代碼,但是想寫優(yōu)秀的代碼并不容易率拒,因此怎樣才能提高我們的代碼質(zhì)量呢好渠?下面來看下我們今天的主角 SOLID 原則
SOLID 原則是什么
SOLID 不是一個原則榛丢,他是一組面向?qū)ο笤O(shè)計原則的簡稱辱揭,他代表下面5種設(shè)計原則:
- S ingle Responsibility Principle 單一職責原則
- O pen/Closed Principle 開閉原則
- L iskov Substitution Principle 里氏替換原則
- I nterface Segregation Principle 接口分離原則
- D ependency Inversion Principle 依賴倒置原則
以上就是SOLID中的5種面向?qū)ο笤O(shè)計原則亡容,下面分別看看他們具體指的是什么躺屁。
單一職責原則(SPR)
在我看來這個是最簡單的一個設(shè)計原則叭莫,SPR
的說明如下:
每一個類或則方法都應(yīng)該有且僅有一個職責蹈集,而且他的這個職責應(yīng)該被完全封裝在這個類里面。
如何去判斷你的代碼是否符合這一原則的最好方式就是去問問自己:
這個類或者方法到底做了什么雇初?
如果他干了不只一件事情的話拢肆,那么他就違反了SPR
原則。下面來看一個Student
類靖诗,每個Student
對象都有grades
屬性郭怪。
class Student
attr_accessor :first_term_home_work, :first_term_test,
:first_term_paper
attr_accessor :second_term_home_work, :second_term_test,
:second_term_paper
def first_term_grade
(first_term_home_work + first_term_test + first_term_paper) / 3
end
def second_term_grade
(second_term_home_work + second_term_test + second_term_paper) / 3
end
end
也許有些人已經(jīng)意識到了,上面的寫法是錯誤的呻畸,也許有些人還沒有感覺到移盆。不管有沒有意識到,上面的代碼顯然是沒有循序SPR
原則的伤为,原因就是 Student 類擁有計算每個學期平均分的邏輯咒循,但是Student
類是用來封裝關(guān)于學生信息的而不是用來計算分數(shù)的,計算分數(shù)的邏輯應(yīng)當放在Grade
類中才對绞愚。下面我們遵循SPR
原則重構(gòu)一下代碼叙甸,重構(gòu)后的代碼如下:
class Student
def initialize
@terms = [
Grade.new(:first),
Grade.new(:second)
]
end
def first_term_grade
term(:first).grade
end
def second_term_grade
term(:second).grade
end
private
def term reference
@terms.find {|term| term.name == reference}
end
end
class Grade
attr_reader :name, :home_work, :test, :paper
def initialize(name)
@name = name
@home_work = 0
@test = 0
@paper = 0
end
def grade
(home_work + test + paper) / 3
end
end
重構(gòu)之后的代碼Student
類中的計算分數(shù)的邏輯移到了Grade
類中,Student
類中只有對Grade
實例的引用∥获茫現(xiàn)在所有的類都遵循SPR
原則裆蒸,因為每個類都是職責單一的。
開閉原則(OCP)
開閉原則的定義如下:
一個類或者模塊對擴展開放糖驴,對修改關(guān)閉僚祷。
什么意思呢佛致?他的意思就是:一旦一個類已經(jīng)實現(xiàn)了當時的需求,他就不應(yīng)該為了去實現(xiàn)接下來的需求而被修改辙谜。你是不是覺得這樣做沒有意義俺榆?那我們下面看個例子來說明一下:
class MyLogger
def initialize
@format_string = "%s: %s\n"
end
def log(msg)
STDOUT.write @format_string % [Time.now, msg]
end
end
這是一個簡單的logger類,他可以將把給定的msg和當時的時間通過STDOUT
格式化輸出出來装哆。非常簡單對吧罐脊,下面來測試一下:
irb> MyLogger.new.log('test!')
=> 2016-02-28 16:16:32 +0200: test!
測試OK,沒什么問題蜕琴,但是假如在以后的某一天萍桌,我們想改變一下輸出的格式,想實現(xiàn)下面的日志格式
=> [LOG] 2016-02-28 16:16:32 +0200: test!
怎么辦呢凌简?假如現(xiàn)在由一個不懂OCP
原則的程序員來實現(xiàn)上述格式上炎,實現(xiàn)的代碼如下:
class MyLogger
def initialize
@format_string = "[LOG] %s: %s\n"
end
end
輸出的結(jié)果如下:
irb> MyLogger.new.log('test!')
=> [LOG] 2016-02-28 16:16:32 +0200: test!
輸出的格式完全符合要求,一切都OK号醉,但是這樣做真的就對嗎反症?
仔細想想辛块,假如上面被修改的類是一個App中的核心類畔派,對format_string
方法的修改,可能會破壞那些依賴MyLogger
類的方法使得他們不能正常的工作润绵。也許在APP中存在許許多多的類都依賴剛才說的那些方法线椰,但是現(xiàn)在我們修改了代碼,破壞了這些類和方法尘盼。這就是在破壞OCP
原則憨愉,這會導致災(zāi)難性的后果。
既然不遵循OCP
原則會有很嚴重的問題卿捎,那么實現(xiàn)上面修改日志格式需求的正確姿勢是什么呢配紫?毫無疑問當然是繼承
或者組合
!
我們來看看下面使用繼承的例子:
class NewCoolLogger < MyLogger
def initialize
@format_string = "[LOG] %s: %s\n"
end
end
irb> NewCoolLogger.new.log('test!')
=> [LOG] 2016-02-28 16:16:32 +0200: test!
棒呆午阵!和我們預(yù)期的一樣躺孝,那MyLogger
的輸出呢?
irb> MyLogger.new.log('test!')
=> 2016-02-28 16:16:32 +0200: test!
還是棒呆底桂!那么我剛剛都干了些啥呢植袍?我們創(chuàng)建了一個新的NewCoolLogger
類,擴展(extend)
了MyLogger
類。那些之前依賴老的logger方法的類和方法依然可以正常的工作籽懦,老的logger還是和以前一樣提供相同的方法于个,新的logger則提供新的logger方法,這是我們所期待的暮顺。
我剛才說了兩種重構(gòu)方式厅篓,下面我們來看看使用另外一種方式組合
來重構(gòu)代碼的例子:
class MyLogger
def log(msg, formatter: MyLogFormatter.new)
STDOUT.write formatter.format(msg)
end
end
我們可以注意到秀存,log方法多了一個可選參數(shù)formatter
,對于日志格式化的事情本來就應(yīng)該是MyLogFormatter
類的事情羽氮,而不應(yīng)該是logger類的事情应又。使用上面的方式重構(gòu)更好,因為這樣做了之后MyLogger#log
可以接受各種各樣不同的格式化方式乏苦,而且MyLogger
也不在需要去關(guān)心具體的格式化格式株扛,因為他只需要一條String
,具體是什么格式的則由傳入MyLogger#log
個格式化類來確定汇荐。假如我們又要實現(xiàn) Error Log 輸出洞就,現(xiàn)在簡單了只需要傳入一個ErrorLogFormatter
實例即可輸出帶有 "[ERROR]" 前綴的日志。
里氏替換原則(LSP)
Barbara Liskov對LSP
原則的定義如下:
如果S是T的一個子類掀淘,那么不需要修改代碼中的任何配置和屬性旬蟋,S的實例也可以替換T的實例對象,而且不影響代碼的正常運行革娄。
坦白的講倾贰,我覺得這個定義是非常難理解的,因此經(jīng)過一番思考拦惋,總結(jié)下來如下:
假如現(xiàn)在有一個Bird
類匆浙,還有兩個實例對象 obj1 和 obj2。obj1 是 Duck 類的對象厕妖,Duck 類是 Bird 類的子類首尼,obj2 是 Pigeon 類的對象,Pigeon 類也是Bird 類的子類言秸。LSP
原則的意思是软能,obj2 是Bird子類的實例,obj1 是Bird子類的實例举畸,因此我們應(yīng)當把 obj1 和 obj2 等同對待查排,都當做Bird的實例對待。
譯者注:其實我覺得上面的定義已經(jīng)說的很清楚了抄沮,上面說的 obj1 之類的例子有點多余跋核。。合是。
下面我們來看個例子來說明下:
class Person
def greet
puts "Hey there!"
end
end
class Student < Person
def years_old(age)
return "I'm #{age} years old."
end
end
person = Person.new
student = Student.new
# LSP原則的意思是如果我知道 Person 擁有的接口了罪,那么我應(yīng)該也能猜到 Student 擁有的接口,因為 Student 類是 Person 的子類聪全。
student.greet
# returns "Hey there!"
以上就是對LSP
原則的解釋
接口分離原則(ISP)
接口分離原則的定義如下:
不應(yīng)該強迫客戶端依賴一些他們用不到的方法或接口泊藕。
就像定義那樣很簡單,我們來看看代碼說明一下:
class Computer
def turn_on
# turns on the computer
end
def type
# type on the keyboard
end
def change_hard_drive
# opens the computer body
# and changes the hard drive
end
end
class Programmer
def use_computer
@computer.turn_on
@computer.type
end
end
class Technician
def fix_computer
@computer.change_hard_drive
end
end
在這個例子中有Computer
,Programer
娃圆,Technician
三個類玫锋。其中Programer
,Technician
會使用到電腦讼呢,而且是以不同的方式使用撩鹿,Programer
使用的是type
方法,Technician
用的是change_hard_drive
悦屏,按照LSP
原則要求 不應(yīng)當強迫客戶端依賴一些他們用不到的接口或者方法节沦,Programer
類用不到change_hard_drive
方法,同樣的Technician
用不到type
方法础爬,但是一旦這兩個方法發(fā)生變化甫贯,那么就有可能影響到Programer
或者Technician
類的正常使用。下面我們重構(gòu)一下代碼看蚜,來滿足LSP
原則
class Computer
def turn_on
end
def type
end
end
class ComputerInternals
def change_hard_drive
end
end
class Programmer
def use_computer
@computer.turn_on
@computer.type
end
end
class Technician
def fix_computer
@computer_internals.change_hard_drive
end
end
經(jīng)過重構(gòu)后Technician
使用了ComputerInternals
類的對象叫搁,這個類封裝了從Computer
中分離出來的方法change_hard_drive
。現(xiàn)在Computer
類可以受到Programer
類的影響(寫代碼改變OS)供炎,但是再也影響不到Technician
類了渴逻。
依賴倒置原則(DIP)
依賴倒置原則代表了一種軟件模塊解耦的方式,他的定義有兩部分:
- 上層模塊不應(yīng)該依賴下層模塊音诫,他們應(yīng)該都依賴抽象惨奕。
- 抽象不能依賴具體實現(xiàn),具體實現(xiàn)應(yīng)該依賴抽象纽竣。
我知道這個理解起來有點繞墓贿,但是在開始看具體的例子之前,我希望你不要把 依賴倒置 和 依賴注入 弄混淆蜓氨,后者是一種軟件技巧或者說是一種軟件設(shè)計模式,而前者是面向?qū)ο笤O(shè)計原則的一種队伟。
好了下面來看看具體的例子:
class Report
def initialize
@body = "whatever"
end
def print
XmlFormatter.new.generate @body
end
end
class XmlFormatter
def generate(body)
# convert the body argument into XML
end
end
Report
類是用來生成 XML 報表的穴吹,在他的初始化方法中,我們設(shè)置了報表內(nèi)容(body)嗜侮,print
方法使用XmlFormatter
類去將報表內(nèi)容轉(zhuǎn)換成 XML 格式港令。
下面我們來看看Report
這個類,從這個類的名字我們能看出來他是個普通的類锈颗,會返回某種類型的報表(report)顷霹,但是他沒告訴我們會返回哪種格式的報表。事實上對于上面這個例子我們能夠很輕松的將Report
重命名為XmlReport
因為我們知道他的實現(xiàn)細節(jié)击吱,知道他只實現(xiàn)了導出 XML 報表的功能淋淀,但是與其讓Report
變的更加具體(丟失更多的擴展性),我們還不如好好想想怎么去將他更好的抽象覆醇。
目前我們的類依賴XmlReport
類和他的generate
方法朵纷,Report
依賴的是一個具體的實現(xiàn)而不是抽象炭臭,只有當提供格式化方法的類是XmlFormatter
的時候,我們的Report
類才能正常的工作袍辞。假如我們現(xiàn)在想導出 CSV 或者 JSON 格式的報表怎么辦鞋仍?那我們就只能提供更多的具體的方法,比如print_xml
搅吁,print_csv
威创,print_json
等。這意味著Report
類和具體實現(xiàn)綁的非常緊谎懦,耦合非常高那婉,他依賴格式化類的類型,但卻不依賴這些格式化類的抽象党瓮。
譯者注:Report 類就是只知道有這么多個格式化類详炬,但是卻不知道他們之間有什么共同特點,依賴這些具體的類卻不依賴他們的共同特點寞奸,也就是不依賴抽象呛谜。假如現(xiàn)在又有新的格式,Report 還得去了解新的格式類枪萄,如果依賴他們共同擁有的一個格式化的接口隐岛,那Report就不用去操心你這個格式化的類到底是格式化成啥了,我直接調(diào)用這個格式化的方法就行了瓷翻。
下面我們重構(gòu)一下代碼:
class Report
def initialize
@body = "whatever"
end
def print(formatter)
formatter.generate @body
end
end
class XmlFormatter
def generate(body)
# convert the body argument into XML
end
end
注意print
方法聚凹,他知道自己需要一個 formatter,但是他關(guān)心的是這個 formatter 的接口齐帚。更具體地講妒牙,他只關(guān)心這個 formatter 能夠給他提供的 generate
方法,具體是什么樣的 formatter 他不在乎对妄,只要能提供generate
方法湘今,幫他完成格式化大業(yè)就行。這樣設(shè)計大家有沒有覺得更靈活呢剪菱?假如我們現(xiàn)在需要 CSV 格式的報表摩瞎,我們只需要提供下面這個類就行了。
class CSVFormatter
def generate(body)
# convert the body argument into CSV
end
end
Report#print
方法將會接收一個CSVFormatter
類的實例對象孝常,這個實例對象能夠?qū)蟊韮?nèi)容轉(zhuǎn)換成 CSV 格式旗们。
OK,到此為止 SOLID 五中面向?qū)ο笤O(shè)計原則已經(jīng)講完了,希望大家在日常編寫代碼的過程中能好好應(yīng)用。