【转】设计模式六大原则——SOLID
SOLID設(shè)計(jì)模式的六大原則有:
把這六個(gè)原則的首字母聯(lián)合起來( L 算做一個(gè))就是 SOLID (solid,穩(wěn)定的),其代表的含義就是這六個(gè)原則結(jié)合使用的好處:建立穩(wěn)定、靈活、健壯的設(shè)計(jì)
下面我們來分別看一下這六大設(shè)計(jì)原則。
單一職責(zé)原則(Single Responsibility Principle)
單一職責(zé)原則簡稱 SRP ,顧名思義,就是一個(gè)類只負(fù)責(zé)一個(gè)職責(zé)。它的定義也很簡單:
There should never be more than one reason for a class to change.
這句話很好理解,就是說,不能有多個(gè)導(dǎo)致類變更的原因。
那這個(gè)原則有什么用呢,它讓類的職責(zé)更單一。這樣的話,每個(gè)類只需要負(fù)責(zé)自己的那部分,類的復(fù)雜度就會降低。如果職責(zé)劃分得很清楚,那么代碼維護(hù)起來也更加容易。試想如果所有的功能都放在了一個(gè)類中,那么這個(gè)類就會變得非常臃腫,而且一旦出現(xiàn)bug,要在所有代碼中去尋找;更改某一個(gè)地方,可能要改變整個(gè)代碼的結(jié)構(gòu),想想都非??膳隆.?dāng)然一般時(shí)候,沒有人會去這么寫的。
當(dāng)然,這個(gè)原則不僅僅適用于類,對于接口和方法也適用,即一個(gè)接口/方法,只負(fù)責(zé)一件事,這樣的話,接口就會變得簡單,方法中的代碼也會更少,易讀,便于維護(hù)。
事實(shí)上,由于一些其他的因素影響,類的單一職責(zé)在項(xiàng)目中是很難保證的。通常,接口和方法的單一職責(zé)更容易實(shí)現(xiàn)。
單一原則的好處:
代碼的粒度降低了,類的復(fù)雜度降低了。
可讀性提高了,每個(gè)類的職責(zé)都很明確,可讀性自然更好。
可維護(hù)性提高了,可讀性提高了,一旦出現(xiàn) bug ,自然更容易找到他問題所在。
改動代碼所消耗的資源降低了,更改的風(fēng)險(xiǎn)也降低了。
里氏替換原則(Liskov Substitution Principle)
里氏替換原則的定義如下:
Functions that use use pointers or references to base classes must be able to use objects of derived classes without knowing it.?
翻譯過來就是,所有引用基類的地方必須能透明地使用其子類的對象。
里氏替換原則的意思是,所有基類在的地方,都可以換成子類,程序還可以正常運(yùn)行。這個(gè)原則是與面向?qū)ο笳Z言的 繼承 特性密切相關(guān)的。
這是為什么呢?由于面向?qū)ο笳Z言的繼承特性,子類擁有父類的所有方法,因此,將基類替換成具體的子類,子類也可以調(diào)用父類中的方法(其實(shí)是它自己的方法,繼承于父類),但是如果要保證完全可以調(diào)用,光名稱相同不行,還需要滿足下面兩個(gè)條件:
子類中的方法的前置條件必須與超類中被覆寫的方法的前置條件相同或更寬松。
子類中的方法的后置條件必須與超類中被覆寫的方法的后置條件相同或更嚴(yán)格。
這樣的話,調(diào)用就沒有問題了。否則,我在父類中傳入一個(gè) List 類型的參數(shù),子類中重寫的方法參數(shù)卻變?yōu)?ArrayList ,那客戶端使用的時(shí)候傳入一個(gè) LinkedList 類型的參數(shù),使用父類的時(shí)候程序正常運(yùn)行,但根據(jù) LSP 原則,替換成子類后程序就會出現(xiàn)問題。同理,后置條件也是如此。
是不是很好理解?
但是這個(gè)原則并不是這么簡單的,語法上是肯定不能有任何問題。但是,同時(shí),功能上也要和替換前相同。
那么這就有些困難了,因?yàn)樽宇愔貙懜割愔械姆椒ㄌ旖?jīng)地義,子類中的方法不同于父類中的方法也是很正常的,但是,如果這么隨便重寫父類中的方法的話,那么肯定會違背 LSP 原則,可以看下面的例子:
首先有一個(gè)父類,水果類:
public class Fruit {
void introduce() {
System.out.println("我是水果父類...");
}
}
其次,它有一個(gè)子類,蘋果類:
public class Apple extends Fruit {
@Override
void introduce() {
System.out.println("我是水果子類——蘋果");
}
}
客戶端代碼如下:
public static void main(String[] args) {
Fruit fruit = new Fruit();
HashMap map = new HashMap<>();
fruit.introduce();
}
運(yùn)行結(jié)果:
我是水果父類...
那么,如果按照 LSP 原則,所有父類出現(xiàn)的地方都能換成其子類,代碼如下:
public static void main(String[] args) {
Apple fruit = new Apple();
HashMap map = new HashMap<>();
fruit.introduce();
}
那么運(yùn)行結(jié)果就會變成:
我是水果子類——蘋果
與原來的輸出不同,程序的功能被改變了,違背了 LSP 原則。
因此,可以看到, LSP 原則最重要的一點(diǎn)就是:避免子類重寫父類中已經(jīng)實(shí)現(xiàn)的方法。這就是 LSP 原則的本質(zhì)。這里由于 Fruit 父類已經(jīng)實(shí)現(xiàn)了 introduce 方法,因此子類應(yīng)該避免再對其進(jìn)行重寫,如果需要增加個(gè)性化,就應(yīng)該對父類進(jìn)行擴(kuò)展,而不是重寫,否則也會違背開閉原則。
一般來講,程序中父類大多是抽象類,因?yàn)楦割愔皇且粋€(gè)框架,具體功能還需要子類來實(shí)現(xiàn)。因此很少直接去 new 一個(gè)父類。而如果出現(xiàn)這種情況,那么就說明父類中實(shí)現(xiàn)的代碼已經(jīng)很好了,子類只需要對其進(jìn)行擴(kuò)展就會,盡量避免對其已經(jīng)實(shí)現(xiàn)的方法再去重寫。
依賴倒置原則(Dependence Inversion Principle)
依賴倒置原則的原始定義是這樣的:
High level modules should not depend upon low level modules.?
Both should depend upon abstractions.?
Abstractions should not depend upon details. Details should depend upon abstractions.
翻譯一下,就是下面三句話:
高層模塊不應(yīng)該依賴底層模塊,兩者都應(yīng)該依賴其抽象。
抽象不應(yīng)該依賴細(xì)節(jié)。
細(xì)節(jié)應(yīng)該依賴抽象。
在Java語言中的表現(xiàn)就是:
模塊間的依賴通過抽象發(fā)生,實(shí)現(xiàn)類之間不發(fā)生直接的依賴關(guān)系,其依賴關(guān)系是通過接口或抽象類產(chǎn)生的。
接口或抽象類不依賴于實(shí)現(xiàn)類。
實(shí)現(xiàn)類依賴于接口或抽象類。
簡而言之,我們要盡可能使用接口或抽象類。也就是“面向接口編程” 或者說 “面向抽象編程” ,也就是說程序中要盡可能使用抽象類或是接口。
可能現(xiàn)在還沒有使用抽象的習(xí)慣,可以看一個(gè)例子:比如我們要寫一個(gè)人吃蘋果的程序,首先創(chuàng)建一個(gè)蘋果類:
public class Apple {
public void eaten() {
System.out.println("正在吃蘋果...");
}
}
然后寫人的類:
public class Person {
void eat(Apple apple) {
apple.eaten();
}?
}
這樣,在客戶端中我們就可以這樣調(diào)用:
Person xiaoMing = new Person();
Apple apple = new Apple();
xiaoMing.eat(apple);
但是這樣就有一個(gè)問題,如果我不僅僅想吃蘋果,還想吃橘子怎么辦,在 Person 類中再加一個(gè)函數(shù)處理橘子?那么吃別的水果呢??總不能程序中每增加一種水果就要在其中增加一個(gè)函數(shù)吧。
這時(shí)候就需要接口了(抽象類也是可以的)
程序就可以這樣更改,增加一個(gè)水果接口:
public interface Fruit {
void eaten();?
}
讓蘋果類實(shí)現(xiàn)該接口:
public class Apple implements Fruit {
@Override
public void eaten() {
System.out.println("正在吃蘋果...");
}
}
然后將Person類中的函數(shù)的參數(shù)稍作修改:
public class Person {
void eat(Fruit fruit) {
fruit.eaten();
}
}
這樣,客戶端代碼也稍作修改:
Person xiaoMing = new Person();
Fruit apple = new Apple();
xiaoMing.eat(apple);
這樣改完之后,如果再增加一種新的水果,就不需要改變 Person 類了,方便吧。
那么再回來說我們的依賴倒置原則,依賴就是類與類之間的依賴,主要是函數(shù)傳參,上面的例子已經(jīng)很明白地介紹了,參數(shù)要盡可能使用抽象類或接口,這就是“高層模塊不應(yīng)該依賴底層模塊,兩者都應(yīng)該依賴其抽象” 的解釋。那么如果要實(shí)現(xiàn)這個(gè),就要求每個(gè)實(shí)現(xiàn)類都應(yīng)該盡可能從抽象中派生,這就是上面的 “細(xì)節(jié)應(yīng)該依賴抽象”。
簡而言之,該原則主要有下面幾點(diǎn)要求:
每個(gè)類都盡量要有接口或抽象類,或者兩者都有
變量的表面類型盡量是接口或者抽象類(比如程序中的 Fruit apple = new Apple() Fruit 是表面類型, Apple 是實(shí)際類型)
任何類都不應(yīng)該從具體類中派生
接口隔離原則(Interface Segregation Principle)
首先聲明,該原則中的接口,是一個(gè)泛泛而言的接口,不僅僅指Java中的接口,還包括其中的抽象類。
首先,給出該原則的定義,該原則有兩個(gè)定義:
Clients should not be forced to depend upon interfaces that they don`t use.
客戶端不應(yīng)該依賴它不需要的接口。
The dependency of one class to another one should depend on the smallest possible.
類間的依賴關(guān)系應(yīng)該建立在最小的接口上。
這是什么意思呢,這是讓我們把接口進(jìn)行細(xì)分。舉個(gè)例子,如果一個(gè)類實(shí)現(xiàn)一個(gè)接口,但這個(gè)接口中有它不需要的方法,那么就需要把這個(gè)接口拆分,把它需要的方法提取出來,組成一個(gè)新的接口讓這個(gè)類去實(shí)現(xiàn),這就是接口隔離原則。簡而言之,就是說,接口中的所有方法對其實(shí)現(xiàn)的子類都是有用的。否則,就將接口繼續(xù)細(xì)分。
看起來,該原則與單一職責(zé)原則很相像。確實(shí)很像,二者都是強(qiáng)調(diào)要將接口進(jìn)行細(xì)分,只不過分的方式不同。單一職責(zé)原則是按照 職責(zé) 進(jìn)行劃分接口的;而接口隔離原則則是按照實(shí)現(xiàn)類對方法的使用來劃分的??梢哉f,接口隔離原則更細(xì)一些。
要想完美地實(shí)現(xiàn)該原則,基本上就需要每個(gè)實(shí)現(xiàn)類都有一個(gè)專用的接口。但實(shí)際開發(fā)中,這樣顯然是不可能的,而且,這樣很容易違背單一職責(zé)原則(可能出現(xiàn)同一個(gè)職責(zé)分成了好幾個(gè)接口的情況),因此我們能做的就是盡量細(xì)分。
該原則主要強(qiáng)調(diào)兩點(diǎn):
接口盡量小。
就像前面說的那樣,接口中只有實(shí)現(xiàn)類中有用的方法。
接口要高內(nèi)聚
就是說,在接口內(nèi)部實(shí)現(xiàn)的方法,不管怎么改,都不會影響到接口外的其他接口或是實(shí)現(xiàn)類,只能影響它自己。
迪米特法則(Law of Demeter)
迪米特法則也叫最少知道原則(Least Knowledge Principle, LKP ),雖然名稱不同,但都是同一個(gè)意思:一個(gè)對象應(yīng)該對其他對象有最少的了解。
該原則也很好理解,我們在寫一個(gè)類的時(shí)候,應(yīng)該盡可能的少暴露自己的接口。什么意思呢?就是說,在寫類的時(shí)候,能不 public 就不 public ,所有暴露的屬性或是接口,都是不得不暴露的,這樣的話,就能保證其他類對這個(gè)類有最少的了解了。
這個(gè)原則也沒什么需要多講的,調(diào)用者只需要知道被調(diào)用者公開的方法就好了,至于它內(nèi)部是怎么實(shí)現(xiàn)的或是有其他別的方法,調(diào)用者并不關(guān)心,調(diào)用者只關(guān)心它需要用的。反而,如果被調(diào)用者暴露太多不需要暴露的屬性或方法,那么就可能導(dǎo)致調(diào)用者濫用其中的方法,或是引起一些其他不必要的麻煩。
開閉原則(Open Closed Principle)
開閉原則所有設(shè)計(jì)模式原則中,最基礎(chǔ)的那個(gè)原則。首先,還是先來看一下它的定義:
Software entities like classes, modules and functions should be open for extension but closed for modifications.
翻譯過來就是:一個(gè)軟件實(shí)體,如類、模塊和函數(shù)應(yīng)該對擴(kuò)展開放,對修改關(guān)閉。
開閉原則之前也提到過,在 LSP 中,我們說,要避免子類重寫父類中已經(jīng)實(shí)現(xiàn)的方法。這個(gè)時(shí)候,繼承父類就是對其進(jìn)行擴(kuò)展,但沒有進(jìn)行修改。這就是開閉原則一個(gè)很好的體現(xiàn)。
那是不是開閉原則與 LSP 原則混淆了呢?并不是, LSP 原則強(qiáng)調(diào)的是基類與子類的關(guān)系,只是其中的一種實(shí)現(xiàn)方式用到了開閉原則而已。
那么開閉原則具體是什么呢?可以說,開閉原則貫穿于以上五個(gè)設(shè)計(jì)模式原則。開閉原則中的對擴(kuò)展開放,就是說,如果在項(xiàng)目中添加一個(gè)功能的時(shí)候,可以直接對代碼進(jìn)行擴(kuò)展;如果要修改某一部分的功能時(shí),我們應(yīng)該做的是,盡量少做修改(完全不修改是不可能的),但是修改的時(shí)候,要保留原來的功能,只是在上面擴(kuò)展出新的功能,就像版本更新一樣,更新后,依然支持舊版本。
開閉原則是一個(gè)特別重要的原則,無論是在設(shè)計(jì)模式中還是在其他領(lǐng)域中,這都是一個(gè)非?;A(chǔ)的設(shè)計(jì)理念。
總結(jié)
總而言之,這六個(gè)設(shè)計(jì)模式原則是以后學(xué)習(xí)設(shè)計(jì)模式的基礎(chǔ),它們的共同目的就是 SOLID ——建立穩(wěn)定、靈活、健壯的設(shè)計(jì)。
但是原則歸原則,有時(shí)候由于種種原因,這些條條框框是不得不去打破的,一味地遵循它是不會有好果子吃的(就像接口隔離原則,不可能創(chuàng)建那么多的接口)。因此我們應(yīng)該正確使用這些原則,主要目的還是為了我們軟件的穩(wěn)定性、靈活性、健壯性和可維護(hù)性。
注:本文所有原則定義皆出自《設(shè)計(jì)模式之禪》。
總結(jié)
以上是生活随笔為你收集整理的【转】设计模式六大原则——SOLID的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 刷中信信用卡有积分吗?中信信用卡积分问题
- 下一篇: 第一节: Timer的定时任务的复习、Q