之前去上保哥的DI課程,有附贈他的SOLID課程影片(Udemy),想想影片看完就那麼過去不太好,花點時間做了一點筆記,跟去年我參加iT邦幫忙鐵人賽寫的文章物件導向設計原則中的五個原則縮寫叫SOLID 比起來,感覺今年又更了解SOLID一點了
不過文章大部分是老師講話的截語,雖然我有潤飾一下並加了一點自己的理解,但如果有不通順或前後不達意,請多多包涵 🙂
內容目錄
前言
開發的時間長?還是維護的時間長?
是多人開發?還是一人開發?
長期維護的專案,需求變更程度如何?一個月一次?一年一次?
OOP的四個特性
抽象(Abstraction)
抽象是將真實世界的需求轉換成程是類別的過程(庚:abstract可以翻譯成簡化、簡單化,"將真實世界的需求簡化成程式類別"),而類別可以包含屬性或方法,屬性為靜態狀態,方法為動態行為
封裝(Encapsulation)
隱藏或保護類別內部實作的細節
繼承(Inheritance)
當你要擴充或修改類別中定義的行為。可以使用繼承建立為新類別並保留類別原有的功能。
但在覆寫使要記得只有virtual代表可以被子代複寫,執行時才會執行子代的方法
多型(Polymorphism)
多個型別擁有相同的介面,或指某個型別擁有多個子型別。在C#所有類別全部都繼承自object,換句話說C#所有型別都是object的多型。
內聚力與偶合力
先了解什麼是模組(Model)
以C#來說,其實C#沒有Model這個概念
但我們談OOP的Model時,可以想成是C#中某個類別、方法或組件
內聚力
內聚力是在一個模組內完成一件工作的度量指標
高內聚力,一個模組內只完成一件工作
內聚力高,意味著模組更容易獨立運作、更容易重複使用(例如一個類別只負責一件事
低內聚力,在一個模組內完成多份工作
內聚力低,意味著這個模組會造成難以維護/測試/重用/理解(例如一個類別或甚至一個方法程式碼五千多行
實務上,我們要盡力的設計出高內聚力的程式碼(SRP原則會提高內聚力)
耦合力
A模組與B模組相依性高,若改了B模組的內容,則A模組會受到影響,這是耦合力太高,導致改B壞A(而使用DIP原則可降低耦合力)
高內聚與低耦合本質上互斥,理論上提高內聚力,就會把工作切成多個類別,而切成更多類別時,就意味著耦和度越高,所以現實上會依工作需求有很多考量。
相依性
有用到類別或介面就會產生相依,宣告什麼型別,new什麼物件都算相依
原則
原則是一種概念或價值,用來導引你產生適切的行為與價值評量的方法
SOLID設計原則
依循SOLID原則,可以寫出比較好的程式碼
依循SOLID原則,能夠判斷程式碼的好壞
讓你知道什麼程式碼是好的,什麼程式碼是壞的,而且你知道要怎麼寫出好的程式碼
當然,前提是你要了解需求
原則不會教你怎麼寫code,每個人的需求都不一樣,原則是幫你分辨什麼code是好是壞
補充
Robert C. Martin整理了很多原則並濃縮提倡為物件導向設計原則,過程有增減與修改,於2004年以S、O、L、I、D順序排列,拼出SOLID(固體)設計原則。在物件導向設計原則中,SOLID是最有名也是蠻實用的幾個原則。
學習SOLID設計原則的好處
- 降低程式碼複雜度
- 具有較佳的程式碼可讀性
- 提升模組可重複利用性
- 讓模組具有高內聚力,低耦合力
- 當面臨需求變更時,可減少破壞現有模組的風險
單一職責原則 Single Responsibility Principle (SRP)
一個模組應該只有一個需要改變的理由
以上是最初理念,隨者時間演變Martin將版本改為:
一個模組應該只對一個角色負責。
所以來說,一個類別如果負擔太多責任時,就意味著該類別可以被切割
(What is Single Responsibility Principle)
所有功能都寫在一個類別中時,類別複雜度越高,程式碼越寫越長,要改code時、要找bug時找不到要改哪裡修哪裡,一個類別的方法太多時,也不知道要呼叫哪一個方法
所以當一段程式碼有重複利用的需求,那就可以考慮SRP,當然,前提是你要了解需求,若你沒有足夠的經驗來定義一個物件的責任,(或你還不了解需求),不用太早就進行SRP規劃
開放封閉原則 Open Closed Principle (OCP)
一個軟體製品應該對於擴展是開放的,但對於修改是封閉的。
應該藉由新增程式碼來擴充功能,而不是修改原本的程式碼。
可以透過繼承來輕鬆擴充
在C#中也可以使用擴充方法來擴充既有類別
或利用抽象,用抽象的型別來裝載一個物件
OCP的時機
既有類別已經定義清楚,處於一個強調穩定的狀態
或是你需要擴充現有類別,加入新需求的屬性或方法
又或是擔心修改現有程式碼會破壞現有系統,於是擴充它而不改原本的code
不過,並不是所有類別都需要可擴充性,這要看需求、看你對系統本身的理解。當然,若你的程式碼一開始就不具備可擴充性,其實也沒關係,我們可以透過重構的技術讓程式碼變得可擴充
OCP有很多實作方式,可以透過繼承,保留原本的code,並撰寫新的code,套用OCP有其成本也有其價值在,價值就是你不會改到原本的code,但成本就是-類別越來越多,若能從類別名稱判斷差異(v1、v2XDD),可讀性不一定會差。
後面會提到DIP原則,透過相依於interface來實踐OCP原則
選擇用抽象類別也可以實做OCP,抽象類別可以包含實作,interface不能包含實作,所以用interface好或抽像類別好哪個好不一定,抽象類別可以寫實作,所以耦和度會比較高,看人怎麼寫。
里氏替換原則 Liskov Substitution Principle (LSP)
Martin將里氏論文中的原文理解並改為以下解釋:
子類別可以被替換為其父類別
保哥註釋:若你的程式碼有採用繼承或介面,在父型別出現的地方,都可以用子型別來取代,而不會破壞程式原有的行為,又指子型別可以替換為父型別,而在程式執行的過程中不會有異常
LSP精神
確實正確的實作繼承與多型
(注意:為避免繼承的錯誤實作,C#中要確實的使用virtual和override,程式看到父類別有virtual才會正確的執行子代override的方法,轉型的過程中才不會出縣意想不到的錯誤,若無則會執行父類別的方法。)
LSP可以使用介面來實現,耦和度比較低,(我覺得用interface會比較好,我目前寫類別方法都沒加virtual的習慣,不能保證未來能否正確繼承)
(保哥喜歡用介面)
但抽象類別或是類別也能實現LSP(就是繼承要寫好,沒寫好容易掛掉,但是介面不會(不記得有,保險點說很少))
(注意Visual studio的任何警告,才不會出錯)
介面隔離原則 Interface Segregation Principle (ISP)
客戶不應該強迫相依於沒有使用的方法。
多個給客戶專用的介面,優於一個通用需求介面
在ISP原則下,interface裡面有多個方法的話,應該拆成多個介面,每個介面就只有一點點的方法,只用有用到的介面,沒用到的方法不要繼承,免得用錯
介面不要太大,剛好就好
使用interface可以容易達到鬆散耦合、容易安全重構、容易功能擴充
ISP時機
當介面需要分割時,或當類別使用的時機也可以被分割時
例如一個類別有十幾個方法,你繼承他是因為要用到某一個,那就把那個方法切成介面,你的新類別跟就類別就可以透過介面相依,達到鬆散耦合,就只個一個方法有關聯
相依反轉原則 Dependency Inversion Principle (DIP)
高層次的模組不應該依賴於低層次的模組,兩者都應該依賴於抽象介面
高接模組呼叫(new)低階模組,低階模組被高階呼叫(被new)
抽象介面不應該相依於具體實現,而是具體實現應該相依於抽象介面。
DIP的基本精神
所有類別都相依於抽象,而不是具體實作(可以透過DI來達到目的地)
直接相依:
相依於抽象:
所有人相依於介面就符合DIP精神
類別與類別之間沒有直接相依,而是相依於介面 ,複雜度就會下來,可維護性提高、可讀性也提高
這樣原本的緊密耦合關係變成鬆散偶合關係,可以依據需求,隨時抽換具體實作類別
var demo1 = new Demo(new GmailService());
var demo2 = new Demo(new OutlookService());
使用DIP的時機
想要降低耦和度時
希望類別都相依於抽象,讓團隊可以有效率的開發系統
先撰寫抽象介面就是把真實世界的需求轉換成類別的過程,規劃出介面後再開始寫類別,定義好介面、方法、API、屬性,先定義好,任何人看到介面就可以開使開發
符合DIP的話通常意味著也符合OCP與LSP原則
當然,如果程式沒有變更的需求,就不會動他拉
大量DIP可能對剛進入團隊的人有障礙。那如果是小系統可能怎麼寫都可以。這裡討論的是稍微大的需求,系統比較複雜,如何讓剛進來的成員快速了解需求掌握系統加入維護開發,可能系統用了很多pattern,新進人員改不動,而用DI可能更容易一些,只剩interface,就只是實做哪個interface的事情,這樣比較好理解,比叫好改,程式碼比較乾淨
我們就怕如果一個需求來,會改到很多地方,那就可以思考重構,看看有什麼改善空間
如果有依據SOLID設計出好的OOP,那最完美的情況是一次只要改一個地方,降低這個class每次更動需要更改數量
關於單元測試與SOLID
OOP做不好,模組太大不方便測試,程式碼耦和度太大不方便測試,SRP OCP、ISP做不好,單元測試當然做不好,因為程式碼品質不好,所以你不好寫測試。
寫測試的好處,是測試寫完就只有幾個方法要實作,那我們的主程式不可能太複雜,當然我們不會第一時間了解需求,所以不會一次寫出好的code,怎麼辦呢?,就是透過SOLID把現有的爛code改的稍為好一點,就比較容易進入單元測試。
重點
原則是一種概念或價值,用來導引你產生適切的行為與價值評量的方法
依循SOLID原則,可以寫出比較好的程式碼
依循SOLID原則,能夠幫助你判斷程式碼的好壞
談話
大部分人寫code時間多?還是debug時間多?
SOLID教你怎麼寫出好的Code,不是講對或錯,而是講程式的好與壞
實務開發都會先把程式碼寫在一起,驗證邏輯是否成功,再進行重構成好的code
抽象是有分等級的,抽象是理解需求,簡化概念