什么是設(shè)計模式
在GoF(Gang of Four)的書籍《Design Patterns - Elements of Reusable Object-Oriented Software(設(shè)計模式-可復(fù)用面向?qū)ο筌浖幕A(chǔ))》中是這樣定義設(shè)計模式的:Christopher Alexander說過:“每一個模式描述了一個在我們周圍不斷重復(fù)發(fā)生的問題以及該問題的解決方案的核心。這樣,你就能一次又一次地使用該方案而不必做重復(fù)勞動” [AIS+77,第10頁]。盡管Alexander所指的是城市和建筑模式,但他的思想也同樣適用于于面向?qū)ο笤O(shè)計模式,只是在面向?qū)ο蟮慕鉀Q方案里, 我們喲偶那個對象和接口代替了墻壁和門窗。兩類模式的核心都在于提供了相關(guān)問題的解決方案。一般而言,設(shè)計模式有四個基本要素:
- 1、模式名稱(pattern name):一個助記名,它用一兩個詞來描述模式的問題、解決方案和效果。
- 2、問題(problem):描述了應(yīng)該在何時使用模式。
- 3、解決方案(solution):描述了設(shè)計的組成成分,它們之間的相關(guān)關(guān)系以及各自的職責(zé)和協(xié)作方案。
- 4、效果(consequences):描述了模式應(yīng)用的效果以及使用模式應(yīng)該權(quán)衡的問題。
設(shè)計模式的創(chuàng)始人很明確地指出了設(shè)計模式的基本要素,但是由于現(xiàn)實中浮躁、偏向過度設(shè)計等因素的干擾,開發(fā)者很多時候會重點關(guān)注第1和第3點要素(過度關(guān)注設(shè)計模式和設(shè)計模式的實現(xiàn)),忽略第2和第4點要素(忽視使用設(shè)計模式的場景和目標(biāo)),導(dǎo)致設(shè)計出來的編碼邏輯可能過于復(fù)雜或者達(dá)不到預(yù)期的效果。
總的來說,設(shè)計模式(Design Pattern)是一套被反復(fù)使用、多數(shù)人知曉的、經(jīng)過分類編目的、代碼設(shè)計經(jīng)驗的總結(jié)。也就是本來并不存在所謂設(shè)計模式,用的人多了,也便成了設(shè)計模式。
設(shè)計模式的七大原則
面向?qū)ο蟮脑O(shè)計模式有七大基本原則:
- 開閉原則(Open Closed Principle,OCP)
- 單一職責(zé)原則(Single Responsibility Principle, SRP)
- 里氏代換原則(Liskov Substitution Principle,LSP)
- 依賴倒轉(zhuǎn)原則(Dependency Inversion Principle,DIP)
- 接口隔離原則(Interface Segregation Principle,ISP)
- 合成/聚合復(fù)用原則(Composite/Aggregate Reuse Principle,CARP)
- 最少知識原則(Least Knowledge Principle,LKP)或者迪米特法則(Law of Demeter,LOD)
| 設(shè)計模式原則名稱 | 簡單定義 |
|---|---|
| 開閉原則 | 對擴展開放,對修改關(guān)閉 |
| 單一職責(zé)原則 | 一個類只負(fù)責(zé)一個功能領(lǐng)域中的相應(yīng)職責(zé) |
| 里氏代換原則 | 所有引用基類的地方必須能透明地使用其子類的對象 |
| 依賴倒轉(zhuǎn)原則 | 依賴于抽象,不能依賴于具體實現(xiàn) |
| 接口隔離原則 | 類之間的依賴關(guān)系應(yīng)該建立在最小的接口上 |
| 合成/聚合復(fù)用原則 | 盡量使用合成/聚合,而不是通過繼承達(dá)到復(fù)用的目的 |
| 迪米特法則 | 一個軟件實體應(yīng)當(dāng)盡可能少的與其他實體發(fā)生相互作用 |
這個表格看起來有點抽象,下面逐條分析。
開閉原則
開閉原則(Open Closed Principle,OCP)的定義是:一個軟件實體如類、模塊和函數(shù)應(yīng)該對擴展開放,對修改關(guān)閉。模塊應(yīng)盡量在不修改原(是"原",指原來的代碼)代碼的情況下進(jìn)行擴展。
開閉原則的意義:
在軟件的生命周期內(nèi),因為變化、升級和維護(hù)等原因需要對軟件原有代碼進(jìn)行修改時,可能會給舊代碼中引入錯誤,也可能會使我們不得不對整個功能進(jìn)行重構(gòu),并且需要原有代碼經(jīng)過重新測試。當(dāng)軟件需要變化時,盡量通過擴展軟件實體的行為來實現(xiàn)變化,而不是通過修改已有的代碼來實現(xiàn)變化。
如何實現(xiàn)對擴展開放,對修改關(guān)閉?
要實現(xiàn)對擴展開放,對修改關(guān)閉,即遵循開閉原則,需要對系統(tǒng)進(jìn)行抽象化設(shè)計,抽象可以基于抽象類或者接口。一般來說需要做到幾點:
- 1、通過接口或者抽象類約束擴展,對擴展進(jìn)行邊界限定,不允許出現(xiàn)在接口或抽象類中不存在的public方法,也就是擴展必須添加具體實現(xiàn)而不是改變具體的方法。
- 2、參數(shù)類型、引用對象盡量使用接口或者抽象類,而不是實現(xiàn)類,這樣就能盡量保證抽象層是穩(wěn)定的。
- 3、一般抽象模塊設(shè)計完成(例如接口的方法已經(jīng)敲定),不允許修改接口或者抽象方法的定義。
下面通過一個例子遵循開閉原則進(jìn)行設(shè)計,場景是這樣:某系統(tǒng)的后臺需要監(jiān)測業(yè)務(wù)數(shù)據(jù)展示圖表,如柱狀圖、折線圖等,在未來需要支持圖表的著色操作。在開始設(shè)計的時候,代碼可能是這樣的:
public class BarChart {
public void draw(){
System.out.println("Draw bar chart...");
}
}
public class LineChart {
public void draw(){
System.out.println("Draw line chart...");
}
}
public class App {
public void drawChart(String type){
if (type.equalsIgnoreCase("line")){
new LineChart().draw();
}else if (type.equalsIgnoreCase("bar")){
new BarChart().draw();
}
}
}
這樣做在初期是能滿足業(yè)務(wù)需要的,開發(fā)效率也十分高,但是當(dāng)后面需要新增一個餅狀圖的時候,既要添加一個餅狀圖的類,原來的客戶端App類的drawChart方法也要新增一個if分支,這樣做就是修改了原有客戶端類庫的方法,是十分不合理的。如果這個時候,在圖中加入一個顏色屬性,復(fù)雜性也大大提高?;诖?,需要引入一個抽象Chart類AbstractChart,App類在畫圖的時候總是把相關(guān)的操作委托到具體的AbstractChart的派生類實例,這樣的話App類的代碼就不用修改:
public abstract class AbstractChart {
public abstract void draw();
}
public class BarChart extends AbstractChart{
@Override
public void draw() {
System.out.println("Draw bar chart...");
}
}
public class LineChart extends AbstractChart {
@Override
public void draw() {
System.out.println("Draw line chart...");
}
}
public class App {
public void drawChart(AbstractChart chart){
chart.draw();
}
}
如果新加一種圖,只需要新增一個AbstractChart的子類即可??蛻舳祟怉pp不需要改變原來的邏輯。修改后的設(shè)計符合開閉原則,因為整個系統(tǒng)在擴展時原有的代碼沒有做任何修改。
單一職責(zé)原則
單一職責(zé)原則(Single Responsibility Principle, SRP)的定義是:指一個類或者模塊應(yīng)該有且只有一個改變的原因。如果一個類承擔(dān)的職責(zé)過多,就等于把這些職責(zé)耦合在一起了。一個職責(zé)的變化可能會削弱或者抑制這個類完成其他職責(zé)的能力。這種耦合會導(dǎo)致脆弱的設(shè)計,當(dāng)發(fā)生變化時,設(shè)計會遭受到意想不到的破壞。而如果想要避免這種現(xiàn)象的發(fā)生,就要盡可能的遵守單一職責(zé)原則。此原則的核心就是解耦和增強內(nèi)聚性。
單一職責(zé)原則的意義:
單一職責(zé)原則告訴我們:一個類不能做太多的東西。在軟件系統(tǒng)中,一個類(一個模塊、或者一個方法)承擔(dān)的職責(zé)越多,那么其被復(fù)用的可能性就會越低。一個很典型的例子就是萬能類。其實可以說一句大實話:任何一個常規(guī)的MVC項目,在極端的情況下,可以用一個類(甚至一個方法)完成所有的功能。但是這樣做就會嚴(yán)重耦合,甚至牽一發(fā)動全身。一個類承(一個模塊、或者一個方法)擔(dān)的職責(zé)過多,就相當(dāng)于將這些職責(zé)耦合在一起,當(dāng)其中一個職責(zé)變化時,可能會影響其他職責(zé)的運作,因此要將這些職責(zé)進(jìn)行分離,將不同的職責(zé)封裝在不同的類中,即將不同的變化原因封裝在不同的類中,如果多個職責(zé)總是同時發(fā)生改變則可將它們封裝在同一類中。
不過說實話,其實有的時候很難去衡量一個類的職責(zé),主要是很難確定職責(zé)的粒度。這一點不僅僅體現(xiàn)在一個類或者一個模塊中,也體現(xiàn)在采用微服務(wù)的分布式系統(tǒng)中。這也就是為什么我們在實施微服務(wù)拆分的時候經(jīng)常會撕逼:"這個功能不應(yīng)該發(fā)在A服務(wù)中,它不做這個領(lǐng)域的東西,應(yīng)該放在B服務(wù)中"諸如此類的爭論。存在爭論是合理的,不過最好不要不了了之,而應(yīng)該按照領(lǐng)域定義好每個服務(wù)的職責(zé)(職責(zé)的粒度最好找業(yè)務(wù)和架構(gòu)專家咨詢),得出相對合理的職責(zé)分配。
下面通過一個很簡單的實例說明一下單一職責(zé)原則:
在一個項目系統(tǒng)代碼編寫的時候,由于歷史原因和人為的不規(guī)范,導(dǎo)致項目沒有分層,一個Service類的偽代碼是這樣的:
public class Service {
public UserDTO findUser(String name){
Connection connection = getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM t_user WHERE name = ?");
preparedStatement.setObject(1, name);
User user = //處理結(jié)果
UserDTO dto = new UserDTO();
//entity值拷貝到dto
return dto;
}
}
這里出現(xiàn)一個問題,Service做了太多東西,包括數(shù)據(jù)庫連接的管理,Sql的執(zhí)行這些業(yè)務(wù)層不應(yīng)該接觸到的邏輯,更可怕的是,例如到時候如果數(shù)據(jù)庫換成了Oracle,這個方法將會大改。因此,拆分出新的DataBaseUtils類用于專門管理數(shù)據(jù)庫資源,Dao類用于專門執(zhí)行查詢和查詢結(jié)果封裝,改造后Service類的偽代碼如下:
public class Service {
private Dao dao;
public UserDTO findUser(String name){
User user = dao.findUserByName(name);
UserDTO dto = new UserDTO();
//entity值拷貝到dto
return dto;
}
}
public class Dao{
public User findUserByName(String name){
Connection connection = DataBaseUtils.getConnnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM t_user WHERE name = ?");
preparedStatement.setObject(1, name);
User user = //處理結(jié)果
return user;
}
}
現(xiàn)在,如果有查詢封裝的變動只需要修改Dao類,數(shù)據(jù)庫相關(guān)變動只需要修改DataBaseUtils類,每個類的職責(zé)分明。這個時候,如果我們要把底層的存儲結(jié)構(gòu)緩成Redis或者M(jìn)ongoDB怎么辦,這樣顯然要重建整個Dao類,這種情況下,需要進(jìn)行接口隔離,下面分析接口隔離原則的時候再詳細(xì)分析。
里氏代換原則
里氏代換原則(Liskov Substitution Principle,LSP)的定義是:所有引用基類的地方必須能透明地使用其子類的對象,也可以簡單理解為任何基類可以出現(xiàn)的地方,子類一定可以出現(xiàn)。
里氏代換原則的意義:
只有當(dāng)衍生類可以替換掉基類,軟件單位的功能不受到影響時,基類才能真正被復(fù)用,而衍生類也能夠在基類的基礎(chǔ)上增加新的行為。里氏代換原則是對"開-閉"原則的補充。實現(xiàn)"開-閉"原則的關(guān)鍵步驟就是抽象化。而基類與子類的繼承關(guān)系就是抽象化的具體實現(xiàn),所以里氏代換原則是對實現(xiàn)抽象化的具體步驟的規(guī)范。當(dāng)然,如果反過來,軟件單位使用的是一個子類對象的話,那么它不一定能夠使用基類對象。舉個很簡單的例子說明這個問題:如果一個方法接收Map類型參數(shù),那么它一定可以接收Map的子類參數(shù)例如HashMap、LinkedHashMap、ConcurrentHashMap類型的參數(shù);但是返過來,如果另一個方法只接收HashMap類型的參數(shù),那么它一定不能接收所有Map類型的參數(shù),否則它可以接收LinkedHashMap、ConcurrentHashMap類型的參數(shù)。
子類為什么可以替換基類的位置?
其實原因很簡單,只要存在繼承關(guān)系,基類的所有非私有屬性或者方法,子類都可以通過繼承獲得(白箱復(fù)用),反過來不成立,因為子類很有可能擴充自身的非私有屬性或者方法,這個時候不能用基類獲取子類新增的這些屬性或者方法。
里氏代換原則是實現(xiàn)開閉原則的基礎(chǔ),它告訴我們在設(shè)計程序的時候進(jìn)可能使用基類進(jìn)行對象的定義和引用,在運行時再決定基類的具體子類型。
舉個簡單的例子,假設(shè)一種會呼吸的動物作為父類,子類豬和鳥也有自身的呼吸方式:
public abstract class Animal {
protected abstract void breathe();
}
public class Bird extends Animal {
@Override
public void breathe() {
System.out.println("Bird breathes...");
}
}
public class Pig extends Animal {
@Override
public void breathe() {
System.out.println("Pig breathes...");
}
}
public class App {
public static void main(String[] args) throws Exception {
Animal bird = new Bird();
bird.breathe();
Animal pig = new Pig();
pig.breathe();
}
}
依賴倒轉(zhuǎn)原則
依賴倒轉(zhuǎn)原則(Dependency Inversion Principle,DIP)的定義:程序要依賴于抽象接口,不要依賴于具體實現(xiàn)。簡單的說就是要求對抽象進(jìn)行編程,不要對實現(xiàn)進(jìn)行編程,這樣就降低了客戶與實現(xiàn)模塊間的耦合。
依賴倒轉(zhuǎn)原則的意義:
依賴倒轉(zhuǎn)原則要求我們在程序代碼中傳遞參數(shù)時或在關(guān)聯(lián)關(guān)系中,盡量引用層次高的抽象層類,即使用接口和抽象類進(jìn)行變量類型聲明、參數(shù)類型聲明、方法返回類型聲明,以及數(shù)據(jù)類型的轉(zhuǎn)換等,而不要用具體類來做這些事情。為了確保該原則的應(yīng)用,一個具體類應(yīng)當(dāng)只實現(xiàn)接口或抽象類中聲明過的方法,而不要給出多余的方法,否則將無法調(diào)用到在子類中增加的新方法。在引入抽象層后,系統(tǒng)將具有很好的靈活性,在程序中盡量使用抽象層進(jìn)行編程,而將具體類寫在配置文件中,這樣一來,如果系統(tǒng)行為發(fā)生變化,只需要對抽象層進(jìn)行擴展,并修改配置文件,而無須修改原有系統(tǒng)的源代碼,在不修改的情況下來擴展系統(tǒng)的功能,滿足開閉原則的要求。
依賴倒轉(zhuǎn)原則的注意事項:
- 高層模塊不應(yīng)該依賴低層模塊,高層模塊和低層模塊都應(yīng)該依賴于抽象。
- 抽象不應(yīng)該依賴于具體,具體應(yīng)該依賴于抽象。
在實現(xiàn)依賴倒轉(zhuǎn)原則時,我們需要針對抽象層編程,而將具體類的對象通過依賴注入(DependencyInjection, DI)的方式注入到其他對象中,依賴注入是指當(dāng)一個對象要與其他對象發(fā)生依賴關(guān)系時,通過抽象來注入所依賴的對象。常用的注入方式有三種,分別是:構(gòu)造注入,設(shè)值注入(Setter注入)和接口注入。Spring的IOC是此實現(xiàn)的典范。
從Java角度看待依賴倒轉(zhuǎn)原則的本質(zhì)就是:面向接口(抽象)編程。
- 每個具體的類都應(yīng)該有其接口或者基類,或者兩者都具備。
- 類中的引用對象應(yīng)該是接口或者基類。
- 任何具體類都不應(yīng)該派生出子類。
- 盡量不要覆寫基類中的方法。
- 結(jié)合里氏代換原則使用。
遵循依賴倒轉(zhuǎn)原則的一個例子,場景是司機開車:
public interface Driver {
void drive();
void setCar(Car car);
}
public interface Car {
void run();
}
public class DefaultDriver implements Driver {
private Car car;
@Override
public void drive() {
car.run();
}
@Override
public void setCar(Car car) {
this.car = car;
}
}
public class Bmw implements Car {
@Override
public void run() {
System.out.println("Bmw runs...");
}
}
public class Benz implements Car {
@Override
public void run() {
System.out.println("Benz runs...");
}
}
public class App {
public static void main(String[] args) throws Exception {
Driver driver = new DefaultDriver();
Car car = new Benz();
driver.setCar(car);
driver.drive();
car = new Bmw();
driver.setCar(car);
driver.drive();
}
}
這樣實現(xiàn)了一個司機可以開各種類型的車,如果還有其他類型的車,只需要新加一個Car的實現(xiàn)即可。
接口隔離原則
接口隔離原則(Interface Segregation Principle,ISP)的定義是客戶端不應(yīng)該依賴它不需要的接口,類間的依賴關(guān)系應(yīng)該建立在最小的接口上。簡單來說就是建立單一的接口,不要建立臃腫龐大的接口。也就是接口盡量細(xì)化,同時接口中的方法盡量少。
如何看待接口隔離原則和單一職責(zé)原則?
單一職責(zé)原則注重的是類和接口的職責(zé)單一,這里職責(zé)是從業(yè)務(wù)邏輯上劃分的,但是在接口隔離原則要求當(dāng)一個接口太大時,我們需要將它分割成一些更細(xì)小的接口,使用該接口的客戶端僅需知道與之相關(guān)的方法即可。也就是說,我們在設(shè)計接口的時候有可能滿足單一職責(zé)原則但是不滿足接口隔離原則。
接口隔離原則的規(guī)范:
- 使用接口隔離原則前首先需要滿足單一職責(zé)原則。
- 接口需要高內(nèi)聚,也就是提高接口、類、模塊的處理能力,少對外發(fā)布public的方法。
- 定制服務(wù),就是單獨為一個個體提供優(yōu)良的服務(wù),簡單來說就是拆分接口,對特定接口進(jìn)行定制。
- 接口設(shè)計是有限度的,接口的設(shè)計粒度越小,系統(tǒng)越靈活,但是值得注意不能過小,否則變成"字節(jié)碼編程"。
如果有用過spring-data-redis的人就知道,RedisTemplate中持有一些列的基類,分別是ValueOperations(處理K-V)、ListOperations(處理Hash)、SetOperations(處理集合)等等。
public interface ValueOperations<K, V> {
void set(K key, V value);
void set(K key, V value, long timeout, TimeUnit unit);
//....
}
合成/聚合復(fù)用原則
合成/聚合復(fù)用原則(Composite/Aggregate Reuse Principle,CARP)一般也叫合成復(fù)用原則(Composite Reuse Principle, CRP),定義是:盡量使用合成/聚合,而不是通過繼承達(dá)到復(fù)用的目的。
合成/聚合復(fù)用原則就是在一個新的對象里面使用一些已有的對象,使之成為新對象的一部分;新的對象通過向內(nèi)部持有的這些對象的委派達(dá)到復(fù)用已有功能的目的,而不是通過繼承來獲得已有的功能。
聚合(Aggregate)的概念:
聚合表示一種弱的"擁有"關(guān)系,一般表現(xiàn)為松散的整體和部分的關(guān)系,其實,所謂整體和部分也可以是完全不相關(guān)的。例如A對象持有B對象,B對象并不是A對象的一部分,也就是B對象的生命周期是B對象自身管理,和A對象不相關(guān)。
合成(Composite)的概念:
合成表示一種強的"擁有"關(guān)系,一般表現(xiàn)為嚴(yán)格的整體和部分的關(guān)系,部分和整體的生命周期是一樣的。
聚合和合成的關(guān)系:
這里用山羊舉例說明聚合和合成的關(guān)系:

為什么要用合成/聚合來替代繼承達(dá)到復(fù)用的目的?
繼承復(fù)用破壞包裝,因為繼承將基類的實現(xiàn)細(xì)節(jié)暴露給派生類,基類的內(nèi)部細(xì)節(jié)通常對子類來說是可見的,這種復(fù)用也稱為"白箱復(fù)用"。這里有一個明顯的問題是:派生類繼承自基類,如果基類的實現(xiàn)發(fā)生改變,將會影響到所有派生類的實現(xiàn);如果從基類繼承而來的實現(xiàn)是靜態(tài)的,不可能在運行時發(fā)生改變,不夠靈活。
由于合成或聚合關(guān)系可以將已有的對象,一般叫成員對象,納入到新對象中,使之成為新對象的一部分,因此新對象可以調(diào)用已有對象的功能,這樣做可以使得成員對象的內(nèi)部實現(xiàn)細(xì)節(jié)對于新對象不可見,所以這種復(fù)用又稱為"黑箱"復(fù)用,相對繼承關(guān)系而言,其耦合度相對較低,成員對象的變化對新對象的影響不大,可以在新對象中根據(jù)實際需要有選擇性地調(diào)用成員對象的操作;合成/聚合復(fù)用可以在運行時動態(tài)進(jìn)行,新對象可以動態(tài)地引用與成員對象類型相同的其他對象。
如果有閱讀過《Effective Java 2nd》的同學(xué)就知道,此書也建議慎用繼承。一般情況下,只有明確知道派生類和基類滿IS A的時候才選用繼承,當(dāng)滿足HAS A或者不能判斷的情況下應(yīng)該選用合成/聚合。
下面舉個很極端的例子說明一下如果在非IS A的情況下使用繼承會出現(xiàn)什么問題:
先定義一個抽象手,手有一個搖擺的方法,然后定義左右手繼承抽象手,實現(xiàn)搖擺方法:
public abstract class AbstractHand {
protected abstract void swing();
}
public class LeftHand extends AbstractHand {
@Override
public void swing() {
System.out.println("Left hand swings...");
}
}
public class RightHand extends AbstractHand {
@Override
public void swing() {
System.out.println("Right hand swings...");
}
}
現(xiàn)在看起來沒有任何問題,實現(xiàn)也十分正確,現(xiàn)在出現(xiàn)了人(Person)這個類,具備搖左右手的功能,如果不考慮IS A的關(guān)系,很有可能有人會這樣做:
public abstract class AbstractSwingHand extends AbstractHand{
@Override
protected void swing() {
System.out.println(" hand swings...");
}
}
public class Person extends AbstractSwingHand {
public void swingLeftHand(){
System.out.print("Left ");
super.swing();
}
public void swingRightHand(){
System.out.print("Right ");
super.swing();
}
}
上面Person的實現(xiàn)讓人覺得百思不得其解,但是往往這會出現(xiàn)在真實的環(huán)境中,因為Hand不是Person,所以Person繼承Hand一定會出現(xiàn)曲線實現(xiàn)等奇葩邏輯。Hand和Person是嚴(yán)格的部分和整體的關(guān)系,或者說Person和Hand是HAS A的關(guān)系,如果使用合成,邏輯將會十分清晰:
public class Person {
private AbstractHand leftHand;
private AbstractHand rightHand;
public Person() {
leftHand = new LeftHand();
rightHand = new RightHand();
}
public void swingLeftHand(){
leftHand.swing();
}
public void swingRightHand(){
rightHand.swing();
}
}
這里使用了合成,說明了Person和AbstractHand實例的生命周期是一致的。
迪米特法則
迪米特法則(Law of Demeter,LOD),有時候也叫做最少知識原則(Least Knowledge Principle,LKP),它的定義是:一個軟件實體應(yīng)當(dāng)盡可能少地與其他實體發(fā)生相互作用。每一個軟件單位對其他的單位都只有最少的知識,而且局限于那些與本單位密切相關(guān)的軟件單位。迪米特法則的初衷在于降低類之間的耦合。由于每個類盡量減少對其他類的依賴,因此,很容易使得系統(tǒng)的功能模塊功能獨立,相互之間不存在(或很少有)依賴關(guān)系。迪米特法則不希望類之間建立直接的聯(lián)系。如果真的有需要建立聯(lián)系,也希望能通過它的友元類(中間類或者跳轉(zhuǎn)類)來轉(zhuǎn)達(dá)。
迪米特法則的規(guī)則:
- Only talk to your immediate friends(只與直接的朋友通訊),一個對象的"朋友"包括他本身(this)、它持有的成員對象、入?yún)ο蟆⑺鶆?chuàng)建的對象。
- 盡量少發(fā)布public的變量和方法,一旦公開的屬性和方法越多,修改的時候影響的范圍越大。
- "是自己的就是自己的",如果一個方法放在本類中,既不產(chǎn)生新的類間依賴,也不造成負(fù)面的影響,那么次方法就應(yīng)該放在本類中。
迪米特法則的意義:
迪米特法則的核心觀念就是類間解耦,也就降低類之間的耦合,只有類處于弱耦合狀態(tài),類的復(fù)用率才會提高。所謂降低類間耦合,實際上就是盡量減少對象之間的交互,如果兩個對象之間不必彼此直接通信,那么這兩個對象就不應(yīng)當(dāng)發(fā)生任何直接的相互作用,如果其中的一個對象需要調(diào)用另一個對象的某一個方法的話,可以通過第三者轉(zhuǎn)發(fā)這個調(diào)用。簡言之,就是通過引入一個合理的第三者來降低現(xiàn)有對象之間的耦合度。但是這樣會引發(fā)一個問題,有可能產(chǎn)生大量的中間類或者跳轉(zhuǎn)類,導(dǎo)致系統(tǒng)的復(fù)雜性提高,可維護(hù)性降低。如果一味追求極度解耦,那么最終有可能變成面向字節(jié)碼編程甚至是面向二進(jìn)制的0和1編程。
舉個很簡單的例子,體育老師要知道班里面女生的人數(shù),他委托體育課代表點清女生的人數(shù):
public class Girl {
}
public class GroupLeader {
private final List<Girl> girls;
public GroupLeader(List<Girl> girls) {
this.girls = girls;
}
public void countGirls() {
System.out.println("The sum of girls is " + girls.size());
}
}
public class Teacher {
public void command(GroupLeader leader){
leader.countGirls();
}
}
public class App {
public static void main(String[] args) throws Exception {
Teacher teacher = new Teacher();
GroupLeader groupLeader = new GroupLeader(Arrays.asList(new Girl(), new Girl()));
teacher.command(groupLeader);
}
}
這個例子中,體育課代表就是中間類,體育課代表對于體育老師來說就是"直接的朋友",如果去掉體育課代表這個中間類,體育老師必須親自清點女生的人數(shù)(實際上就數(shù)人數(shù)這個功能,體育老師是不必要獲取所有女生的對象列表),這樣做會違反迪米特法則。
小結(jié)
說實話,設(shè)計模式的七大原則理解是比較困難的,我們在設(shè)計模式的學(xué)習(xí)和應(yīng)用中經(jīng)常會聽到或者看到"XXX模式符合XXX原則"、"YYY模式不符合YYY原則"這樣的語句。因此,為了分析設(shè)計模式的合理性和完善我們?nèi)粘5木幋a,掌握和理解這七大原則是十分必要的。
參考
- 《Java設(shè)計模式》
- 《設(shè)計模式之禪-2nd》
- 《設(shè)計模式-可復(fù)用面向?qū)ο筌浖幕A(chǔ)》
(本文完)