第23次文章:結構性模式

  • 2019 年 10 月 8 日
  • 筆記

前面三期我們主要介紹了4中創建型模式:單例模式、工廠模式、建造者模式、原生模式。這周我們開始進入下一大塊兒的模式學習——結構性模式。

一、結構性模式:

1、核心作用

從程式的結構上實現松耦合,從而可以擴大整體的類結構,用來解決更大的問題。

2、分類

適配器模式、代理模式、橋接模式、組合模式、裝飾模式、外觀模式、享元模式

二、適配器adapter模式

1、什麼是適配器模式?

將一個類的介面轉換成客戶希望的另外一個介面。adapter模式使得原本由於介面不兼容而不能一起工作的那些類可以在一起工作。舉個生活中的常見例子,讀卡器是作為記憶體卡和筆記型電腦之間的適配器。我們將記憶體卡插入讀卡器,再將讀卡器插入筆記型電腦,這樣就可以通過筆記型電腦來讀取記憶體卡。

2、模式中的角色

(1)目標介面(Target):客戶所期待的介面。目標可以是具體的或抽象的類,也可以是介面

(2)需要適配的類(Adaptee):需要適配的類或適配者類。

(3)適配器(Adapter):通過包裝一個需要適配的對象,把原介面轉換成目標介面。

3、應用場景

(1)經常用來做舊系統改造和升級。

(2)如果我們的系統開發之後再也不需要維護,那麼很多模式都是沒有必要的,但是,維護一個系統的代價往往是開發一個系統的數倍。

(3)適配器不是在詳細設計時考慮的,而是解決正在服役的項目的問題。

4、適配器模式的實例化

假設我們現在有一台年代久遠的電腦,只能讀取SD卡中的內容,然而隨著時間飛逝,出現了TF卡,同樣想要在這台電腦上讀取卡中的內容,那麼我們就需要使用適配器作為一個中轉,使得此台電腦還可以讀取TF卡中的內容。

(1)我們先定義一個SD卡介面

public interface SDCard {  void readMessge();}

(2)實現SD卡介面的一個具體類

public class SDObject implements SDCard{  @Override  public void readMessge() {    System.out.println("I am SDObject!");  }}

(3)我們再定義一個電腦介面,只能讀取SD卡

public interface Computer {  void readSD(SDCard sdCard);}

(4)實現電腦介面,創建一個具體實現類

public class ComputerObj implements Computer{  @Override  public void readSD(SDCard sdCard) {    if (sdCard == null) {      try {        throw new Exception();      } catch (Exception e) {        e.printStackTrace();      }    }else {      sdCard.readMessge();    }  }}

(5)此時,我們就已經完成了一個只有SD卡介面的電腦的創建。現在如果我們需要再增加一個讀取TF卡內容的功能,那麼我們就需要使用相應的適配器來完成這種功能。首先還是需要創建一個TF卡的介面。

public interface TFCard {  void readMessage();}

(6)實現TF介面,並且創建一個具體的實現類

public class TFObject implements TFCard{  @Override  public void readMessage() {    System.out.println("I am TFObject!");  }}

(7)創建一個適配器介面。此介面需要和電腦的SD卡介面對接,所以需要實現SDCard介面,而傳輸的內容卻來自於TFCard,所以在適配器的內部,需要增加一個TF卡對象作為私有屬性,在適配器的內部進行真實數據的傳輸。

public class TFadapteeSD implements SDCard {  private TFCard tf;    public TFadapteeSD(TFCard tf) {    super();    this.tf = tf;  }  @Override  public void readMessge() {    if(tf == null) {      try {        throw new Exception();      } catch (Exception e) {        e.printStackTrace();      }    }else {      tf.readMessage();              }    }}

(8)最後我們可以對上述的適配器模式進行一個簡單的測試

