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、TextNode和CommentNode,我們就可以構(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、ServletInputStream、Socket.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") // 核心功能
));
觀察BufferedInputStream和GZIPInputStream,它們實(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的就是FileInputStream、ServletInputStream這些數(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");
}
}
}