1. 程式人生 > >Java——設計模式(結構型模式)

Java——設計模式(結構型模式)

一、介面卡模式(不相容結構的協調)

在介面卡模式中引入了一個被稱為介面卡(Adapter)的包裝類,而它所包裝的物件稱為適配者(Adaptee),即被適配的類。介面卡的實現就是把客戶類的請求轉化為對適配者的相應介面的呼叫。也就是說:當客戶類呼叫介面卡的方法時,在介面卡類的內部將呼叫適配者類的方法,而這個過程對客戶類是透明的,客戶類並不直接訪問適配者類。因此,介面卡讓那些由於介面不相容而不能互動的類可以一起工作。(實際上客戶端拿到的物件,已經是介面卡的物件了)

介面卡模式可以將一個類的介面和另一個類的介面匹配起來,而無須修改原來的適配者介面和抽象目標類介面。介面卡模式定義如下:

介面卡模式(Adapter Pattern):將一個介面轉換成客戶希望的另一個介面,使介面不相容的那些類可以一起 工作,其別名為包裝器(Wrapper)。介面卡模式既可以作為類結構型模式,也可以作為物件結構型模式。(實現不改程式碼,只新增類和修改配置檔案完成呼叫其它物件方法)

  • Target(目標抽象類):目標抽象類定義客戶所需介面,可以是一個抽象類或介面,也可以是具體類。
  • Adapter(介面卡類):介面卡可以呼叫另一個介面,作為一個轉換器,對Adaptee和Target進行適配,介面卡類是介面卡模式的核心,在物件介面卡中,它通過繼承Target並關聯一個Adaptee物件使二者產生聯絡。
  • Adaptee(適配者類):適配者即被適配的角色,它定義了一個已經存在的介面,這個介面需要適配,適配者類一般是一個具體類,包含了客戶希望使用的業務方法,在某些情況下可能沒有適配者類的原始碼。

根據物件介面卡模式結構圖,在物件介面卡中,客戶端需要呼叫 request() 方法,而適配者類 Adaptee 沒有該 方法,但是它所提供的 specificRequest() 方法卻是客戶端所需要的。為了使客戶端能夠使用適配者類,需要提 供一個包裝類 Adapter,即介面卡類。這個包裝類包裝了一個適配者的例項,從而將客戶端與適配者銜接起 來,在介面卡的 request() 方法中呼叫適配者的 specificRequest() 方法。因為介面卡類與適配者類是關聯關 系(也可稱之為委派關係),所以這種介面卡模式稱為物件介面卡模式。典型的物件介面卡程式碼如下所示:

/**
 * @author x5456
 */
public class AdaptorPattern {

    //抽象成績操作類:目標介面
    interface ScoreOperation {
        public int[] sort(int array[]); //成績排序

        public int search(int array[], int key); //成績查詢
    }

    //快速排序類:適配者
    class QuickSort {
        public int[] quickSort(int array[]) {
            sort(array, 0, array.length - 1);
            return array;
        }

        public void sort(int array[], int p, int r) {
            int q = 0;
            if (p < r) {
                q = partition(array, p, r);
                sort(array, p, q - 1);
                sort(array, q + 1, r);
            }
        }

        public int partition(int[] a, int p, int r) {
            int x = a[r];
            int j = p - 1;
            for (int i = p; i <= r - 1; i++) {
                if (a[i] <= x) {
                    j++;
                    swap(a, j, i);
                }
            }
            swap(a, j + 1, r);
            return j + 1;
        }

        public void swap(int[] a, int i, int j) {
            int t = a[i];
            a[i] = a[j];
            a[j] = t;
        }
    }

    //二分查詢類:適配者
    class BinarySearch {
        public int binarySearch(int array[], int key) {
            int low = 0;
            int high = array.length - 1;
            while (low <= high) {
                int mid = (low + high) / 2;
                int midVal = array[mid];
                if (midVal < key) {
                    low = mid + 1;
                } else if (midVal > key) {
                    high = mid - 1;
                } else {
                    return 1; //找到元素返回1
                }
            }
            return -1; //未找到元素返回-1
        }
    }