public class Demo {  public static void main(String[] args) {    //直接利用電腦的SD卡介面,讀取SD卡內容    Computer c = new ComputerObj();    SDCard sd = new SDObject();    c.readSD(sd);            //使用新增的適配器來完成TF卡的讀取操作    TFCard tf = new TFObject();    TFadapteeSD tas = new TFadapteeSD(tf);//創建一個適配器        c.readSD(tas);  }}

查看一下結果:

tips:首先使用Computer對象c讀取SDCard對象sd的內容,可以兼容。後面又創建一個TFCard對象tf,通過適配器,使得最後c也讀取到了對象tf的內容。適配器模式完成了兩個不同介面的對接。

5、要點

適配器模式屬於一種補救模式,在一個系統中,如果大量的出現適配器,會導致整個系統的邏輯及其混亂。因為系統調用的適配器介面,其真實內部的調用內容源自於其他介面,這就使得整體的系統邏輯分析和判斷十分麻煩。所以在一個系統最初的設計時,並不會去考慮使用適配器,只有在後期系統功能的擴展時,為了達到不去更改源程式碼的目的,才會適當的增加一定量的適配器,來使得系統兼容新的產品類資訊。

三、代理模式(proxy pattern)

1、核心作用

(1)通過代理,控制對對象的訪問.

(2)可以詳細控制訪問某個(某類)對象的方法,在調用這個方法前做前置處理,調用這個方法後做後置處理。(即:AOP的微觀實現)

(3)AOP(Aspect Oriebted Programming 面向切面編程)的核心實現機制。

2、核心角色

(1)抽象角色:定義代理角色和真實角色的公共對外方法。

(2)真實角色:實現抽象角色,定義真實角色所要實現的業務邏輯,供代理角色調用。關注真正的業務邏輯。

(3)代理角色:實現抽象角色,是真實角色的代理,通過真實角色的業務邏輯方法來實現抽象方法,並可以附加自己的操作。將統一的流程式控制制放到代理角色中處理。

3、應用場景:

(1)安全代理:屏蔽對真實角色的直接訪問。

(2)遠程代理:通過代理類處理遠程方法調用(RMI)

(3)延遲載入:先載入輕量級的代理對象,真正需要再載入真實對象。

4、分類:

(1)靜態代理(靜態定義代理類):屬於我們自己定義好的代理類

(2)動態代理(動態生成代理類):屬於程式自動生成的代理類(一般都使用動態代理)

5、靜態代理的實現

以一個歌手和其經紀人為背景。此時歌手就屬於真實角色,而經紀人就屬於代理角色。在一場活動中,會有很多流程需要走,但是只有唱歌這一個環節需要歌手真實的出面完成任務。所以我們首先定義一個介面,包含有整個演唱會所需要的全部流程,然後對於真實角色和代理角色分別去進行實現這些方法。

(1)流程介面

public interface Star {  void confer();//面談  void signContract();//簽約  void bookTicket();//訂票  void sing();//唱歌  void collectMoney();//收尾款}

(2)真實角色實現介面中的全部方法

public class RealStar implements Star{  @Override  public void confer() {    System.out.println("RealStar.confer()");  }  @Override  public void signContract() {    System.out.println("RealStar.signContract()");    }  @Override  public void bookTicket() {    System.out.println("RealStar.bookTicket()");    }  @Override  public void sing() {    System.out.println("RealStar.sing()");    }  @Override  public void collectMoney() {    System.out.println("RealStar.collectMoney()");  }}

(3)代理角色實現介面中的全部方法

public class ProxyStar implements Star {      private RealStar realstar;  public ProxyStar(RealStar realstar) {    super();    this.realstar = realstar;  }  @Override  public void confer() {    System.out.println("ProxyStar.confer()");  }  @Override  public void signContract() {    System.out.println("ProxyStar.signContract()");  }  @Override  public void bookTicket() {    System.out.println("ProxyStar.bookTicket()");  }  @Override  public void sing() {    realstar.sing();  }  @Override  public void collectMoney() {    System.out.println("ProxyStar.collectMoney()");  }}

tips:在程式碼中我們可以看到,代理角色中的所有方法都是自己的方法,唯獨在sing方法上,代理角色調用的是真實角色的sing方法。

(4)對代理模式進行測試

public class Client {  public static void main(String[] args) {    RealStar realStar = new RealStar();    ProxyStar proxyStar = new ProxyStar(realStar);        proxyStar.confer();    proxyStar.signContract();    proxyStar.bookTicket();    proxyStar.sing();    proxyStar.collectMoney();  }}

我們來查看一下結果:

tips:在所有的流程中,只有sing()方法的真實調用時RealStar,其他的所有方法都是屬於代理角色中的方法。所以在真箇流程中,只是將最重要的sing()交給RealStar,其他方法代理角色全部代替,這就屬於是代理模式

6、動態代理的實現

在靜態代理的基礎上,我們保留真實角色RealSatr和Star介面,然後創建一個類StarHandler,並且實現InvocationHandler介面

public class StarHandler implements InvocationHandler{  Star realStar;  public StarHandler(Star realStar) {    super();    this.realStar = realStar;  }    @Override  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {    Object object = null;    System.out.println("真正的方法執行前!");    System.out.println("面談,簽合約,預付款,訂機票");    if(method.getName().equals("sing")) {      object = method.invoke(realStar, args);    }    System.out.println("真正的方法執行以後!");    System.out.println("收尾款");    return object;  }}

測試程式碼:

public class Client {  public static void main(String[] args) {    Star realStar = new RealStar();    StarHandler handler = new StarHandler(realStar);    Star proxy = (Star) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),         new Class[] {Star.class }, handler);    proxy.sing();  }}

查看結果:

tips:在使用動態代理的時候,我們將所有代理角色的活動可以提前寫在StarHandler中的invoke方法中,然後在客戶端創建handler對象,將其傳入程式自己生成的代理對象proxy中。在proxy調用star方法的時候,會在內部調用invoke方法,程式按照invoke方法中的流程依次執行。

7、動態代理相比於靜態代理的優點

抽象角色中(介面)聲明的所有方法都被轉移到一個集中的方法中處理,這樣,我們可以更加靈活和統一的處理眾多的方法。

四、橋接模式

1、場景:

商城系統中常見的商品分類,以電腦為類,如何良好的處理商品分類的問題?我們可以用多層繼承結構實現,例如:

上圖顯示了一個電腦分類的現狀,分別有戴爾和聯想兩個品牌,每個品牌都有三個產品,筆記型電腦,台式機,iPad。所以在使用多層集成結構的時候,總共需要6個類,假若需要再增加一個三星系列,那麼首先需要增加一個三星品牌類,然後再在這個品牌類下面增加3個產品系列。這樣的結構會面對著幾個明顯的問題。

(1)擴展問題(類個數膨脹問題)

如果要增加一個新的電腦類型:智慧手機,則要增加各個品牌下面的類;如果要增加一個新的品牌,也要增加各種電腦類型的類。

(2)違反單一職責原則

所有的子類都含有品牌和機型兩個變化因素,有兩個引起這個類變化的原因。

為了解決上面的問題,我們提出了橋接模式

2、核心要點

處理多層繼承結構,處理多維度變化的場景,將各個維度設計成獨立的集成結構,使各個維度可以獨立的擴展在抽象層上建立關聯。

3、程式碼實現

我們將每一個維度設計成為獨立的集成結構,分析上面的場景,擁有兩個維度,分別是品牌和電腦類型,所以我們分析之後,可以分別建立品牌維度——聯想、戴爾,以及電腦類型維度——筆記型電腦電腦、台式機、iPad。下面我們依次實現這兩個維度。

(1)品牌維度

public interface Brand {  void sale();}  class Lenovo implements Brand{  @Override  public void sale() {    System.out.println("銷售聯想電腦!");  }}  class Dell implements Brand{  @Override  public void sale() {    System.out.println("銷售戴爾電腦!");  }}

(2)電腦類型維度

public class Computer {  protected Brand brand;  public Computer(Brand brand) {    super();    this.brand = brand;  }    public void sale() {    brand.sale();  }}  class Desktop extends Computer{  public Desktop(Brand brand) {    super(brand);  }  @Override  public void sale() {    super.sale();    System.out.println("銷售台式機!");  }}  class Laptop extends Computer{  public Laptop(Brand brand) {    super(brand);  }    @Override  public void sale() {    super.sale();    System.out.println("銷售筆記型電腦電腦!");  }  }  class Ipad extends Computer{  public Ipad(Brand brand) {    super(brand);  }  @Override  public void sale() {    super.sale();    System.out.println("銷售平板電腦!");  }  }

(3)使用客戶端進行檢測

public class Client {  public static void main(String[] args) {    Brand brand = new Lenovo();    Computer c = new Desktop(brand);    c.sale();        Computer c2 = new Laptop(new Dell());    c2.sal

tips:

1.在整個橋接模式中,我們分別以Brand和Computer建立了兩個不同維度的類。在Computer類中,自定義一個brand屬性,然後在sale方法中調用brand的sale方法, 並且增加自己獨有的方法。通過這樣的做法,就可以很容易的對類進行擴展。假如需要新增華碩的筆電、台式機、iPad時,僅需要在Brand中增加一個華碩品牌就好了,並不需要對Computer類進行任何的改動。這樣就使得橋接模式具有高可擴展性。

2.使用橋接模式之後,各個類之間的關係如下所示:

4、總結

(1)橋接模式可以取代多層繼承的方案。多層繼承違背了單一職責原則,復用性較差,類的個數也非常多。橋接模式可以極大的減少子類的個數,從而降低管理和維護的成本。

(2)橋接模式極大的提高了系統可擴展性,在兩個變化維度中任意擴展一個維度買都不需要修改原有的系統,符合開閉原則。

五、組合模式

1、使用組合模式的場景

把部分和整體的關係用樹形結構來表示,從而使客戶端可以使用統一的方式處理部分對象和整體對象。

2、組合模式核心

(1)抽象構件角色:定義了葉子和容器構件的共同點

(2)葉子構件角色:無子節點

(3)容器構件角色:有容器特徵,可以包含子節點,一般容器構件中會擁有一個list容器,進行存儲所有的葉子節點,並且在查找文件的時候使用遍歷該list的方法進行搜索。

3、組合模式工作流程分析

(1)組合模式為處理樹形結構提供了完美的解決方案,描述了如何將容器和葉子進行遞歸組合,使得用戶在使用時可以一致性的對待容器和葉子。

(2)當容器對象的而制定方法被調用時,將遍歷整個樹形結構,尋找也包含這個方法的成員,並調用執行。其中,使用了遞歸調用的機制對整個結構進行處理。

4、使用組合模式,模擬殺毒軟體架構設計

在我們使用殺毒軟體的時候,對每個文件夾下面的每個文件進行殺毒處理時,也屬於樹形結構的處理,一般也是利用組合模式對所有的文件進行處理。我們利用這個背景,來對組合模式進行一個模擬。

(1)首先我們需要建立一個抽象構件,提供一個處理所有文件的方法,然後再建立相應的文件類型(相當於葉子構件)實現抽象構件,最後再建立文件夾(相當於容器構件)存放文件,這樣就可以形成一個樹形結構

public interface AbstractFile {  void killVirus();}  //文本文件殺毒class TextFile implements AbstractFile{  private String name ;  public TextFile(String name) {    super();    this.name = name;  }  @Override  public void killVirus() {    System.out.println("對文本文件:"+name+",進行殺毒!");  }}  //圖片文件class ImgFile implements AbstractFile{  private String name;  public ImgFile(String name) {    super();    this.name = name;  }  @Override  public void killVirus() {    System.out.println("對圖片文件:"+name+",進行殺毒!");  }}  //影片文件class VideoFile implements AbstractFile{  private String name;  public VideoFile(String name) {    super();    this.name = name;  }    @Override  public void killVirus() {    System.out.println("對影片文件:"+name+",進行殺毒!");  }}  //文件夾class Folder implements AbstractFile{  private String name;  private List<AbstractFile> list = new ArrayList<AbstractFile>();     public Folder(String name) {    super();    this.name = name;  }   public void add(AbstractFile a) {    list.add(a);  }  public void remove(AbstractFile a) {    list.remove(a);  }  public AbstractFile getChild(int index) {    return (AbstractFile) list.get(index);    }  @Override  public void killVirus() {    System.out.println("----------開始對文件夾「"+name+"」進行殺毒----------");    for(AbstractFile temp:list) {      temp.killVirus();    }  }}

tips:

1.在模擬這個背景的時候,首先構建了一個AbstractFile介面,在其中定義了一個killVirus方法,充當抽象構件的角色。然後我們建立了三個類:文本文件,圖片文件,影片文件。使用這三個類充當我們組合模式中的葉子節點。最後又創建了一個文件夾類,充當容器構件的角色。

2.在Folder類中,我們在killVirus方法中進行了一個遞歸操作,當文件夾下面擁有文件夾時,會直接進行遞歸操作,再次調用子類文件夾的killVirus方法。

3.在Folder類中,我們使用了一個List容器來存儲Folder中的每一個葉子節點,在遍歷的時候更加方便。

(2)簡單的測試一下

public class Client {  public static void main(String[] args) {    AbstractFile f1,f2,f3,f4;    f1 = new TextFile("歌詞.txt");    f2 = new ImgFile("風景.img");    f3 = new VideoFile("雷神.avi");    f4 = new VideoFile("鋼鐵俠.avi");    Folder f5 = new Folder("漫威電影");    Folder f6 = new Folder("全部文件");    f5.add(f3);    f5.add(f4);    f6.add(f1);    f6.add(f2);    f6.add(f5);        f6.killVirus();  }}

查看結果:

tips:

(1)在程式碼中我們一共創建了4個文件,以及兩個文件夾,依次使用add方法將所有的文件與文件夾進行存儲操作,最後形成一個樹形結構。

(2)通過程式碼我們可以看出,Folder對象f6屬於整個樹形結構的根節點,f1,f2,f3,f4,屬於葉子節點,f5屬於一個容器節點。所以f6中,存放有兩個文件以及一個文件夾。

(3)在最後調用f6的killVirus方法的時候,程式直接將內部的所有文件全部進行了遍歷,這就是組合模式的一種優點整體和局部的操作方法是一樣的。這樣客戶端不論是處理單獨的文件,還是處理文件夾,都是調用killVirus方法,大大簡化了客戶端對不同AbstractFile的處理。