設計模式的基本概念
在軟件工程中看,設計模式是對軟件設計中普遍存在(反覆出現)的各種問題,所提出的解決方案。設計模式不是代碼,而是某類問題的通用解決方案。設計模式代表着最佳實踐。這些解決方案是衆多軟件人員的試錯和總結。設計模式包含了面向對象的精髓,懂得了設計模式,就懂得了面向對象分析和設計的精要。
-
設計模式的本質:
- 提高軟件的 “可維護性”,“通用性”,“可擴展性”,並降低"軟件的複雜度"。
- 提高軟件的 “可維護性”,“通用性”,“可擴展性”,並降低"軟件的複雜度"。
-
設計模式的重要性
- 當項目開發完成時,如果客戶提出新增功能怎麼辦?
==> 可擴展性 - 當項目開發完成時,程序員離職時,需要接受維護怎麼辦?
==> 可維護性,可讀性, 規範性 - 在實際項目中用過什麼設計模式?怎麼使用的?解決了哪些問題?
- 設計模式在軟件的什麼位置?
面向對象OOP ==> 功能模塊 (設計模式+數據結構+算法) ==> 框架(使用多種設計模式) ==> 架構(服務器集羣)
- 當項目開發完成時,如果客戶提出新增功能怎麼辦?
-
設計模式的目的
- 代碼可重用性 ==> 相同功能的代碼,不用多次編寫。
- 代碼可讀性 ==> 編碼規範性,便於其他人閱讀和理解。
- 代碼可擴展性 ==>當需要新增功能時,非常方便,易維護。
- 代碼可靠性 ==>新增功能時,對原來的功能沒有影響。
- 高內聚,低耦合 ==>對外呈現高內聚,低耦合。
-
核心思想:
- 找出應用中可能需要變化的地方,把它們獨立出來。不要和那些不需要的代碼混合在一起。
- 針對接口編程,而不針對實現類編程。
- 爲交互對象之間的松耦合設計而努力。
24種設計模式
- 創建型模式
- 單例模式, 工廠方法模式,抽象工廠模式,原型模式, 建造者模式
- 結構型模式
- 適配器模式, 橋接模式, 裝飾模式, 組合模式, 外觀模式, 享元模式,代理模式
- 行爲型模式
- 模板方法模式,命令模式,訪問者模式, 迭代器模式,觀察者模式,中介者模式, 備忘錄模式,解釋器模式,狀態模式,策略模式, 責任鏈模式
- 模板方法模式,命令模式,訪問者模式, 迭代器模式,觀察者模式,中介者模式, 備忘錄模式,解釋器模式,狀態模式,策略模式, 責任鏈模式
設計模式常用的七大原則
- 1、單一職責原則
- 2、接口隔離原則
- 3、依賴倒轉原則
- 4、里氏替換原則
- 5、開閉原則OCP
- 6、迪米特法則(最少知道原則)
- 7、合成複用原則
一、單一職責原則
- 基本概念
對類來說,一個類只負責一項職責。如果類負責兩個不同的職責:職責A和職責B,當職責A的需求發生改變時,則類中的代碼發生改變,此時可能造成職責A和職責B的代碼發生改變而造成錯誤,以及要重新進行單元測試。所以最好是將類進行單一職責的粒度進行分解。- 由於有多個職責,則由於改動而需要維護的代碼更多
- 只有當類中邏輯足夠簡單,纔可以在代碼級別違反單一原則,只有當類中方法數量足夠少時,可在方法級別上保持單一職責原則。
- 優點
- 降低了類的複雜度,一個類只負責一項職責。
- 提高類的可讀性,可維護性。
- 降低了變更代碼引起的風險。
- 通常情況下,我們應該遵循單一職責原則,除非邏輯足夠簡單,可以在放方法級別保持單一職責原則。
#b,c表示兩類不同的功能
public class A {
public void b(){
...
}
public void c(){
...
}
}
==>
public class AB {
public void b(){
...
}
}
public class AC {
public void c(){
...
}
}
二、接口隔離原則
傳統方法的問題和接口隔離原則的改進
- 傳統方法的問題:
==>類A通過接口interface依賴B, 類C通過接口interface依賴類D,如果接口interface對於類A和類C不是最小接口,那麼類B和類D就必須要去實現他們不需要的方法。
- 接口隔離原則:
==>將接口interface拆分成獨立的幾個接口,類A和類C分別與他們需要的接口建立依賴關係,保持接口的最小依賴。這就是接口分離原則。
#類A通過interface E依賴類C, 其只依賴a(),b()方法
public class A {
private E e;
public A( E e ){
this.e = e;
this.e.a();
this.e.b();
}
}
#類C通過interface E依賴類D,且只依賴c(),d()方法
public class C {
private E e;
public C( E e ){
this.e = e;
this.e.c();
this.e.d();
}
}
#接口E
public interface E {
public void a();
public void b();
public void c();
public void d();
}
public class B implements E {
...
}
public class D implements E {
...
}
- 根據接口隔離原則進行改進
#類A通過interface E依賴類C, 其只依賴a(),b()方法
public class A {
private E e;
public A( E e ){
this.e = e;
this.e.a();
this.e.b();
}
}
#類C通過interface F依賴類D,且只依賴c(),d()方法
public class C {
private F f;
public C( F f ){
this.f = f;
this.f.c();
this.f.d();
}
}
public interface E {
public void a();
public void b();
}
public interface F {
public void c();
public void d();
}
public class B implements E {
...
}
public class D implements F {
...
}
三、依賴倒轉原則
- 高層模塊不應該依賴底層模塊,二者都應該依賴其抽象。層之間依賴接口。
- 抽象不應該依賴細節,細節應該依賴抽象。
- 依賴倒轉的核心思想:面向接口編程。
- 依賴倒轉原則是基於以下的設計理念:
- 相對於細節的多變性,抽象的東西要穩定很多。以抽象爲基礎搭建的架構比以細節爲基礎搭建的架構要穩定的多。在java中,抽象是指接口或者抽象類,細節就是具體的實現類。
- 使用接口或者抽象類的目的是制定好規範,而不涉及任何具體的操作,把戰線細節的任務交給他們的實現類去完成。
依賴倒轉的核心思想就是:面向接口編程。層與層之間通過接口來訪問。不要在類的內部去new所依賴的對象,而是通過依賴關係傳遞到類內部來。
-
依賴關係傳遞的三種方式:
- 接口傳遞依賴;
public interface IOpenAndClose { public void open( TV tv ); } public interface TV { public void play(); } public class AqyTV implements TV { public void play(){ System.out.println("hello aqy!"); } } public OpenAndClose implements IOpenAndClose { public void open( TV tv ) { tv.play(); } public static void main( String[] args ){ IOpenAndClose iOpenAndClose = new OpenAndClose(); iOpenAndClose.open( new AqyTV() ); } }
- 構造方法傳遞依賴;
public interface IOpenAndClose { public void open(); } public interface TV { public void play(); } public class AqyTV implements TV { public void play(){ System.out.println("hello AqyTV!"); } } public class OpenAndClose implements IOpenAndClose { private TV tv; public OpenAndClose( TV tv ){ this.tv = tv; } public static void main( String[] args ){ IOpenAndClose iOpenAndClose = new OpenAndClose(new AqyTV()); iOpenAndClose.open(); } }
- setter方式傳遞依賴;
public interface IOpenAndClose { public void open(); public void setTV( TV tv ); } public interface TV { public void play(); } public class AqyTV implements TV { public void play(){ System.out.println("hello AqyTV!"); } } public class OpenAndClose implements IOpenAndClose { private TV tv; public void setTV( TV tv ){ this.tv = tv; } public static void main( String[] args ){ IOpenAndClose iOpenAndClose = new OpenAndClose(new AqyTV()); iOpenAndClose.open(); } }
依賴倒轉原則的注意事項和細節:
- 底層模塊(被依賴或者被調用的組件)儘量使用抽象類或者接口,或者兩者都有,程序穩定性會更好。
- 變量的聲明類型儘量是抽象類或者接口,這樣我們的變量引用和實際對象間,就存在一個緩衝層,利用實現接口的實體類進行擴展和優化。
- 繼承時準尋里氏替換原則。
四、里氏替換原則
- 如果對每個類型爲T1的對象O1,都有類型爲T2的對象O2,使得以T1定義的所有程序在所有的對象O1都代換爲O2時,程序的行爲沒有發生變化,那麼類型T2是類型T1的子類型。換句話說:素所有引用基類的地方都必須能透明的使用其子類的對象。即子類繼承於父類的方法,行爲保持一致,不要去重寫覆蓋。
- 在使用繼承時,遵循里氏原則,在子類中儘量不要重寫父類的方法,這樣在版本迭代時,能保持較好的向下兼容性。
- 里氏原則告訴我們: 繼承爲讓兩個類之間的耦合增強,如果修改父類代碼時,則會影響到子類的使用。修改子類繼承於父類的代碼,也會導致兼容性問題出現。在適當的情況下,可以通過聚合,組合,依賴來解決問題。
針對繼承父類方法,又不得不重寫父類方法的處理方案
- 可以解除繼承關係,而是通過聚合,組合的方式來結局問題。
- 也可以讓原有的父子繼承關係解除,讓不要重寫的方法提取到一個公共的基類(編程一個虛擬類),讓需要重寫的方法在原來的父子類中分別實現。
子類重寫父類方法的情形
public interface A {
public void a();
public void b();
public void c();
public void d();
}
public class B implements A {
public void a(){
System.out.println("B-a");
}
public void b(){
System.out.println("B-b");
}
public void c(){
System.out.println("B-c");
}
public void d(){
System.out.println("B-d");
}
}
public class C extends B {
public void a(){
System.out.println("B-a");
}
public void b(){
System.out.println("B-b");
}
public void c(){
System.out.println("C-c");
}
public void d(){
System.out.println("C-d");
}
}
遵循里氏替換原則的繼承處理
- 將共有方法提取到公共的抽象類,不相同行爲的方法單獨實現。
public interface A {
public void a();
public void b();
public void c();
public void d();
}
public abstract class D {
public void a() {
System.out.println("D-a");
}
public void b(){
System.out.println("D-b");
}
public abstract void c();
public abstract void d();
}
public class B extends D {
public void c(){
System.out.println("B-c");
}
pulic void d(){
System.out.println("B-d");
}
}
public class C {
public void c(){
System.out.println("C-c");
}
pulic void d(){
System.out.println("C-d");
}
}
五、開閉原則
- 開閉原則: 最基礎,最重要的設計原則。
==> 一個軟件如類,模塊和函數應該對提供方擴展開放,對使用方修改關閉。。用抽象構建框架,用實現擴展細節。 - 當軟件需求發生變化時,儘量通過擴展軟件實體的行爲來實現變化,而不是通過修改已有的代碼來實現變化。(擴展行爲,不要輕易去修改已有的使用方中的代碼)。
- 編程中遵循其他原則,以及使用設計模式的目的就是爲了遵循開閉原則。提供方不允許修改代碼。通過新增來擴展新功能。使用方不允許新增或者修改。(當做第三方庫來對待使用方的代碼)。
#通過接口傳遞依賴關係,根據多態來動態新增功能
public interface A {
public void setTV( TV tv );
}
public interface TV {
public void play();
}
public class IqyTV implements TV {
public void play(){
System.out.println("hello iqyTV");
}
}
public class B implements A {
private TV tv;
public void setTV( TV tv ){
this.tv = tv;
}
public static void main( String[] args ){
A a = new B();
a.setTV( new IqyTV() );
a.tv.play();
}
}
六、迪米特法則(最少知道原則)
- 一個對象應該對其他對象保持最少的瞭解。類與類之間的關係越密切,耦合度越大。
- 迪米特法則:又稱爲最少知道原則,即一個類對自己依賴的類知道的越少越好。也就是說: 對於被依賴的類,不管多複雜,都儘量將邏輯封裝在類的內部,對外除了提供的必要的public方法,不對外泄露任何信息。
關於迪米特法則的簡單定義:
- 只與直接朋友通信,且直接朋友也只提供public方法,不暴露內部邏輯。
關於直接朋友:
- 任何兩個對象之間都有耦合關係,只要有耦合關係,就是朋友。耦合的方式有很多:依賴,關聯,組合,聚合等。其中出現在成員變量,方法參數,方法返回值中的類稱爲直接朋友。出現在方法中的局部變量中的類不是直接朋友。(陌生類最好不要以局部變量的形式出現在類的內部)
七、合成複用原則
- 合成複用原則:
- 能用合成,聚合的方式,就不推薦用繼承。
- 能用合成,聚合的方式,就不推薦用繼承。