1.設計模式經典面試題
????????分析幾個常見的設計模式對應的面試題。
1.1原型設計模式
????????1.使用UML類圖畫出原型模式核心角色(意思就是使用會考察使用UML畫出設計模式中關鍵角色和關系圖等)
????????2.原型設計模式的深拷貝和淺拷貝是什么,寫出深拷貝的兩種方式的源碼(重寫clone方法實現深拷貝,使用序列化實現深拷貝)=> 考察對原型模式細節的理解。
????????3.Spring框架中哪里需要進行使用原型模式 => 分析框架中設計模式的體現。
????????4.分析代碼+Debug => Debug水平如何?會分析設計模式代碼嘛?
1.2解釋器設計模式
1.2.1解釋器設計模式本身的理解
????????1.介紹解釋器模式是什么?=> 其實就是看對這個設計模式的理解如何。
????????2.畫出解釋器設計模式的UML類圖,分析其中的各個角色是什么?=> 其實就是在考察會不會進行畫原型圖,會不會通過原型圖去分析其中的整體結構?
1.2.2解釋器設計模式在Spring框架的理解
????????1.請說明Spring的框架中,哪里使用到了解釋器模式,進行源碼級分析 => 其實就是進行考察對框架中一些關鍵功能的實現,其中的設計模式理解的如何。
????????2.對于解釋器模式在Spring框架中的應用有哪些?
Spring框架中SpelExpressionParser使用的就是解釋器模式。
代碼分析+Debug源碼+說明。
1.3單例設計模式
????????單例設計模式一共有多少種實現方式?請分別使用代碼進行實現,并分析其中的優缺點。
????????1.餓漢式 => 兩種。
????????2.懶漢式 => 三種。
????????3.雙重檢查。
????????4.靜態內部類。
????????5.枚舉。
????????所以說單例設計模式一共八種。
1.4場景題
1.4.1問題再現
????????拼多多搶單項目:拼多多搶單的訂單,有審核-發布-搶單等步驟,隨著操作的不同,會改變訂單的狀態,項目中這個搶單的模塊就是使用狀態模式進行設計的,請你使用狀態模式進行設計一下。
????????其實其中就是給定一個場景,并提示/不提示給你使用什么設計模式進行實現,在有限的時間內思考出問題的解決方案。
1.4.2問題解決
????????如果使用if-else進行判斷各種狀態,會顯得整體代碼十分臃腫,不便于維護,如果在其中發現哪個狀態沒有進行處理之類的,就會出現問題,所以可以進行使用狀態設計模式來進行解決。
2.設計模式中的七大原則
????????設計模式在進行設計的時候,均需要進行遵循七大OOP原則來進行設計。
????????1.單一職責原則。
????????2.接口隔離原則。
????????3.依賴倒轉原則。
????????4.里氏替換原則。
????????5.開閉原則OCP。
????????6.迪米特法則。
????????7.合成復用原則。
3.設計模式的重要性
3.1軟件工程中的解決方案
????????設計模式是一種解決方案,是對軟件設計中普遍存在的問題進行提出的一種解決方案,是前人的經驗的智慧,可以進行解決軟件工程中的一些特定問題。
3.2使用設計模式架構的好處
????????設計模式可以為軟件工程帶來良好的架構體系,不注意軟件架構設計的項目,草草收工的項目就想一個簡易的房子,沒有進行良好的設計,就肯定不會建立成為一個高樓大廈。
????????借助設計模式對整個項目進行良好的架構設計,才可以建立起堅固的地基,最終可以依據堅固的地基,架構起高樓大廈。
3.3設計模式便于擴展新功能
????????一個項目需要進行擴展新功能的時候,如果沒有良好的軟件架構設計,那么將是十分艱難的,很有可能添加了這個功能,別的地方就會出現問題了,需要到處改正。
3.4設計模式可以提供可維護性
????????如果進行胡亂的設計堆砌代碼,不注重設計整個項目,只會導致整個項目可讀性越來越差,可維護性越來越差,最終導致這個項目變成一個破爛玩意,爛尾樓。
????????使用設計模式進行良好架構之后,項目的整體可讀性,可維護性可以提高。
3.5設計模式解決面試問題
????????兩個人在學歷條件等同或者上下浮動一級的情況下,你設計模式玩的六六的,他設計模式一竅不通,當然是你可以取得offer,抱的Offer歸。
3.6設計模式在軟件中的引用
????????1.進行面向對象(OOP)設計時可以進行使用。
????????2.進行設計功能模塊時 => 可以使用設計模式+算法+數據結構的集合。
????????3.框架(使用到多種設計模式)
????????4.架構(架構整個軟件設計,使得軟件設計變得更為合理)
3.7設計模式的目的
????????1.代碼可用性(相同的代碼一次編寫即可,無需進行多次編寫)
????????2.可讀性(提高代碼的可讀性)
????????3.可擴展性(增加新功能時會非常方便,不會導致增加新功能的時候對其它功能有很大的影響)
????????4.可靠性(設計模式使得整個軟件工程的可靠性提高)
????????5.使得整個項目達到高內聚,低耦合的特性
4.詳解設計模式七大原則
4.1單一職責原則
4.1.1什么是單一職責原則
????????對于類來說一個類僅僅負責一項職責,如果一個類進行負責兩個不同的職責,當因為職責一需要進行改動增強的時候,可能會導致職責二出現問題,所以要對A類進行按職責細分為兩個類,使得兩個類負責自己的職責,達到類單一職責的目的。
4.1.2單一職責的案例
4.1.2.1不遵循單一職責
????????如果一個類的不遵循單一職責,一個方法進行設計為可以進行處理多個職責,那么就會出現問題。
????????例如下面這個SingleResposibility類中進行定義了一個action開始出發的方法,可以進行處理任意類型的交通工具進行啟動執行,但是你會發現這樣進行使用會出現問題。
/*** 單一職責設計模式*/
public class SingleResponsibility {public static void action(Vehicle vehicle) {System.out.println(vehicle.getName() + "在路上行走");}public static void main(String[] args) {Car car = new Car("汽車");action(car);Plant plant = new Plant("飛機");action(plant);}}abstract class Vehicle {protected String name;public abstract String getName();
}class Car extends Vehicle {public Car(String name) {this.name = name;}@Overridepublic String getName() {return name;}}class Plant extends Vehicle {public Plant(String name) {this.name = name;}@Overridepublic String getName() {return name;}
}
????????看一下運行結果:
????????會發現這個類并沒有把多職責問題給處理好,多職責會出現一定的問題。
????????并且如果我們再去修改處理多職責的代碼,對于另一個職責的處理也會出現問題。
public static void action(Vehicle vehicle) {System.out.println(vehicle.getName() + "在天上飛行");
}
????????發現確實出現了一定的問題:
????????當我們進行修改想要兼容另一個職責的時候,發現對原來的職責發生了影響,所以這就是當一個類進行處理多職責的時候會出現一定的問題。
4.1.2.2修復遵循單一職責
????????將類中的方法進行職責拆分,讓每個方法進行具有單一職責的功能,這樣就解決了剛剛的問題。
????????對于每個類,如果一個類中的方法少,就可以將方法拆分為單一職責的方法;如果一個類中方法比較多,就將這個類拆分為多個遵循單一職責的類,讓每個類都具有單一職責,這樣就可以解決一定的問題。
/*** 單一職責設計模式*/
public class SingleResponsibility {public static void carAction(Car car) {System.out.println(car.getName() + "在路上行駛");}public static void plantAction(Plant plant) {System.out.println(plant.getName() + "在天上飛行");}public static void main(String[] args) {Car car = new Car("汽車");carAction(car);Plant plant = new Plant("飛機");plantAction(plant);}}
????????可以發現這樣就沒問題啦!
4.1.2.2修復遵循單一職責
????????將類中的方法進行職責拆分,讓每個方法進行具有單一職責的功能,這樣就解決了剛剛的問題。
????????對于每個類,如果一個類中的方法少,就可以將方法拆分為單一職責的方法;如果一個類中方法比較多,就將這個類拆分為多個遵循單一職責的類,讓每個類都具有單一職責,這樣就可以解決一定的問題。
????????可以發現這樣就沒問題啦!
4.1.3單一職責原則注意事項和細節
????????1.單一職責原則可以降低復雜度,使得一個類只進行負責一項職責。
????????2.提高類的可讀性和可維護性 => 很容易理解出來一個類僅僅去負責一個職責。
????????3.降低變更引起的風險 => 一個類中如果負責多個職責,當修改A職責的時候,存在影響B職責代碼的風險。
????????4.遵循類級別的單一職責原則還是方法級別的單一職責原則 => 如果類比較簡單的時候,可以在一個類中進行處理多個職責,使用方法去區分每個職責即可;如果類比較復雜,建議進行對類進行拆分為多個具有單一職責的類,這樣就可以降低類的復雜性,提高可維護性。
4.2接口隔離原則
4.2.1基本介紹
????????客戶端不應該依賴它不需要進行依賴的接口,也就是一個類對另一個類的依賴需要進行建立在最小接口上。
4.2.2建模分析
????????先進行簡單建模分析一下,目前進行定義了一個接口interface1,里面進行定義了五個抽象方法。
????????B和D進行實現了interface1中的五個方法。
????????A和C通過依賴于interface1接口調用B和D中的方法。
????????如果A和C去分別依賴B和D中的方法時,將接口中所有的方法都進行調配使用了,就可以認為是達到了接口隔離原則,類去依賴于其它類的時候建立在了最小接口上。
4.2.3不規范代碼分析
4.2.3.1定義的抽象接口
// 抽象接口
interface Interface1 {void operation1();void operation2();void operation3();void operation4();void operation5();
}
4.2.3.2定義的實現類B和實現類D
// 實現類B
class B implements Interface1 {@Overridepublic void operation1() {System.out.println("B進行操作1");}@Overridepublic void operation2() {System.out.println("B進行操作2");}@Overridepublic void operation3() {System.out.println("B進行操作3");}@Overridepublic void operation4() {System.out.println("B進行操作4");}@Overridepublic void operation5() {System.out.println("B進行操作5");}}
// 實現類D
class D implements Interface1 {@Overridepublic void operation1() {System.out.println("D進行操作1");}@Overridepublic void operation2() {System.out.println("D進行操作2");}@Overridepublic void operation3() {System.out.println("D進行操作3");}@Overridepublic void operation4() {System.out.println("D進行操作4");}@Overridepublic void operation5() {System.out.println("D進行操作5");}
}
4.2.3.3定義的依賴類A和依賴類C
// 類A
class A {public void depend1(Interface1 interface1) {interface1.operation1();}public void depend2(Interface1 interface1) {interface1.operation2();}public void depend3(Interface1 interface1) {interface1.operation3();}}// 類C
class C {public void depend1(Interface1 interface1) {interface1.operation1();}public void depend4(Interface1 interface1) {interface1.operation4();}public void depend5(Interface1 interface1) {interface1.operation5();}}
4.2.3.4不規范代碼分析
????????可以發現B和D兩個實現類實現了整個接口中的所有方法,但是A和C兩個類進行依賴于兩個實現類進行調用方法的時候,并沒有進行使用接口中定義的所有方法,都只是依賴了一部分方法,這樣就不是建立在最小接口上了,出現了實現方法冗余的情況,打破了接口隔離原則。
4.2.4接口隔離的的改進策略
????????為了保證A類依賴B類的時候,建立在最小接口上,需要進行遵循接口隔離原則。
????????1.類A通過接口interface1依賴B,類C通過接口interface1依賴類D,如果接口interface1對于A類和C類來說都不是最小接口(接口中存在冗余方法,A和C無法進行利用),那么B類和D類必須去實現A類和C類不需要的方法。
????????2.將接口interface1拆分為幾個接口,類A和類C分別去和需要的接口建立依賴關系,實現接口隔離原則。
4.2.5遵循接口隔離后的規范代碼
????????進行按接口隔離的規范進行修改代碼。
4.2.5.1定義的抽象接口
// 抽象接口
interface Interface1 {void operation1();
}interface Interface2 {void operation2();void operation3();
}interface Interface3 {void operation4();void operation5();
}
4.2.5.2定義的實現類B和實現類D
// 實現類B
class B implements Interface1, Interface2 {@Overridepublic void operation1() {System.out.println("B進行操作1");}@Overridepublic void operation2() {System.out.println("B進行操作2");}@Overridepublic void operation3() {System.out.println("B進行操作3");}}// 實現類D
class D implements Interface1, Interface3 {@Overridepublic void operation1() {System.out.println("D進行操作1");}@Overridepublic void operation4() {System.out.println("D進行操作4");}@Overridepublic void operation5() {System.out.println("D進行操作5");}
}
4.2.5.3定義的依賴類A和依賴類C
// 類A
class A {public void depend1(Interface1 interface1) {interface1.operation1();}public void depend2(Interface2 interface1) {interface1.operation2();}public void depend3(Interface2 interface1) {interface1.operation3();}}// 類C
class C {public void depend1(Interface1 interface1) {interface1.operation1();}public void depend4(Interface3 interface1) {interface1.operation4();}public void depend5(Interface3 interface1) {interface1.operation5();}}
4.3依賴倒轉原則
4.3.1基本介紹
????????依賴倒轉原則指的是:
????????1.高層模塊不應該依賴底層模塊,二者都應該依賴于抽象。
????????2.抽象不應該依賴于細節,細節應該依賴于抽象。
????????3.依賴倒轉(倒置)的中心思想就是面向接口編程。
????????4.依賴倒轉基于的設計理念:相對于細節的多變性,抽象的東西要穩定的多。以抽象為基礎搭建的架構比以細節為基礎的架構要穩定的多。在Java中,抽象指的是接口和抽象類,細節就是具體的實現類。
????????5.使用接口或者抽象類的目的時制定好規范,而不涉及到任何具體的操作,吧展現細節的任務交給他們的實現類去完成。
4.3.2建模分析
????????下面這個UML類圖表示的就是一個非常不規范的結構分析,Person可以進行接收郵件消息和微信消息,但是由于郵件消息和微信消息是兩個類,所以Person就進行定義了兩個方法進行接收Email/WX消息,但是這個設計不是一個優秀的設計,因為這個設計沒有進行遵循依賴倒轉原則,沒有進行抽象化設計,其實Email和Wx這兩個類都可以進行抽象為Message消息類,讓實現類都去繼承Message消息抽象類,讓不同消息類去實現Message抽象類,并自定義消息發送的邏輯。
????????Person類去使用一個方法只去接收Message抽象類即可。
4.3.3不規范的代碼分析
4.3.3.1兩個消息類
class Email {public String getInfo() {return "電子郵件信息: Hello, world";}
}class Wx {public String getInfo() {return "微信信息: 土皇帝牛逼";}
}
4.3.3.2Person使用者類
class Person extends Wx {public void receive(Email email) {System.out.println(email.getInfo());}public void receiveWX(Wx wx) {System.out.println(wx.getInfo());}
}
4.3.3.3不規范設計的總結
????????每多一個消息渠道,Person使用者就需要多增加一個代碼,這就是不使用抽象去架構的壞處,違反了依賴倒轉原則。
4.3.4規范代碼的設計
4.3.4.1抽象類Message的架構設計
????????使用抽象去架構設計。
abstract class Message {public abstract String getInfo();
}
4.3.4.2各種消息類的架構設計
class Email extends Message {@Overridepublic String getInfo() {return "電子郵件信息: Hello, world";}
}class Wx extends Message {@Overridepublic String getInfo() {return "微信信息: 土皇帝牛逼";}
}
4.3.4.3Person使用者類
class Person {public void receive(Message message) {System.out.println(message.getInfo());}
}
4.3.4.4建模分析
????????使用抽象架構設計的方式,讓Person類僅僅使用一個方法就可以接收所有按抽象類Message設計的類的對象,增強了架構設計,簡化了Person使用的類的冗余性。
4.3.5依賴關系傳遞的三種方式和應用案例
????????1.接口傳遞依賴關系 => 使用接口向類中進行傳遞依賴對象。
????????2.構造方法傳遞依賴關系 => 使用構造方法向類中進行傳遞依賴對象。
????????3.setter方法傳遞依賴關系 => 使用setter方法向類中進行傳遞依賴對象。
4.3.6通過接口傳遞依賴
4.3.6.1定義依賴接口
// ITV接口
interface ITV {void play();void stop();
}
4.3.6.2定義操作接口
// 開關的接口
interface IOpenAndClose {void open(ITV tv);void close(ITV tv);
}
4.3.6.3定義實現接口
// 實現接口
class OpenAndClose implements IOpenAndClose {@Overridepublic void open(ITV tv) {tv.play();}@Overridepublic void close(ITV tv) {tv.stop();}
}
4.3.6.4總結使用接口傳遞依賴
????????接口傳遞依賴是什么呢,其實就是通過實現接口的方式將依賴給注入到類中進行使用。
????????通過下面的UML建模圖可以觀察到其中的玄機:
????????ITV其實就是依賴類的抽象接口,IOpenAndClose是動作類的抽象接口,當動作類去實現了抽象接口之后,抽象接口就會通過抽象方法,強制性地將依賴(ITV)注入到實現類中。
4.3.7通過構造方法傳遞依賴
4.3.7.1定義依賴接口
// ITV接口
interface ITV {void play();void stop();
}
4.3.7.2定義操作接口
// 開關的接口
interface IOpenAndClose {void open();void close();
}
4.3.7.3定義實現接口
// 實現接口
class OpenAndClose implements IOpenAndClose {private ITV tv;public OpenAndClose(ITV tv) {this.tv = tv;}@Overridepublic void open() {tv.play();}@Overridepublic void close() {tv.stop();}
}
4.3.7.4總結使用構造方法傳遞依賴
????????構造方法傳遞依賴其實就是將依賴對象使用構造方法的形式傳遞過來在字段中進行存儲。
存儲到類的字段中后,進行在類內部進行使用。
4.3.8通過Setter方法進行傳遞
4.3.8.1定義依賴接口
// ITV接口
interface ITV {void play();void stop();
}
4.3.8.2定義操作接口
// 開關的接口
interface IOpenAndClose {void open();void close();
}
4.3.8.3定義實現接口
// 實現接口
class OpenAndClose implements IOpenAndClose {private ITV tv;public void setTv(ITV tv) {this.tv = tv;}@Overridepublic void open() {tv.play();}@Overridepublic void close() {tv.stop();}
}
4.3.8.4總結使用setter方法傳遞依賴
????????setter方法主要是使用setter方法將數據進行注入到類的字段中,在類的內部進行使用。
4.3.9依賴倒轉原則的注意事項和細節
1.底層模塊盡量都要有抽象類或者接口,或者兩者都有,程序穩定性更好。
????????也就是說底層的模塊最好先使用抽象類或者接口進行抽象設計,再使用底層模塊類進行實現,保證了程序的闊拓展性。
2.變量的聲明類型盡量是抽象類或者接口,這樣我們的變量引用和實際對象間,就存在一個緩沖層,利于程序擴展和優化。
????????其實就是例如A是繼承自B的,我們要進行實例化的時候,最好不要進行指定實例化A,指定類型是B最好,因為這樣可以在變量引用和實際對象間存在一個緩沖層。這個緩沖層可以起到一個便于擴展的作用。
????????為什么便于擴展呢?
????????因為如果我們想要進行擴展對象的功能的時候,可以不去直接修改A類里面的內容,去擴展B類(父類)的內容,也可以進行達到擴展對象的能力,如果父類的方法達不到要求,可以進行Override重寫父類中的方法,使用多態達到重寫方法的目的。
????????所以多層緩沖可以提高整體的可擴展性和維護性。
3.繼承遵循里氏替換原則。
4.4里氏替換原則
4.4.1什么是里氏替換原則
1.繼承包含的一層含義(子類不可隨意更改父類方法 => 打破契約):
????????父類中凡是實現好的方法,實際上是在設定規范和契約,雖然不強制要求所有子類必須遵循這些契約,但是如果子類對這些已經實現的方法隨意修改,會給整個繼承體系帶來巨大的破壞。
2.繼承帶來的弊端
????????繼承給程序帶來了侵入性,程序的可移植性降低,增加了對象之間的耦合性,如果一個類被其它的類所繼承,則當這個類需要進行修改時,必須考慮到所有的子類,并且父類修改后,所有涉及到子類的功能都有可能出現故障。
3.里氏替換原則可以保障正確的使用繼承
????????里氏替換原則其實就是:子類可以進行直接替換掉基類型,并且替換掉之后不會出現任何問題,即子類替換掉基類后,與基類原本的行為特征表示一致,子類不能隨便去重寫基類中的方法。
4.4.2違反里氏替換原則的典型
????????這個例子就是違反了里氏替換原則,B1可以進行替換掉A1,但是B1重寫了A1中的方法,使得自己內部的方法和原本基類中方法行為不一致,違反了里氏替換原則。
/*** 違反里氏替換原則*/
public class RichterSubstitution {public static void main(String[] args) {A1 a1 = new A1();System.out.println("11 - 3 = " + a1.func1(11, 3));System.out.println("---------------------------");B1 b1 = new B1();System.out.println("11 - 3 = " + b1.func1(11, 3));System.out.println("11 + 3 + 9 = " + b1.func2(11, 3));}}class A1 {public int func1(int num1, int num2) {return num1 - num2;}
}class B1 extends A1 {@Overridepublic int func1(int a, int b) {return a + b;}public int func2(int a, int b) {return func1(a, b) + 9;}
}
4.4.3遵循里氏替換原則的方案
4.4.3.1出現問題的原因
????????出現問題的原因其實就是因為類B無意重寫了父類中的方法,造成了原有功能出現了錯誤。在實際編程中常常會通過重寫父類的方法完成新的功能,這樣雖然寫起來簡單,但是整個繼承體系的復用性會變差。特別是當運行墮胎比較頻繁的時候。
????????所以盡量不要無意重寫方法,遵循里氏替換原則,提高繼承體系的復用性。
4.4.3.2出現問題的原因
????????通用的做法是:原來的父類和子類都繼承一個更通俗的基類,原有的繼承關系去掉,采用依賴,聚合,組合等關系代替。
4.4.4遵循里氏替換原則的代碼
????????準備Base基類,讓B1和A1均去繼承Base基礎類,將A1中不被B1重寫的方法都放在Base基類中,這樣一方面達到了繼承公有方法,提高了代碼復用性,第二方面又不會破壞里氏替換原則,保留了繼承的復用性。
// 準備一個基類
class Base {// 將更基礎的方法和成員寫到Base基類中
}// A1類
class A1 extends Base {// 返回兩個數的差public int func1(int num1, int num2) {return num1 - num2;}
}class B1 extends Base {public int func1(int a, int b) {return a + b;}public int func2(int a, int b) {return func1(a, b) + 9;}
}
4.5開閉原則 => 最重要的原則
????????1.開閉原則是編程中最基礎,最重要的設計原則。
????????2.一個軟件實體如類,模塊和函數應該對擴展開放(對提供方),對修改關閉(對使用方)。用抽象構建框架,用實現擴展細節。
????????3.當軟件需要變化的時候,盡量通過擴展軟件實體的行為來實現變化,而不是通過已有代碼來實現變化。
????????4.編程中遵循其它原則,以及使用設計模式的目的就是遵循開閉原則。
4.5.1違反開閉原則的典型
????????很容易可以發現這就是一個違反開閉原則的典型,開閉原則中我們要進行遵守對提供者開放,對使用者關閉的原則,在這里我們區分出1職責,其中提供者就是Shape的子類,Rectangle和Circle,使用者就是GaphicEditor,使用者接收這些類的實例對象的時候,是寫死里面的,可以發現drawShape去接收的是Shape類型的實例,但是僅僅定義了兩個分支(如果提供者進行增加數據,就會出現問題),并且將不同類型的shape的執行操作定義在了GraphicEditor中,如果提供者進行修改代碼,新增一個Shape的實現類就會出現問題,就要去修改GraphicEditor中的代碼。
????????當我們去修改GraphicEditor中的代碼的時候,就違反了開閉原則,因為我們進行改動了使用者中的代碼,所以說這段代碼違反了開閉原則。
4.5.1.1提供方代碼
class Shape {int type;
}class Rectangle extends Shape {public Rectangle() {super.type = 1;}
}class Circle extends Shape {public Circle() {super.type = 2;}
}
4.5.1.2使用方代碼
class GraphicEditor {public void drawShape(Shape shape) {if (shape.type == 1) {drawRectangle(shape);} else if (shape.type == 2) {drawCircle(shape);}}public void drawRectangle(Shape shape) {System.out.println("矩形");}public void drawCircle(Shape shape) {System.out.println("圓形");}
}
4.5.2遵循開閉原則的修改方式
????????開閉原則要進行保障對使用者關閉,對提供者開放,所以可以將Shape定義為抽象類,提供一個抽象的draw方法,讓子類都去實現,這樣當又新的圖像種類時,僅僅需要讓新的圖像類去繼承Shape,并實現draw方法即可,這樣就保證了,只需要去修改提供者,增強提供者,不需要去修改使用者。
????????其實就是讓使用者aware(感知)不到提供者發生了變化,進行了完美的解耦合。
4.5.3遵循開閉原則的代碼
4.5.3.1提供方代碼
abstract class Shape {int type;public abstract void draw();
}class Rectangle extends Shape {public Rectangle() {super.type = 1;}@Overridepublic void draw() {System.out.println("矩形");}
}class Circle extends Shape {public Circle() {super.type = 2;}@Overridepublic void draw() {System.out.println("圓形");}
}
4.5.3.2使用方代碼
class GraphicEditor {public void drawShape(Shape shape) {shape.draw();}}
4.6迪米特法則
4.6.1什么是迪米特法則
1.一個對象應該對其它對象保持最少的了解
????????其實就是設計類的時候,將字段都設置為private私有的,如果想要外部對象進行訪問/更改自己內部的狀態,使用getter/setter函數即可。
2.類與類關系越密切,耦合度越大
????????假設有兩個類,類A和類B,如果類A中的字段/方法,依賴于類B中的static字段/實例化對象中的實例字段,就稱為類A和類B中有關系,如果這種關系越密切(字段使用越多),則代表類A和類B和耦合度大。
3.迪米特法則 => 最少知道原則
????????一個類對依賴自己的類知道的越少越好。也就是說,對于被依賴的類不管多復雜,都要盡量將邏輯封裝在類的內部。對外除了進行提供public方法,不要對外泄漏任何信息。盡量將自己對外開放的邏輯都封裝在public方法中,不要進行對外暴露更多的信息。
4.迪米特法則更簡單的定義 => 僅僅與直接朋友通信。
5.直接的朋友
????????什么是直接的朋友?每個對象都會與其它對象有耦合關系,只要兩個對象之間有耦合關系,我們就說兩個對象之間是朋友關系。
????????耦合的方式都有什么?依賴,關聯,聚合等。其中我們稱出現成員變量,方法參數,方法返回值中的類為直接的朋友,而出現在局部變量的類不是直接的朋友。
????????其實意思就是說,不要讓陌生的類以局部變量的方式出現在類的內部。
4.6.2違反迪米特法則的典型
4.6.2.1定義的學院員工類和學院管理類
1.學院員工類
/*** 學院員工類*/
public class CollegeEmployee {private String id;public void setId(String id) {this.id = id;}public String getId() {return id;}
}
2.學院管理類
import java.util.ArrayList;
import java.util.List;/*** 管理學院員工的管理類*/
public class CollegeManager {// 返回學院的所有員工public List<CollegeEmployee> getAllEmployee() {List<CollegeEmployee> list = new ArrayList<>();for (int i = 0; i < 10; i++) {CollegeEmployee emp = new CollegeEmployee();emp.setId("學院員工ID = " + i);list.add(emp);}return list;}
}
4.6.2.2定義的學院員工類和學院管理類
1.學校員工類
/*** 學校總部員工*/
public class Employee {private String id;public void setId(String id) {this.id = id;}public String getId() {return id;}
}
2.學院管理類
import java.util.ArrayList;
import java.util.List;// 學校管理類
public class SchoolManager {// 返回學校總部的員工public List<Employee> getAllEmployee() {List<Employee> list = new ArrayList<>();for (int i = 0; i < 10; i++) {Employee emp = new Employee();emp.setId("學校總部員工ID = " + i);list.add(emp);}return list;}// 使用該方法進行輸出學校總部和學院總部員工信息(id)void printAllEmployee(CollegeManager sub) {List<CollegeEmployee> list1 = sub.getAllEmployee();System.out.println("------------計算機科學學院的員工-------------");for (CollegeEmployee e : list1) {System.out.println(e.getId());}System.out.println("------------學校總部的員工-------------");List<Employee> list2 = getAllEmployee();for (Employee e : list2) {System.out.println(e.getId());}}
}
4.6.2.3問題所在
????????問題主要是出現在printAllEmployee這個方法中,在SchoolManager這個類的的printAllEmployee方法中,進行使用了局部變量List<CollegeEmployee>進行其它類,沒有遵循迪米特法則,使用了非直接朋友(局部變量)。
4.6.3問題解決
4.6.3.1解決方案
????????解決這個問題十分簡單,只需要進行將打印學院管理的員工的這段邏輯,封裝抽取到學院類中,在SchoolManager中的printAllEmployee中進行調用學院封裝的打印學院管理的員工的邏輯即可,這樣就不會不遵循迪米特法則了。
4.6.3.2抽取邏輯到學院類中
public void printAllEmployee() {List<CollegeEmployee> list1 = getAllEmployee();System.out.println("------------計算機科學學院的員工-------------");for (CollegeEmployee e : list1) {System.out.println(e.getId());}
}
4.6.3.3在學校類中調用封裝的學院方法
// 使用該方法進行輸出學校總部和學院總部員工信息(id)
void printAllEmployee(CollegeManager collegeManager) {collegeManager.printAllEmployee();System.out.println("------------學校總部的員工-------------");List<Employee> list2 = getAllEmployee();for (Employee e : list2) {System.out.println(e.getId());}
}
4.6.3.4這樣封裝到底解決了什么問題?
????????為什么要遵循迪米特法則,將局部變量給取消呢?封裝為一個方法有什么好處呢?
????????先做一個抽象理解,將SchoolManager抽象為類A,將College抽象為類B,將方法printAllEmployee抽象為方法K。
????????其實我們就可以理解A類在K方法中進行局部依賴了B,其實這是一個很不好的行為,如果類B進行發生了改變,那么A的方法勢必會受到影響,必須要去更改A中的邏輯,其實這也違反了開閉原則,在這里完全可以將類A抽象為使用者,類B抽象為提供者,提供者發生更改的時候,使用者也需要更改,這樣是不是就違反了開閉原則,造成了問題的出現。
????????所以說我們將類A依賴類B的邏輯部分,抽象到類B中,定為方法M,讓A去調用M,這樣如果B發生了變化,只要B能修改M,讓M的行為保持不變,就不會影響A類中K方法,做到了下層類修改,上層類無感知的設計模式,真正實現了遵循迪米特法則解耦合。
4.6.4迪米特法則需要注意點
????????1.迪米特法則的核心 => 將降低類之間的耦合。
????????2.但是注意:由于每個類都減少了不必要的依賴,所以迪米特法則只是要求降低類間(對象間)耦合關系,不是要求做到完全沒有依賴關系。
4.7合成復用原則
????????合成復用原則(Composite Reuse Principle)
4.7.1什么是合成復用原則
????????原則是盡量使用合成/聚合的方式,而不是使用繼承。
4.7.2繼承的缺點 - 增強耦合
????????假設我們現在有兩個類,類A和類B,類A具有強大的能力(方法),類B是一個廢材,B類很渴望和A類一樣強大,那么我們可以進行使用繼承幫它嘛。
4.7.2.1定義具有強大能力的A類
????????A類狂的不行,畢竟太有實力了,此時他在大街上攜帶者三個強大的能力(方法)溜達著。
/*** 定義一個父類A*/
public class A {public void operation1() {System.out.println("進行了操作1");}public void operation2() {System.out.println("進行了操作2");}public void operation3() {System.out.println("進行了操作3");}
}
4.7.2.2定義廢材B類
????????B類看見A類金光閃閃,必是一個強大人物,撲通一聲,B類跪下說,大哥請賜給我力量,A類微微抬頭,得意一笑,說你喊我聲爹,認我當你爹,我就給你力量,B類思索片刻,心情十分復雜,腦海中回憶著往事,又浮現出對未來獲取強大的能力后的幻想,在A類就要離開之時,B類抬起頭,大喊:父親,my Dad,I would get your force!!!
????????于是B類成為了A類的兒子,繼承得到了A類的力量。
/*** 渴望擁有能力(方法)的B類*/
public class B extends A {
}
4.7.2.3測試B類的力量
????????B得到了滿足,回到家鄉,展示給大家看他從A類中繼承出來的能力。
/*** 測試類*/
public class Test {public static void main(String[] args) {B b = new B();b.operation1();b.operation2();b.operation3();}}
4.7.2.4繼承帶來的代價
????????繼承太重了,會增強類之間的強耦合性,如果僅僅是想要去獲取方法使用,不建議使用繼承的方式去獲取。
????????因為繼承會帶來很重的耦合關系(B類直接變成A類的兒子,這操作難道不重?)
4.7.3解決方案
????????像僅僅是去借用一個類中的方法,不是為了在層次上進行設計,不建議進行使用繼承,繼承應該是在層次上有設計需求的時候,進行使用比較好,如果沒有設計需求,不建議去使用這個,因為耦合性實在是太強了。
????????解決方案其實就是將繼承的方案,替換為合成(依賴),聚合,組合的方式,來進行使用其它類中的方法,而不是使用繼承,這樣就可以做到解耦合。
4.7.4使用合成(依賴)解決問題
????????合成(依賴)其實就是B中哪里需要調用A中的方法,在B中需要調用的方法中,將A對象作為參數傳進去,這樣就可以在B需要的地方去調用A中的方法了,這就是合成(依賴)
4.7.4.1定義具有強大能力的A類
????????在你面前的仍然是具有強大能力的A類。
/*** 定義一個父類A*/
public class A {public void operation1() {System.out.println("進行了操作1");}public void operation2() {System.out.println("進行了操作2");}public void operation3() {System.out.println("進行了操作3");}}
4.7.4.2定義廢材B類
????????B類這次沒有認A當爹,而是進行在自己的方法中去調用A中的方法進行使用,其實就是一個狐假虎威。
/*** 渴望擁有能力(方法)的B類*/
public class B {public void operation1(A a) {a.operation1();}public void operation2(A a) {a.operation2();}public void operation3(A a) {a.operation3();}
}
4.7.4.3總結使用合成解決問題
????????使用合成(依賴)的方式其實就是進行在B的方法中進行接收一個A類型的參數,調用A中的方法進行使用,這樣就實現了脫離繼承體系去使用A中的方法的目的。
4.7.5使用聚合解決問題
????????聚合就是在類B中定義一個類A的字段,使用setter方法為類B設置類A字段的A實例對象。
4.7.5.1定義具有強大能力的A類
????????A類沒變化,不貼代碼啦。
4.7.5.2定義廢材B類
/*** 渴望擁有能力(方法)的B類*/
public class B {private A a;public void setA(A a) {this.a = a;}public void operation1() {a.operation1();}public void operation2() {a.operation2();}public void operation3() {a.operation3();}
}
4.7.6使用組合解決問題
????????組合就是直接進行在B類中的A字段中new出來一個A對象,進行使用,這樣就是組合。
4.7.6.1定義具有強大能力的A類
????????A類沒變化,不貼代碼啦。
4.7.6.2定義廢材B類
????????可以看見確實是在B類中的A字段中直接進行new了一個A對象出來進行使用。
/*** 渴望擁有能力(方法)的B類*/
public class B {private A a = new A();public void operation1() {a.operation1();}public void operation2() {a.operation2();}public void operation3() {a.operation3();}
}