    //操作介面卡:介面卡
    class OperationAdapter implements ScoreOperation {
        private QuickSort sortObj; //定義適配者QuickSort物件
        private BinarySearch searchObj; //定義適配者BinarySearch物件

        public OperationAdapter() {
            sortObj = new QuickSort();
            searchObj = new BinarySearch();
        }

        public int[] sort(int array[]) {
            return sortObj.quickSort(array); //呼叫適配者類QuickSort的排序方法
        }

        public int search(int array[], int key) {
            return searchObj.binarySearch(array, key); //呼叫適配者類BinarySearch的查詢方法
        }
    }
}

// 呼叫者
class Client {
    public static void main(String args[]) {
        ScoreOperation operation; //針對抽象目標介面程式設計
        operation = (ScoreOperation) XMLUtil.getBean(); //讀取配置檔案,反射生成物件(只需要修改配置檔案,改成OperationAdapter就行了)
        int scores[] = {84, 76, 50, 69, 90, 91, 88, 96}; //定義成績陣列
        int result[];
        int score;
        System.out.println("成績排序結果:");
        result = operation.sort(scores);
        //遍歷輸出成績 
        for (int i : scores) {
            System.out.print(i + ",");
        }
        System.out.println();
        System.out.println("查詢成績90:");
        score = operation.search(result, 90);
        if (score != -1) {
            System.out.println("找到成績90。");
        } else {
            System.out.println("沒有找到成績90。");
        }
        System.out.println("查詢成績92:");
        score = operation.search(result, 92);

        if (score != -1) {
            System.out.println("找到成績92。");
        } else {
            System.out.println("沒有找到成績92。");
        }
    }
}

類介面卡

除了物件介面卡模式之外,介面卡模式還有一種形式,那就是類介面卡模式,類介面卡模式和物件介面卡模式最大的區別在於介面卡和適配者之間的關係不同,物件介面卡模式中介面卡和適配者之間是關聯關係,而類介面卡模式中介面卡和適配者是繼承關係

class Adapter extends Adaptee implements Target { 
    public void request() {
        specificRequest();
    }
}

由於 Java、C# 等語言不支援多重類繼承,因此類介面卡的使用受到很多限制,例如如果目標抽象類 Target 不 是介面,而是一個類,就無法使用類介面卡;此外,如果適配者 Adapter 為最終(Final)類,也無法使用類適 配器。在 Java 等面向物件程式語言中,大部分情況下我們使用的是物件介面卡,類介面卡較少使用。(類適配受java中無法多繼承的限制)

優點

無論是物件介面卡模式還是類介面卡模式都具有如下優點:

  • (1) 將目標類和適配者類解耦,通過引入一個介面卡類來重用現有的適配者類,無須修改原有結構。
  • (2) 增加了類的透明性和複用性,將具體的業務實現過程封裝在適配者類中,對於客戶端類而言是透明的,而且提 高了適配者的複用性,同一個適配者類可以在多個不同的系統中複用。
  • (3) 靈活性和擴充套件性都非常好,通過使用配置檔案,可以很方便地更換介面卡,也可以在不修改原有程式碼的基礎上 增加新的介面卡類,完全符合“開閉原則”。

具體來說,類介面卡模式還有如下優點:

  • 由於介面卡類是適配者類的子類,因此可以在介面卡類中置換一些適配者的方法,使得介面卡的靈活性更強。

物件介面卡模式還有如下優點:

  • (1) 一個物件介面卡可以把多個不同的適配者適配到同一個目標;
  • (2) 可以適配一個適配者的子類,由於介面卡和適配者之間是關聯關係,根據“里氏代換原則”,適配者的子類也 可通過該介面卡進行適配。

缺點

