SOLID 原則在Ruby中的應(yīng)用(譯)

原文鏈接: 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 LiskovLSP原則的定義如下:

如果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

在這個例子中有ComputerProgramer娃圆,Technician三個類玫锋。其中ProgramerTechnician會使用到電腦讼呢,而且是以不同的方式使用撩鹿,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)

依賴倒置原則代表了一種軟件模塊解耦的方式,他的定義有兩部分:

  1. 上層模塊不應(yīng)該依賴下層模塊音诫,他們應(yīng)該都依賴抽象惨奕。
  2. 抽象不能依賴具體實現(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)用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末箍铲,一起剝皮案震驚了整個濱河市拨齐,隨后出現(xiàn)的幾起案子驰贷,更是在濱河造成了極大的恐慌盛嘿,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件括袒,死亡現(xiàn)場離奇詭異次兆,居然都是意外死亡,警方通過查閱死者的電腦和手機锹锰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門芥炭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人恃慧,你說我怎么就攤上這事园蝠。” “怎么了痢士?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵彪薛,是天一觀的道長。 經(jīng)常有香客問我怠蹂,道長善延,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任城侧,我火速辦了婚禮易遣,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘嫌佑。我一直安慰自己豆茫,他們只是感情好,可當我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布屋摇。 她就那樣靜靜地躺著揩魂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪摊册。 梳的紋絲不亂的頭發(fā)上肤京,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天,我揣著相機與錄音茅特,去河邊找鬼。 笑死棋枕,一個胖子當著我的面吹牛白修,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播重斑,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼兵睛,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起祖很,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤笛丙,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后假颇,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體胚鸯,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年笨鸡,在試婚紗的時候發(fā)現(xiàn)自己被綠了姜钳。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡形耗,死狀恐怖哥桥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情激涤,我是刑警寧澤拟糕,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站倦踢,受9級特大地震影響送滞,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜硼一,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一累澡、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧般贼,春花似錦愧哟、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至腮介,卻和暖如春肥矢,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背叠洗。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工甘改, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人灭抑。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓十艾,卻偏偏與公主長得像,于是被迫代替她去往敵國和親腾节。 傳聞我的和親對象是個殘疾皇子忘嫉,可洞房花燭夜當晚...
    茶點故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容