37.設(shè)計(jì)模式(2)結(jié)構(gòu)型

1. 適配器

將一個(gè)類的接口轉(zhuǎn)換成客戶希望的另外一個(gè)接口,使得原本由于接口不兼容而不能一起工作的那些類可以一起工作。

適配器模式是Adapter,也稱Wrapper,是指如果一個(gè)接口需要B接口,但是待傳入的對象卻是A接口。
編寫一個(gè)Adapter的步驟如下:

  • 實(shí)現(xiàn)目標(biāo)接口,這里是Runnable
  • 內(nèi)部持有一個(gè)待轉(zhuǎn)換接口的引用,這里是通過字段持有Callable接口;
  • 在目標(biāo)接口的實(shí)現(xiàn)方法內(nèi)部,調(diào)用待轉(zhuǎn)換接口的方法。
public class Task implements Callable<Long> {
    private long num;
    public Task(long num) {
        this.num = num;
    }
    public Long call() throws Exception {
        long r = 0;
        for (long n = 1; n <= this.num; n++) {
            r = r + n;
        }
        System.out.println("Result: " + r);
        return r;
    }
}

public class RunnableAdapter implements Runnable {
    // 引用待轉(zhuǎn)換接口:
    private Callable<?> callable;
    public RunnableAdapter(Callable<?> callable) {
        this.callable = callable;
    }
    // 實(shí)現(xiàn)指定接口:
    public void run() {
        // 將指定接口調(diào)用委托給轉(zhuǎn)換接口調(diào)用:
        try {
            callable.call();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

Callable<Long> callable = new Task(123450000L);
Thread thread = new Thread(new RunnableAdapter(callable));
thread.start();

適配器模式在Java標(biāo)準(zhǔn)庫中有廣泛應(yīng)用。比如我們持有數(shù)據(jù)類型是String[],但是需要List接口時(shí),可以用一個(gè)Adapter

String[] exist = new String[] {"Good", "morning", "Bob", "and", "Alice"};
Set<String> set = new HashSet<>(Arrays.asList(exist));

2. 橋接

將抽象部分與它的實(shí)現(xiàn)部分分離,使它們都可以獨(dú)立地變化。(為了避免直接繼承帶來的子類爆炸)

示例:假設(shè)某個(gè)汽車廠商生產(chǎn)三種品牌的汽車:Big、Tiny和Boss,每種品牌又可以選擇燃油、純電和混合動(dòng)力。
如果用傳統(tǒng)的繼承來表示各個(gè)最終車型,一共有3個(gè)抽象類加9個(gè)最終子類:

                   ┌───────┐
                   │  Car  │
                   └───────┘
                       ▲
    ┌──────────────────┼───────────────────┐
    │                  │                   │
┌───────┐          ┌───────┐          ┌───────┐
│BigCar │          │TinyCar│          │BossCar│
└───────┘          └───────┘          └───────┘
    ▲                  ▲                  ▲
    │                  │                  │
    │ ┌───────────────┐│ ┌───────────────┐│ ┌───────────────┐
    ├─│  BigFuelCar   │├─│  TinyFuelCar  │├─│  BossFuelCar  │
    │ └───────────────┘│ └───────────────┘│ └───────────────┘
    │ ┌───────────────┐│ ┌───────────────┐│ ┌───────────────┐
    ├─│BigElectricCar │├─│TinyElectricCar│├─│BossElectricCar│
    │ └───────────────┘│ └───────────────┘│ └───────────────┘
    │ ┌───────────────┐│ ┌───────────────┐│ ┌───────────────┐
    └─│ BigHybridCar  │└─│ TinyHybridCar │└─│ BossHybridCar │
      └───────────────┘  └───────────────┘  └───────────────┘

如果要新增一個(gè)品牌,或者加一個(gè)新的引擎(比如核動(dòng)力),那么子類的數(shù)量增長更快。
用橋接就可以避免因直接繼承帶來的子類爆炸。

  • 首先定義抽象類Car,它引用一個(gè)Engine
public abstract class Car {
    // 引用Engine:
    protected Engine engine;
    public Car(Engine engine) {
        this.engine = engine;
    }
    public abstract void drive();
}

Engine接口的定義如下:

public interface Engine {
    void start();
}
  • 在一個(gè)“修正”的抽象類RefinedCar中定義一些額外操作:
public abstract class RefinedCar extends Car {
    public RefinedCar(Engine engine) {
        super(engine);
    }
    public void drive() {
        this.engine.start();
        System.out.println("Drive " + getBrand() + " car...");
    }
    public abstract String getBrand();
}
  • 最終的不同品牌繼承自RefinedCar,例如BossCar
public class BossCar extends RefinedCar {
    public BossCar(Engine engine) {
        super(engine);
    }
    public String getBrand() {
        return "Boss";
    }
}
  • 而針對每一種引擎,繼承自Engine,例如HybridEngine
public class HybridEngine implements Engine {
    public void start() {
        System.out.println("Start Hybrid Engine...");
    }
}
  • 客戶端通過自己選擇一個(gè)品牌,再配合一種引擎,得到最終的Car:
RefinedCar car = new BossCar(new HybridEngine());
car.drive();

這樣品牌和引擎都可以獨(dú)立地變化。結(jié)構(gòu)如下:

       ┌───────────┐
       │    Car    │
       └───────────┘
             ▲
             │
       ┌───────────┐       ┌─────────┐
       │RefinedCar │ ─ ─ ─>│ Engine  │
       └───────────┘       └─────────┘
             ▲                  ▲
    ┌────────┼────────┐         │ ┌──────────────┐
    │        │        │         ├─│  FuelEngine  │
┌───────┐┌───────┐┌───────┐     │ └──────────────┘
│BigCar ││TinyCar││BossCar│     │ ┌──────────────┐
└───────┘└───────┘└───────┘     ├─│ElectricEngine│
                                │ └──────────────┘
                                │ ┌──────────────┐
                                └─│ HybridEngine │
                                  └──────────────┘

3. 組合

將對象組合成樹形結(jié)構(gòu)以表示“部分-整體”的層次結(jié)構(gòu),使得用戶對單個(gè)對象和組合對象的使用具有一致性。

組合模式(Composite)經(jīng)常用于樹形結(jié)構(gòu),為了簡化代碼,使用Composite可以把一個(gè)葉子節(jié)點(diǎn)與一個(gè)父節(jié)點(diǎn)統(tǒng)一起來處理。
舉例:在XML或HTML中,從根節(jié)點(diǎn)開始,每個(gè)節(jié)點(diǎn)都可能包含任意個(gè)其他節(jié)點(diǎn),這些層層嵌套的節(jié)點(diǎn)就構(gòu)成了一顆樹。

  • 先抽象出節(jié)點(diǎn)類型Node:
public interface Node {
    // 添加一個(gè)節(jié)點(diǎn)為子節(jié)點(diǎn):
    Node add(Node node);
    // 獲取子節(jié)點(diǎn):
    List<Node> children();
    // 輸出為XML:
    String toXml();
}
  • 對于一個(gè)<abc></abc>這樣的節(jié)點(diǎn),我們稱之為ElementNode,它可以作為容器包含多個(gè)子節(jié)點(diǎn):
public class ElementNode implements Node {
    private String name;
    private List<Node> list = new ArrayList<>();
    public ElementNode(String name) {
        this.name = name;
    }
    public Node add(Node node) {
        list.add(node);
        return this;
    }
    public List<Node> children() {
        return list;
    }
    public String toXml() {
        String start = "<" + name + ">\n";
        String end = "</" + name + ">\n";
        StringJoiner sj = new StringJoiner("", start, end);
        list.forEach(node -> {
            sj.add(node.toXml() + "\n");// 循環(huán)子階段
        });
        return sj.toString();
    }
}
  • 對于普通文本,我們把它看作TextNode,它沒有子節(jié)點(diǎn):
public class TextNode implements Node {
    private String text;
    public TextNode(String text) {
        this.text = text;
    }
    public Node add(Node node) {
        throw new UnsupportedOperationException();
    }
    public List<Node> children() {
        return List.of();
    }
    public String toXml() {
        return text;
    }
}
  • 還可以有注釋節(jié)點(diǎn):
public class CommentNode implements Node {
    private String text;
    public CommentNode(String text) {
        this.text = text;
    }
    public Node add(Node node) {
        throw new UnsupportedOperationException();
    }
    public List<Node> children() {
        return List.of();
    }
    public String toXml() {
        return "<!-- " + text + " -->";
    }
}
  • 通過ElementNode、TextNodeCommentNode,我們就可以構(gòu)造出一顆樹:
Node root = new ElementNode("school");
root.add(new ElementNode("classA")
        .add(new TextNode("Tom"))
        .add(new TextNode("Alice")));
root.add(new ElementNode("classB")
        .add(new TextNode("Bob"))
        .add(new TextNode("Grace"))
        .add(new CommentNode("comment...")));
System.out.println(root.toXml());
  • 輸出的XML如下
<school>
<classA>
Tom
Alice
</classA>
<classB>
Bob
Grace
<!-- comment... -->
</classB>
</school>

使用Composite模式時(shí),需要先統(tǒng)一單個(gè)節(jié)點(diǎn)以及“容器”節(jié)點(diǎn)的接口:

             ┌───────────┐
             │   Node    │
             └───────────┘
                   ▲
      ┌────────────┼────────────┐
      │            │            │
┌───────────┐┌───────────┐┌───────────┐
│ElementNode││ TextNode  ││CommentNode│
└───────────┘└───────────┘└───────────┘

4. 裝飾器

動(dòng)態(tài)地給一個(gè)對象添加一些額外的職責(zé)。就增加功能來說,相比生成子類更為靈活。

裝飾器(Decorator)模式,是一種在運(yùn)行期動(dòng)態(tài)給某個(gè)對象的實(shí)例增加功能的方法。

在Java標(biāo)準(zhǔn)庫中,InputStream是抽象類,FileInputStream、ServletInputStreamSocket.getInputStream()這些InputStream都是最終數(shù)據(jù)源。
如果要給不同的最終數(shù)據(jù)源增加緩沖功能、計(jì)算簽名功能、加密解密功能,那么,3個(gè)最終數(shù)據(jù)源、3種功能一共需要9個(gè)子類。如果繼續(xù)增加最終數(shù)據(jù)源,或者增加新功能,子類會爆炸式增長,這種設(shè)計(jì)方式顯然是不可取的。

  • FileInputStream增加緩沖和解壓縮功能,用Decorator模式寫出來如下:
// 創(chuàng)建原始的數(shù)據(jù)源:
InputStream fis = new FileInputStream("test.gz");
// 增加緩沖功能:
InputStream bis = new BufferedInputStream(fis);
// 增加解壓縮功能:
InputStream gis = new GZIPInputStream(bis);

InputStream input = new GZIPInputStream( // 第二層裝飾
                        new BufferedInputStream( // 第一層裝飾
                            new FileInputStream("test.gz") // 核心功能
                        ));

觀察BufferedInputStreamGZIPInputStream,它們實(shí)際上都是從FilterInputStream繼承的,這個(gè)FilterInputStream就是一個(gè)抽象的Decorator。我們用圖把Decorator模式畫出來如下:

             ┌───────────┐
             │ Component │
             └───────────┘
                   ▲
      ┌────────────┼─────────────────┐
      │            │                 │
┌───────────┐┌───────────┐     ┌───────────┐
│ComponentA ││ComponentB │...  │ Decorator │
└───────────┘└───────────┘     └───────────┘
                                     ▲
                              ┌──────┴──────┐
                              │             │
                        ┌───────────┐ ┌───────────┐
                        │DecoratorA │ │DecoratorB │...
                        └───────────┘ └───────────┘

最頂層的Component是接口,對應(yīng)到IO的就是InputStream這個(gè)抽象類。ComponentA、ComponentB是實(shí)際的子類,對應(yīng)到IO的就是FileInputStreamServletInputStream這些數(shù)據(jù)源。Decorator是用于實(shí)現(xiàn)各個(gè)附加功能的抽象裝飾器,對應(yīng)到IO的就是FilterInputStream。而從Decorator派生的就是一個(gè)一個(gè)的裝飾器,它們每個(gè)都有獨(dú)立的功能,對應(yīng)到IO的就是BufferedInputStream、GZIPInputStream等。

5. 外觀

為子系統(tǒng)中的一組接口提供一個(gè)一致的界面。Facade模式定義了一個(gè)高層接口,這個(gè)接口使得這一子系統(tǒng)更加容易使用。

外觀模式,即Facade,是一個(gè)比較簡單的模式。它的基本思想如下:
如果客戶端要跟許多子系統(tǒng)打交道,那么客戶端需要了解各個(gè)子系統(tǒng)的接口,比較麻煩。如果有一個(gè)統(tǒng)一的“中介”,讓客戶端只跟中介打交道,中介再去跟各個(gè)子系統(tǒng)打交道,對客戶端來說就比較簡單。所以Facade就相當(dāng)于搞了一個(gè)中介。

我們以注冊公司為例,假設(shè)注冊公司需要三步:

  • 向工商局申請公司營業(yè)執(zhí)照;
  • 在銀行開設(shè)賬戶;
  • 在稅務(wù)局開設(shè)納稅號。

以下是三個(gè)系統(tǒng)的接口:

// 工商注冊:
public class AdminOfIndustry {
    public Company register(String name) {
        ...
    }
}
// 銀行開戶:
public class Bank {
    public String openAccount(String companyId) {
        ...
    }
}
// 納稅登記:
public class Taxation {
    public String applyTaxCode(String companyId) {
        ...
    }
}

如果子系統(tǒng)比較復(fù)雜,并且客戶對流程也不熟悉,那就把這些流程全部委托給中介:

public class Facade {
    public Company openCompany(String name) {
        Company c = this.admin.register(name);
        String bankAccount = this.bank.openAccount(c.getId());
        c.setBankAccount(bankAccount);
        String taxCode = this.taxation.applyTaxCode(c.getId());
        c.setTaxCode(taxCode);
        return c;
    }
}

Company c = facade.openCompany("Facade Software Ltd.");

6. 享元

運(yùn)用共享技術(shù)有效地支持大量細(xì)粒度的對象。

享元(Flyweight)的核心思想很簡單:如果一個(gè)對象實(shí)例一經(jīng)創(chuàng)建就不可變,那么反復(fù)創(chuàng)建相同的實(shí)例就沒有必要,直接向調(diào)用方返回一個(gè)共享的實(shí)例就行,這樣即節(jié)省內(nèi)存,又可以減少創(chuàng)建對象的過程,提高運(yùn)行速度。

public class Student {
    // 持有緩存:
    private static final Map<String, Student> cache = new HashMap<>();
    // 靜態(tài)工廠方法:
    public static Student create(int id, String name) {
        String key = id + "\n" + name;
        // 先查找緩存:
        Student std = cache.get(key);
        if (std == null) {
            // 未找到,創(chuàng)建新對象:
            System.out.println(String.format("create new Student(%s, %s)", id, name));
            std = new Student(id, name);
            // 放入緩存:
            cache.put(key, std);
        } else {
            // 緩存中存在:
            System.out.println(String.format("return cached Student(%s, %s)", std.id, std.name));
        }
        return std;
    }
    private final int id;
    private final String name;
    public Student(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

Java的Integer.valueOf()Byte.valueOf()都是使用的享元模式。

7. 代理

為其他對象提供一種代理以控制對這個(gè)對象的訪問。

代理模式,即Proxy,它和適配器(Adapter)模式很類似。

  • Adapter模式,它用于把A接口轉(zhuǎn)換為B接口
  • Proxy模式,還是轉(zhuǎn)換成A接口
public class AProxy implements A {
    private A a;
    public AProxy(A a) {
        this.a = a;
    }
    public void a() {
        if (getCurrentUser().isRoot()) {
            this.a.a();
        } else {
            throw new SecurityException("Forbidden");
        }
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • """1.個(gè)性化消息: 將用戶的姓名存到一個(gè)變量中,并向該用戶顯示一條消息。顯示的消息應(yīng)非常簡單,如“Hello ...
    她即我命閱讀 4,899評論 0 6
  • 為了讓我有一個(gè)更快速、更精彩、更輝煌的成長,我將開始這段刻骨銘心的自我蛻變之旅!從今天開始,我將每天堅(jiān)持閱...
    李薇帆閱讀 2,232評論 1 4
  • 似乎最近一直都在路上,每次出來走的時(shí)候感受都會很不一樣。 1、感恩一直遇到好心人,很幸運(yùn)。在路上總是...
    時(shí)間里的花Lily閱讀 1,717評論 1 3
  • 1、expected an indented block 冒號后面是要寫上一定的內(nèi)容的(新手容易遺忘這一點(diǎn)); 縮...
    庵下桃花仙閱讀 1,064評論 1 2
  • 一、工具箱(多種工具共用一個(gè)快捷鍵的可同時(shí)按【Shift】加此快捷鍵選取)矩形、橢圓選框工具 【M】移動(dòng)工具 【V...
    墨雅丫閱讀 1,458評論 0 0

友情鏈接更多精彩內(nèi)容