類介面卡模式的缺點如下:

  • (1) 對於 Java、C# 等不支援多重類繼承的語言,一次最多隻能適配一個適配者類,不能同時適配多個適配者;
  • (2) 適配者類不能為最終類,如在 Java 中不能為 final 類,C# 中不能為 sealed 類;
  • (3) 在 Java、C# 等語言中,類介面卡模式中的目標抽象類只能為介面,不能為類,其使用有一定的侷限性。

物件介面卡模式的缺點如下:

  • 與類介面卡模式相比,要在介面卡中置換適配者類的某些方法比較麻煩。如果一定要置換掉適配者類的一個或多個方法,可以先做一個適配者類的子類,將適配者類的方法置換掉,然後再把適配者類的子類當做真正的適配者進行適配,實現過程較為複雜。

適用場景

  • (1) 系統需要使用一些現有的類,而這些類的介面(如方法名)不符合系統的需要,甚至沒有這些類的原始碼。
  • (2) 想建立一個可以重複使用的類,用於與一些彼此之間沒有太大關聯的一些類,包括一些可能在將來引進的類一 起工作。

二、橋接模式(處理多維度變化)

引文

在正式介紹橋接模式之前,我先跟大家談談兩種常見文具的區別,它們是毛筆和蠟筆。假如我們需要大中小 3 種 型號的畫筆,能夠繪製 12 種不同的顏色,如果使用蠟筆,需要準備 3×12 = 36 支,但如果使用毛筆的話,只需 要提供 3 種型號的毛筆,外加 12 個顏料盒即可,涉及到的物件個數僅為 3 + 12 = 15,遠小於36,卻能實現與 3 6 支蠟筆同樣的功能。如果增加一種新型號的畫筆,並且也需要具有 12 種顏色,對應的蠟筆需增加 12 支,而毛 筆只需增加一支。為什麼會這樣呢?通過分析我們可以得知:在蠟筆中,顏色和型號兩個不同的變化維度(即兩 個不同的變化原因)融合在一起,無論是對顏色進行擴充套件還是對型號進行擴充套件都勢必會影響另一個維度;但在毛 筆中,顏色和型號實現了分離,增加新的顏色或者型號對另一方都沒有任何影響。如果使用軟體工程中的術 語,我們可以認為在蠟筆中顏色和型號之間存在較強的耦合性(從而違反了單一原則:一個類只幹一件事,否則如果另一件事變了,那麼你這個類又要修改),而毛筆很好地將二者解耦,使用起來非常靈 活,擴充套件也更為方便。在軟體開發中,我們也提供了一種設計模式來處理與畫筆類似的具有多變化維度的情 況,即本章將要介紹的橋接模式。

橋接模式(JDBC的設計採用該模式)

橋接模式是一種很實用的結構型設計模式,如果軟體系統中某個類存在兩個獨立變化的維度,通過該模式可以將這兩個維度分離出來,使兩者可以獨立擴充套件,讓系統更加符合“單一職責原則”。與多層繼承方案不同,它將兩個獨立變化的維度設計為兩個獨立的繼承等級結構,並且在抽象層建立一個抽象關聯,該關聯關係類似一條連線兩個獨立繼承結構的橋,故名橋接模式。

橋接模式用一種巧妙的方式處理多層繼承存在的問題,用抽象關聯取代了傳統的多層繼承,將類之間的靜態繼承關係轉換為動態的物件組合關係,使得系統更加靈活,並易於擴充套件,同時有效控制了系統中類的個數。橋接定義如下:

橋接模式(Bridge Pattern):將抽象部分與它的實現部分分離,使它們都可以獨立地變化。它是一種物件結構型模式,又稱為柄體(Handle and Body)模式或介面(Interface)模式。

