一、單一職責(zé)原則(SRP)
單一職責(zé)原則(SRP)用于指導(dǎo)我們,在對功能劃分到具體的類中的時(shí)候,要保證具有高內(nèi)聚性。對于SRP的一個(gè)很好的描述是:就一個(gè)類而言,應(yīng)該僅有一個(gè)引起它變化的原因。
想要使用好SRP,一個(gè)首先要搞清楚的問題是:什么是職責(zé)?每一個(gè)職責(zé)都是變化的一個(gè)軸線,職責(zé)被定義為"變化的原因"。
這里對于職責(zé)的定義個(gè)人感覺比較清楚明確。我們要知道,SRP中的職責(zé)并不是一個(gè)類中的函數(shù),而是一個(gè)變化的軸線,具體到不同層次的類,其職責(zé)有大有小。如對于一個(gè)Activity來說,其負(fù)責(zé)的是展示一個(gè)完整的界面,那么界面的內(nèi)容獲取勢必從他里面發(fā)起,界面的數(shù)據(jù)處理勢必在其里面完成,一個(gè)Activity里面做的事情,調(diào)用的方法有很多,但是站在其層次的角度來考慮,一個(gè)Activity的職責(zé)就是負(fù)責(zé)好他所對應(yīng)的頁面,而其他頁面的事情對它來說就是多余的職責(zé)。
即SRP告訴我們,當(dāng)一個(gè)類中某幾個(gè)功能常常衍生出新的場景、新的實(shí)現(xiàn)方式時(shí),那么應(yīng)該考慮將他們各自進(jìn)行獨(dú)立的職責(zé)封裝,而不是在當(dāng)前類中不斷的改來改去、加來加去來使得當(dāng)前類變得臃腫、難以維護(hù)。
1、一個(gè)例子
舉一個(gè)例子,假設(shè)需要裝一臺電腦,以下是配置:
public class Computer {
private String CPU() {
return "最貴的最好的CPU";
}
private String board(){
return "最實(shí)惠的、型號對應(yīng)的主板";
}
private void build() {
String cpu = CPU();
String board = board();
return "電腦配置為"+cpu+board;
}
}
當(dāng)前來看這個(gè)類很單純,就是組裝一臺電腦嘛,完全可以勝任,目前的方案是在CPU上花錢,主板挑一個(gè)過得去的就行;但是,不同的人需求不一樣,有的人追求夠用即可在CPU上也追求實(shí)惠,有的人土豪一個(gè),在主板上也追求最貴最好。這樣的需求變動不得不使我們在Computer類中添加對應(yīng)的方法來滿足以上需求,這樣就導(dǎo)致了Computer類的臃腫和難以維護(hù)。
上述的問題在于,在可預(yù)見的未來,用戶選擇CPU的方案和選擇board的方案都會產(chǎn)生很多變化,即Computer類變化的軸線有兩個(gè):CPU和board,這違反了SRP,所以方案就是將CPU的選擇方案封裝成一個(gè)單獨(dú)的類CPU,將board的選擇方案封裝成一個(gè)單獨(dú)的類Board。同時(shí),這個(gè)例子也體現(xiàn)了單一職責(zé)也是分層次的,Computer的職責(zé)是負(fù)責(zé)組裝電腦,CPU的職責(zé)是負(fù)責(zé)確定哪款CPU,Board的職責(zé)是負(fù)責(zé)確定哪款主板。
另外,在上述問題中CPU的選擇方案與主板之間還存在一種約束關(guān)系(型號對不上),因此,如果不將職責(zé)進(jìn)行拆分的話,build方法還可能因?yàn)轳詈详P(guān)系,出現(xiàn)運(yùn)行錯(cuò)誤的情況,而這種情況當(dāng)代碼量很大時(shí)是很難察覺的。如果進(jìn)行了職責(zé)的拆分,Computer類只負(fù)責(zé)CPU和board方案的匹配校驗(yàn)工作,那么這種錯(cuò)誤發(fā)生的概率就會降低很多。
2. 小結(jié)
通過上述例子我們可以粗略大膽的認(rèn)為當(dāng)一下情況出現(xiàn)的時(shí)候,你的類違背了SRP,并且到了需要拆分的時(shí)候:
- 當(dāng)前類包含了多個(gè)子功能的具體實(shí)現(xiàn);
- 在應(yīng)用場景中,上述子功能分別會有多種可能的實(shí)現(xiàn)方案。
二、開放封閉原則(OCP)
開閉原則(OCP):軟件實(shí)體應(yīng)該是可擴(kuò)展的,但是不可修改的。
按照OCP設(shè)計(jì)出的模塊具有兩個(gè)特征:
- 對擴(kuò)展開放:模塊的行為是可以擴(kuò)展的;當(dāng)應(yīng)用的需求改變時(shí),可以對模塊進(jìn)行擴(kuò)展來滿足新的行為。
- 對更改封閉:對模塊進(jìn)行擴(kuò)展時(shí),不必改變模塊的源碼。
上面的兩個(gè)特征看起來有些矛盾,不改變源碼怎么進(jìn)行擴(kuò)展?實(shí)際上OCP要求我們使用抽象來定義行為,使用具體類來實(shí)現(xiàn)行為,擴(kuò)展也就是說使用新的具體類來擴(kuò)展行為;封閉也就是說在使用該行為時(shí)使用抽象類對象來囊括不同具體行為,這樣一來就不用了更改源碼了(實(shí)際上由于多了個(gè)具體類,那么最起碼初始化該具體類對象的地方還是相當(dāng)于修改了源碼,這是沒法避免的)。
同時(shí),還有一個(gè)重點(diǎn)在于,在使用OCP來定義行為的時(shí)候,一定要選擇程序中呈現(xiàn)頻繁變化的那些部分進(jìn)行抽象;如若不然,那么濫用OCP也會帶來很高的維護(hù)成本。
小結(jié)
通過上面的說明,我們對OCP可以有一個(gè)大概的認(rèn)知,即具有如下結(jié)構(gòu)的應(yīng)用:
- 定義類時(shí)采用抽象類,實(shí)例化時(shí)采用具體類,即所謂的左類型與右類型。
三、里式替換原則(LSP)
上述的OCP核心思想在于:利用抽象來定義行為,利用具體類來實(shí)現(xiàn)和實(shí)施不同種的行為。但是,在面向?qū)ο箝_發(fā)中還有另一種形式的繼承機(jī)制,即父類并不是抽象類,在一些情況下父類可以勝任很多工作,但有時(shí)候需要子類對象來擴(kuò)展一些工作,而不得不實(shí)例化子類對象,這時(shí)候就需要要求在實(shí)例化子類對象時(shí),他要能夠完成其父類角色的任務(wù)。
即:在大多數(shù)情況下Parent a = new Parent();a.fun1();即可完成工作,但有時(shí)候需要使用到子類:Parent b = new Child();b.fun1();b.fun2();才能夠完成工作。那么就需要子類的fun1()方法能夠像父類的該方法一樣承擔(dān)應(yīng)有的工作。
LSP的解釋如下:子類型必須能夠替換掉他們的基類型。并且在替換掉基類型之后,程序依然能夠運(yùn)行。
1. 一個(gè)例子
假設(shè)父類的sort函數(shù)可以實(shí)現(xiàn)對數(shù)組的升序排序:
class Parent {
int[] array;
public Parent(int[] a) {
this.array = a;
}
public void sort() {
實(shí)現(xiàn)對array的升序排序
...
}
}
class Child {
...
@override
public void sort(){
實(shí)現(xiàn)對array的降序排序
}
}
很顯然,上述的子類雖然是繼承了父類,并且重寫了父類的sort方法,但是將全局的所有父類對象的實(shí)現(xiàn)變?yōu)樽宇悓ο?,那么程序肯定會出問題。這就是LSP所約束的問題。
2. 小結(jié)
LSP和OCP看起來都是在說繼承的問題,但是他們所關(guān)注的場景不同,而且可以看出OCP顯然是遵從了LSP的,因?yàn)镺CP的背景為不同子類去擴(kuò)展抽象類所定義的行為的。
而LSP則告訴我們,子類在覆蓋父類的方法的時(shí)候不能夠任意實(shí)現(xiàn),而是要遵循父類對該方法的期望與要求。為此,有人提出了一種契約設(shè)計(jì)的方式:
- 契約是通過為每一個(gè)方法聲明的前置條件和后置條件來指定的;
- 要使一個(gè)方法得以執(zhí)行,前置條件必須要為真;執(zhí)行完畢后,后置條件必須為真;
- 派生類的前置條件和后置條件的規(guī)則為:在重新聲明派生類中的例程時(shí),只能使用相等或更弱的前置條件來替換基類的前置條件;只能使用相等或更強(qiáng)的后置條件來替換基類的后置條件。
四、依賴倒置原則(DIP)
開門見山,該原則的解釋為:
- 高層模塊不應(yīng)該依賴于低層模塊,二者都應(yīng)該依賴于抽象;
- 抽象不應(yīng)該依賴于細(xì)節(jié),細(xì)節(jié)應(yīng)該依賴于抽象。
如果高層模塊依賴于低層模塊,那么在不同的上下文中重用高層模塊會變得非常困難。
首先要明確一點(diǎn)的是,既然是高層模塊,那么其勢必要使用低層模塊提供的功能或者說服務(wù),如何做到高層不依賴于低層呢?答案就在后半句,抽象,不過要加上一種要求:抽象由高層模塊來定義,低層模塊去實(shí)現(xiàn)它。這樣一來,高層模塊就可以通過抽象類對象來使用想要的服務(wù),而不必理會低層模塊是如何實(shí)現(xiàn)它的,從而避免了對低層模塊的依賴。這也體現(xiàn)了第二個(gè)要求,是細(xì)節(jié)依賴抽象而不是抽象依賴細(xì)節(jié),即高層模塊一旦定義了想要的服務(wù),就不必理會低層模塊的具體實(shí)現(xiàn)方式,即這種服務(wù)一定要抽象到不用去理會實(shí)現(xiàn)細(xì)節(jié)。
很明顯,一個(gè)典型的例子就是計(jì)算機(jī)網(wǎng)絡(luò)協(xié)議的設(shè)計(jì),高層協(xié)議通過服務(wù)訪問點(diǎn)來使用下層協(xié)議所提供的服務(wù),而不必理會下層協(xié)議是何種協(xié)議。例如TCP是傳輸層協(xié)議,其只需要下層能夠?qū)?shù)據(jù)傳輸?shù)蕉思纯?,而不關(guān)心你是IPv4還是IPv6.
五、接口隔離原則(ISP)
接口隔離原則很簡單:
不應(yīng)該強(qiáng)迫客戶依賴于他們不用的方法
因?yàn)橐坏┻@些方法發(fā)生變化,他們不得不做出相應(yīng)的改變。
很顯然,出現(xiàn)上述情況時(shí),應(yīng)該對接口進(jìn)行拆分了。