橋接模式的結構與其名稱一樣,存在一條連線兩個繼承等級結構的橋,橋接模式結構如圖所示:

  • Abstraction(抽象類):用於定義抽象類的介面,它一般是抽象類而不是介面,其中定義了一個 Implementor(實現類介面)型別的物件並可以維護該物件,它與 Implementor 之間具有關聯關係,它既可以包含抽象業務方法,也可以包含具體業務方法。
  • RefinedAbstraction(擴充抽象類):擴充由 Abstraction 定義的介面,通常情況下它不再是抽象類而是具體類,它實現了在 Abstraction 中宣告的抽象業務方法,在 RefinedAbstraction 中可以呼叫在 Implementor 中定義的業務方法。
  • Implementor(實現類介面):定義實現類的介面,這個介面不一定要與 Abstraction 的介面完全一致,事 實上這兩個介面可以完全不同,一般而言,Implementor 介面僅提供基本操作,而 Abstraction 定義的介面 可能會做更多更復雜的操作。Implementor 介面對這些基本操作進行了宣告,而具體實現交給其子類。通過 關聯關係,在 Abstraction 中不僅擁有自己的方法,還可以呼叫到 Implementor 中定義的方法,使用關聯 關係來替代繼承關係。
  • ConcreteImplementor(具體實現類):具體實現 Implementor 介面,在不同的 ConcreteImplementor 中提供基本操作的不同實現,在程式執行時,ConcreteImplementor 物件將替換其父類物件,提供給抽象 類具體的業務操作方法。

橋接模式是一個非常有用的模式,在橋接模式中體現了很多面向物件設計原則的思想,包括“單一職責原則”、“開閉原則”、“合成複用原則”、“里氏代換原則”、“依賴倒轉原則”等。熟悉橋接模式有助於我們深入理解這些設計原則,也有助於我們形成正確的設計思想和培養良好的設計風格。

在使用橋接模式時,我們首先應該識別出一個類所具有的兩個獨立變化的維度,將它們設計為兩個獨立的繼承等級結構,為兩個維度都提供抽象層,並建立抽象耦合。通常情況下,我們將具有兩個獨立變化維度的類的一些普通業務方法和與之關係最密切的維度設計為“抽象類”層次結構(抽象部分),而將另一個維度設計為“實現類”層次結構(實現部分)。例如:對於毛筆而言,由於型號是其固有的維度,因此可以設計一個抽象的毛筆類,在該類中宣告並部分實現毛筆的業務方法,而將各種型號的毛筆作為其子類;顏色是毛筆的另一個維度,由於它與毛筆之間存在一種“設定”的關係,因此我們可以提供一個抽象的顏色介面,而將具體的顏色作為實現該介面的子類。在此,型號可認為是毛筆的抽象部分,而顏色是毛筆的實現部分,結構示意圖如圖所示:

public class BridgingPattern {

    //畫素矩陣類:輔助類,各種格式的檔案最終都被轉化為畫素矩陣,不同的作業系統提供不同的方式顯示畫素矩陣
    class Matrix {
        //此處程式碼省略
    }

    //抽象影象類:抽象類
    abstract class Image {
        protected ImageImp imp;

        public void setImageImp(ImageImp imp) {
            this.imp = imp;
        }

        public abstract void parseFile(String fileName);
    }

    //抽象作業系統實現類:實現類介面
    interface ImageImp {
        public void doPaint(Matrix m); //顯示畫素矩陣
    }

    //Windows作業系統實現類:具體實現類
    class WindowsImp implements ImageImp {
        public void doPaint(Matrix m) { //呼叫Windows系統的繪製函式繪製畫素矩陣
            System.out.print("在Windows作業系統中顯示影象:");
        }
    }

    //Linux作業系統實現類:具體實現類
    class LinuxImp implements ImageImp {
        public void doPaint(Matrix m) { //呼叫Linux系統的繪製函式繪製畫素矩陣
            System.out.print("在Linux作業系統中顯示影象:");
        }
    }

    //Unix作業系統實現類:具體實現類
    class UnixImp implements ImageImp {
        public void doPaint(Matrix m) { //呼叫Unix系統的繪製函式繪製畫素矩陣
            System.out.print("在Unix作業系統中顯示影象:");
        }
    }

    //JPG格式影象:擴充抽象類
    class JPGImage extends Image {
        public void parseFile(String fileName) {
            //模擬解析JPG檔案並獲得一個畫素矩陣物件m;
            Matrix m = new Matrix();
            imp.doPaint(m);
            System.out.println(fileName + ",格式為JPG。");
        }
    }

    //PNG格式影象:擴充抽象類
    class PNGImage extends Image {
        public void parseFile(String fileName) {
            //模擬解析PNG檔案並獲得一個畫素矩陣物件m;
            Matrix m = new Matrix();
            imp.doPaint(m);
            System.out.println(fileName + ",格式為PNG。");
        }
    }
}


// 客戶端呼叫
class Client {
    public static void main(String args[]) {
        Image image = (Image) XMLUtil.getBean("image");
        ImageImp imp = (ImageImp) XMLUtil.getBean("os");
        image.setImageImp(imp);
        image.parseFile("小龍女");
    }
}

介面卡模式與橋接模式的聯用

在某系統的報表處理模組中,需要將報表顯示和資料採集分開,系統可以有多種報表顯示方式也可以有多種資料採集方式,如可以從文字檔案中讀取資料,也可以從資料庫中讀取資料,還可以從 Excel 檔案中獲取資料。如果需要從 Excel 檔案中獲取資料,則需要呼叫與Excel 相關的 API,而這個 API 是現有系統所不具備的,該 API 由廠商提供。使用介面卡模式和橋接模式設計該模組。

在設計過程中,由於存在報表顯示和資料採集兩個獨立變化的維度,因此可以使用橋接模式進行初步設計;為了 使用 Excel 相關的 API 來進行資料採集則需要使用介面卡模式。系統的完整設計中需要將兩個模式聯用,如圖所示:

優點

(1)分離抽象介面及其實現部分。橋接模式使用“物件間的關聯關係”解耦了抽象和實現之間固有的繫結關係,使 得抽象和實現可以沿著各自的維度來變化。所謂抽象和實現沿著各自維度的變化,也就是說抽象和實現不再在同 一個繼承層次結構中,而是“子類化”它們,使它們各自都具有自己的子類,以便任何組合子類,從而獲得多維 度組合物件。

(2)在很多情況下,橋接模式可以取代多層繼承方案,多層繼承方案違背了“單一職責原則”,複用性較差,且類 的個數非常多,橋接模式是比多層繼承方案更好的解決方法,它極大減少了子類的個數。

(3)橋接模式提高了系統的可擴充套件性,在兩個變化維度中任意擴充套件一個維度,都不需要修改原有系統,符合“開閉原則”。

缺點

(1)橋接模式的使用會增加系統的理解與設計難度,由於關聯關係建立在抽象層,要求開發者一開始就針對抽象層進行設計與程式設計。

(2)橋接模式要求正確識別出系統中兩個獨立變化的維度,因此其使用範圍具有一定的侷限性,如何正確識別兩個獨立維度也需要一定的經驗積累。

適用場景

(1)如果一個系統需要在抽象化和具體化之間增加更多的靈活性,避免在兩個層次之間建立靜態的繼承關係,通過 橋接模式可以使它們在抽象層建立一個關聯關係。

(2)“抽象部分”和“實現部分”可以以繼承的方式獨立擴充套件而互不影響,在程式執行時可以動態將一個抽象化子 類的物件和一個實現化子類的物件進行組合,即系統需要對抽象化角色和實現化角色進行動態耦合。

(3)一個類存在兩個(或多個)獨立變化的維度,且這兩個(或多個)維度都需要獨立進行擴充套件。

(4)對於那些不希望使用繼承或因為多層繼承導致系統類的個數急劇增加的系統,橋接模式尤為適用。

三、組合模式(樹形結構的處理)