設計模式-結構型模式
閱讀推薦:設計模式-簡單篇
專案地址:https://gitee.com/zwtgit/gof23
學習網站推薦:
設計模式是針對軟體設計中常見問題的工具箱, 其中的工具就是各種經過實踐驗證的解決方案。 即使你從未遇到過這些問題, 瞭解模式仍然非常有用, 因為它能指導你如何使用面向物件的設計原則來解決各種問題。
演算法更像是菜譜: 提供達成目標的明確步驟。 而模式更像是藍圖: 你可以看到最終的結果和模式的功能, 但需要自己確定實現步驟。
GoF 23(分類)
- 建立型模式:提供建立物件的機制, 增加已有程式碼的靈活性和可複用性。
- 結構型模式:介紹如何將物件和類組裝成較大的結構, 並同時保持結構的靈活和高效。
- 行為模式:負責物件間的高效溝通和職責委派。
不同設計模式的複雜程度、 細節層次以及在整個系統中的應用範圍等方面各不相同。 我喜歡將其類比於道路的建造: 如果你希望讓十字路口更加安全, 那麼可以安裝一些交通訊號燈, 或者修建包含行人地下通道在內的多層互通式立交橋。 最基礎的、 底層的模式通常被稱為慣用技巧。 這類模式一般只能在一種程式語言中使用。 最通用的、 高層的模式是構架模式。 開發者可以在任何程式語言中使用這類模式。 與其他模式不同, 它們可用於整個應用程式的架構設計。
- 建立型模式:
- 單例模式,工廠模式, 抽象工廠模式, 建造者模式, 原型模式
- 結構型模式:
- 介面卡模式, 橋接模式, 裝飾模式, 組合模式, 外觀模式, 享元模式, 代理模式
- 行為型模式:
- 模板方法模式, 命令模式, 迭代器模式, 觀察者模式, 中介者模式, 備忘錄模式, 直譯器模式, 狀態模式, 策略模式, 職責鏈模式, 訪問者模式
OOP 七大原則
面向物件程式設計(Object Oriented Programming,OOP)。
1、開閉原則
對擴充套件開放, 對修改關閉。
2、單一職責原則
每個類應該實現單一的職責,不要存在多於一個導致類變更的原因,否則就應該把類拆分。該原則是實現高內聚、低耦合的指導方針。
3、里氏替換原則(Liskov Substitution Principle)
任何基類可以出現的地方,子類一定可以出現。里氏替換原則是繼承複用的基石,只有當衍生類可以替換基類,軟體單位的功能不受到影響時,基類才能真正被複用,而衍生類也能夠在基類的基礎上增加新的行為。
里氏替換原則是對開閉原則的補充。實現開閉原則的關鍵就是抽象化。而基類與子類的繼承關係就是抽象化的具體實現,所以里氏替換原則是對實現抽象化的具體步驟的規範。里氏替換原則中,子類對父類的方法儘量不要重寫和過載。因為父類代表了定義好的結構,通過這個規範的介面與外界互動,子類不應該隨便破壞它。
4、依賴倒轉原則(Dependence Inversion Principle)
面向介面程式設計,依賴於抽象而不依賴於具體。用到具體類時,不與具體類互動,而與具體類的上層介面互動。
5、介面隔離原則(Interface Segregation Principle)
每個介面中不存在子類用不到卻必須實現的方法,否則就要將介面拆分。使用多個隔離的介面,比使用單個介面(多個介面中的方法聚合到一個的介面)要好。
6、迪米特法則(最少知道原則)(Demeter Principle)
一個類對自己依賴的類知道的越少越好。無論被依賴的類多麼複雜,都應該將邏輯封裝在方法的內部,通過 public 方法提供給外部。這樣當被依賴的類變化時,才能最小的影響該類。
7、合成複用原則(Composite Reuse Principle)
軟體複用時,要先儘量使用組合或者聚合等關聯關係實現,其次才考慮使用繼承。即在一個新物件裡通過關聯的方式使用已有物件的一些方法和功能。
StructuralPatterns
Adapter
背景
介面卡模式是一種結構型設計模式, 它能使介面不相容的物件能夠相互合作。
假如你正在開發一款股票市場監測程式,
它會從不同來源下載 XML 格式的股票資料, 然後向用戶呈現出美觀的圖表。
在開發過程中, 你決定在程式中整合一個第三方智慧分析函式庫。
但是遇到了一個問題, 那就是分析函式庫只相容 JSON 格式的資料。
你可以修改程式庫來支援 XML。
但是, 這可能需要修改部分依賴該程式庫的現有程式碼。
甚至還有更糟糕的情況, 你可能根本沒有程式庫的原始碼, 從而無法對其進行修改。
你可以建立一個介面卡。 這是一個特殊的物件, 能夠轉換物件介面, 使其能與其他物件進行互動。
介面卡模式通過封裝物件將複雜的轉換過程隱藏於幕後。 被封裝的物件甚至察覺不到介面卡的存在。
物件介面卡:實現時使用了構成原則: 介面卡實現了其中一個物件的介面, 並對另一個物件進行封裝。
類介面卡: 這一實現使用了繼承機制: 介面卡同時繼承兩個物件的介面。
適用場景
- 當你希望使用某個類, 但是其介面與其他程式碼不相容時, 可以使用介面卡類。
- 如果您需要複用這樣一些類, 他們處於同一個繼承體系, 並且他們又有了額外的一些共同的方法, 但是這些共同的方法不是所有在這一繼承體系中的子類所具有的共性。
例項-實現
使用示例: 介面卡模式在 Java 程式碼中很常見。 基於一些遺留程式碼的系統常常會使用該模式。 在這種情況下, 介面卡讓遺留程式碼與現代的類得以相互合作。
Java 核心程式庫中有一些標準的介面卡:
java.util.Arrays#asList()
java.util.Collections#list()
java.util.Collections#enumeration()
java.io.InputStreamReader(InputStream)
(返回Reader
物件)java.io.OutputStreamWriter(OutputStream)
(返回Writer
物件)javax.xml.bind.annotation.adapters.XmlAdapter#marshal()
和#unmarshal()
識別方法: 介面卡可以通過以不同抽象或介面型別例項為引數的建構函式來識別。 當介面卡的任何方法被呼叫時, 它會將引數轉換為合適的格式, 然後將呼叫定向到其封裝物件中的一個或多個方法。
讓方釘適配圓孔
這個簡單的例子展示了介面卡如何讓不相容的物件相互合作。
round
round/RoundHole.java: 圓孔
/**
* RoundHoles are compatible with RoundPegs.
*/
public class RoundHole {
private double radius;
public RoundHole(double radius) {
this.radius = radius;
}
public double getRadius() {
return radius;
}
public boolean fits(RoundPeg peg) {
boolean result;
result = (this.getRadius() >= peg.getRadius());
return result;
}
}
round/RoundPeg.java: 圓釘
/**
* RoundPegs are compatible with RoundHoles.
*/
public class RoundPeg {
private double radius;
public RoundPeg() {}
public RoundPeg(double radius) {
this.radius = radius;
}
public double getRadius() {
return radius;
}
}
square
square/SquarePeg.java: 方釘
/**
* SquarePegs are not compatible with RoundHoles (they were implemented by
* previous development team). But we have to integrate them into our program.
*/
public class SquarePeg {
private double width;
public SquarePeg(double width) {
this.width = width;
}
public double getWidth() {
return width;
}
public double getSquare() {
double result;
result = Math.pow(this.width, 2);
return result;
}
}
adapters
adapters/SquarePegAdapter.java: 方釘到圓孔的介面卡
/**
* Adapter allows fitting square pegs into round holes.
*/
public class SquarePegAdapter extends RoundPeg {
private SquarePeg peg;
public SquarePegAdapter(SquarePeg peg) {
this.peg = peg;
}
@Override
public double getRadius() {
double result;
// Calculate a minimum circle radius, which can fit this peg.
result = (Math.sqrt(Math.pow((peg.getWidth() / 2), 2) * 2));
return result;
}
}
Demo.java: 客戶端程式碼
/**
* Somewhere in client code...
*/
public class Demo {
public static void main(String[] args) {
// Round fits round, no surprise.
RoundHole hole = new RoundHole(5);
RoundPeg rpeg = new RoundPeg(5);
if (hole.fits(rpeg)) {
System.out.println("Round peg r5 fits round hole r5.");
}
SquarePeg smallSqPeg = new SquarePeg(2);
SquarePeg largeSqPeg = new SquarePeg(20);
// hole.fits(smallSqPeg); // Won't compile.
// Adapter solves the problem.
SquarePegAdapter smallSqPegAdapter = new SquarePegAdapter(smallSqPeg);
SquarePegAdapter largeSqPegAdapter = new SquarePegAdapter(largeSqPeg);
if (hole.fits(smallSqPegAdapter)) {
System.out.println("Square peg w2 fits round hole r5.");
}
if (!hole.fits(largeSqPegAdapter)) {
System.out.println("Square peg w20 does not fit into round hole r5.");
}
}
}
OutputDemo.txt: 執行結果
Round peg r5 fits round hole r5.
Square peg w2 fits round hole r5.
Square peg w20 does not fit into round hole r5.
與其他模式的關係
- 橋接模式通常會於開發前期進行設計, 使你能夠將程式的各個部分獨立開來以便開發。 另一方面, 介面卡模式通常在已有程式中使用, 讓相互不相容的類能很好地合作。
- 介面卡可以對已有物件的介面進行修改, 裝飾模式則能在不改變物件介面的前提下強化物件功能。 此外, 裝飾還支援遞迴組合, 介面卡則無法實現。
- 介面卡能為被封裝物件提供不同的介面, 代理模式能為物件提供相同的介面, 裝飾則能為物件提供加強的介面。
- 外觀模式為現有物件定義了一個新介面, 介面卡則會試圖運用已有的介面。 介面卡通常只封裝一個物件, 外觀通常會作用於整個物件子系統上。
- 橋接、 狀態模式和策略模式 (在某種程度上包括介面卡) 模式的介面非常相似。 實際上, 它們都基於組合模式——即將工作委派給其他物件, 不過也各自解決了不同的問題。 模式並不只是以特定方式組織程式碼的配方, 你還可以使用它們來和其他開發者討論模式所解決的問題。
Bridge
背景
橋接模式是一種結構型設計模式, 可將一個大類或一系列緊密相關的類拆分為抽象和實現兩個獨立的層次結構, 從而能在開發時分別使用。
適用場景
- 如果你想要拆分或重組一個具有多重功能的龐雜類 (例如能與多個數據庫伺服器進行互動的類), 可以使用橋接模式。
- 如果你希望在幾個獨立維度上擴充套件一個類, 可使用該模式。
- 如果你需要在執行時切換不同實現方法, 可使用橋接模式。
例項-實現
使用示例: 橋接模式在處理跨平臺應用、 支援多種型別的資料庫伺服器或與多個特定種類 (例如雲平臺和社交網路等) 的 API 供應商協作時會特別有用。
識別方法: 橋接可以通過一些控制實體及其所依賴的多個不同平臺之間的明確區別來進行識別。
裝置和遠端控制之間的橋接
本例展示了遠端控制器及其所控制的裝置的類之間的分離。
遠端控制器是抽象部分, 裝置則是其實現部分。 由於有通用的介面, 同一遠端控制器可與不同的裝置合作, 反過來也一樣。
橋接模式允許在不改動另一層次程式碼的前提下修改已有類, 甚至建立新類。
devices
devices/Device.java: 所有裝置的通用介面
public interface Device {
boolean isEnabled();
void enable();
void disable();
int getVolume();
void setVolume(int percent);
int getChannel();
void setChannel(int channel);
void printStatus();
}
devices/Radio.java: 收音機
public class Radio implements Device {
private boolean on = false;
private int volume = 30;
private int channel = 1;
@Override
public boolean isEnabled() {
return on;
}
@Override
public void enable() {
on = true;
}
@Override
public void disable() {
on = false;
}
@Override
public int getVolume() {
return volume;
}
@Override
public void setVolume(int volume) {
if (volume > 100) {
this.volume = 100;
} else if (volume < 0) {
this.volume = 0;
} else {
this.volume = volume;
}
}
@Override
public int getChannel() {
return channel;
}
@Override
public void setChannel(int channel) {
this.channel = channel;
}
@Override
public void printStatus() {
System.out.println("------------------------------------");
System.out.println("| I'm radio.");
System.out.println("| I'm " + (on ? "enabled" : "disabled"));
System.out.println("| Current volume is " + volume + "%");
System.out.println("| Current channel is " + channel);
System.out.println("------------------------------------\n");
}
}
devices/Tv.java: 電視機
public class Tv implements Device {
private boolean on = false;
private int volume = 30;
private int channel = 1;
@Override
public boolean isEnabled() {
return on;
}
@Override
public void enable() {
on = true;
}
@Override
public void disable() {
on = false;
}
@Override
public int getVolume() {
return volume;
}
@Override
public void setVolume(int volume) {
if (volume > 100) {
this.volume = 100;
} else if (volume < 0) {
this.volume = 0;
} else {
this.volume = volume;
}
}
@Override
public int getChannel() {
return channel;
}
@Override
public void setChannel(int channel) {
this.channel = channel;
}
@Override
public void printStatus() {
System.out.println("------------------------------------");
System.out.println("| I'm TV set.");
System.out.println("| I'm " + (on ? "enabled" : "disabled"));
System.out.println("| Current volume is " + volume + "%");
System.out.println("| Current channel is " + channel);
System.out.println("------------------------------------\n");
}
}
remotes
remotes/Remote.java: 所有遠端控制器的通用介面
public interface Remote {
void power();
void volumeDown();
void volumeUp();
void channelDown();
void channelUp();
}
remotes/BasicRemote.java: 基礎遠端控制器
public class BasicRemote implements Remote {
protected Device device;
public BasicRemote() {}
public BasicRemote(Device device) {
this.device = device;
}
@Override
public void power() {
System.out.println("Remote: power toggle");
if (device.isEnabled()) {
device.disable();
} else {
device.enable();
}
}
@Override
public void volumeDown() {
System.out.println("Remote: volume down");
device.setVolume(device.getVolume() - 10);
}
@Override
public void volumeUp() {
System.out.println("Remote: volume up");
device.setVolume(device.getVolume() + 10);
}
@Override
public void channelDown() {
System.out.println("Remote: channel down");
device.setChannel(device.getChannel() - 1);
}
@Override
public void channelUp() {
System.out.println("Remote: channel up");
device.setChannel(device.getChannel() + 1);
}
}
remotes/AdvancedRemote.java: 高階遠端控制器
public class AdvancedRemote extends BasicRemote {
public AdvancedRemote(Device device) {
super.device = device;
}
public void mute() {
System.out.println("Remote: mute");
device.setVolume(0);
}
}
Demo.java: 客戶端程式碼
public class Demo {
public static void main(String[] args) {
testDevice(new Tv());
testDevice(new Radio());
}
public static void testDevice(Device device) {
System.out.println("Tests with basic remote.");
BasicRemote basicRemote = new BasicRemote(device);
basicRemote.power();
device.printStatus();
System.out.println("Tests with advanced remote.");
AdvancedRemote advancedRemote = new AdvancedRemote(device);
advancedRemote.power();
advancedRemote.mute();
device.printStatus();
}
}
OutputDemo.txt: 執行結果
Tests with basic remote.
Remote: power toggle
------------------------------------
| I'm TV set.
| I'm enabled
| Current volume is 30%
| Current channel is 1
------------------------------------
Tests with advanced remote.
Remote: power toggle
Remote: mute
------------------------------------
| I'm TV set.
| I'm disabled
| Current volume is 0%
| Current channel is 1
------------------------------------
Tests with basic remote.
Remote: power toggle
------------------------------------
| I'm radio.
| I'm enabled
| Current volume is 30%
| Current channel is 1
------------------------------------
Tests with advanced remote.
Remote: power toggle
Remote: mute
------------------------------------
| I'm radio.
| I'm disabled
| Current volume is 0%
| Current channel is 1
------------------------------------
與其他模式的關係
- 橋接模式通常會於開發前期進行設計, 使你能夠將程式的各個部分獨立開來以便開發。 另一方面, 介面卡模式通常在已有程式中使用, 讓相互不相容的類能很好地合作。
- 橋接、 狀態模式和策略模式 (在某種程度上包括介面卡) 模式的介面非常相似。 實際上, 它們都基於組合模式——即將工作委派給其他物件, 不過也各自解決了不同的問題。 模式並不只是以特定方式組織程式碼的配方, 你還可以使用它們來和其他開發者討論模式所解決的問題。
- 你可以將抽象工廠模式和橋接搭配使用。 如果由橋接定義的抽象只能與特定實現合作, 這一模式搭配就非常有用。 在這種情況下, 抽象工廠可以對這些關係進行封裝, 並且對客戶端程式碼隱藏其複雜性。
- 你可以結合使用生成器模式和橋接模式: 主管類負責抽象工作, 各種不同的生成器負責實現工作。
Composite
組合模式是一種結構型設計模式, 你可以使用它將物件組合成樹狀結構, 並且能像使用獨立物件一樣使用它們。
背景
該方式的最大優點在於你無需瞭解構成樹狀結構的物件的具體類。 你也無需瞭解物件是簡單的產品還是複雜的盒子。 你只需呼叫通用介面以相同的方式對其進行處理即可。 當你呼叫該方法後, 物件會將請求沿著樹結構傳遞下去。
適用場景
- 如果你需要實現樹狀物件結構, 可以使用組合模式。
- 如果你希望客戶端程式碼以相同方式處理簡單和複雜元素, 可以使用該模式 。
例項-實現
使用例項: 組合模式在 Java 程式碼中很常見,常用於表示與圖形打交道的使用者介面元件或程式碼的層次結構。
下面是一些來自 Java 標準程式庫中的組合示例:
java.awt.Container#add(Component)
(幾乎廣泛存在於 Swing 元件中)javax.faces.component.UIComponent#getChildren()
(幾乎廣泛存在於 JSF UI 元件中)
識別方法: 組合可以通過將同一抽象或介面型別的例項放入樹狀結構的行為方法來輕鬆識別。
簡單和複合圖形
本例展示瞭如何利用較為簡單的形狀來組成複雜圖形, 以及如何統一處理簡單和複雜圖形。
shapes
shapes/Shape.java: 通用形狀介面
import java.awt.*;
public interface Shape {
int getX();
int getY();
int getWidth();
int getHeight();
void move(int x, int y);
boolean isInsideBounds(int x, int y);
void select();
void unSelect();
boolean isSelected();
void paint(Graphics graphics);
}
shapes/BaseShape.java: 提供基本功能的抽象形狀
import java.awt.*;
abstract class BaseShape implements Shape {
public int x;
public int y;
public Color color;
private boolean selected = false;
BaseShape(int x, int y, Color color) {
this.x = x;
this.y = y;
this.color = color;
}
@Override
public int getX() {
return x;
}
@Override
public int getY() {
return y;
}
@Override
public int getWidth() {
return 0;
}
@Override
public int getHeight() {
return 0;
}
@Override
public void move(int x, int y) {
this.x += x;
this.y += y;
}
@Override
public boolean isInsideBounds(int x, int y) {
return x > getX() && x < (getX() + getWidth()) &&
y > getY() && y < (getY() + getHeight());
}
@Override
public void select() {
selected = true;
}
@Override
public void unSelect() {
selected = false;
}
@Override
public boolean isSelected() {
return selected;
}
void enableSelectionStyle(Graphics graphics) {
graphics.setColor(Color.LIGHT_GRAY);
Graphics2D g2 = (Graphics2D) graphics;
float dash1[] = {2.0f};
g2.setStroke(new BasicStroke(1.0f,
BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER,
2.0f, dash1, 0.0f));
}
void disableSelectionStyle(Graphics graphics) {
graphics.setColor(color);
Graphics2D g2 = (Graphics2D) graphics;
g2.setStroke(new BasicStroke());
}
@Override
public void paint(Graphics graphics) {
if (isSelected()) {
enableSelectionStyle(graphics);
}
else {
disableSelectionStyle(graphics);
}
// ...
}
}
shapes/Dot.java: 點
import java.awt.*;
public class Dot extends BaseShape {
private final int DOT_SIZE = 3;
public Dot(int x, int y, Color color) {
super(x, y, color);
}
@Override
public int getWidth() {
return DOT_SIZE;
}
@Override
public int getHeight() {
return DOT_SIZE;
}
@Override
public void paint(Graphics graphics) {
super.paint(graphics);
graphics.fillRect(x - 1, y - 1, getWidth(), getHeight());
}
}
shapes/Circle.java: 圓形
import java.awt.*;
public class Circle extends BaseShape {
public int radius;
public Circle(int x, int y, int radius, Color color) {
super(x, y, color);
this.radius = radius;
}
@Override
public int getWidth() {
return radius * 2;
}
@Override
public int getHeight() {
return radius * 2;
}
public void paint(Graphics graphics) {
super.paint(graphics);
graphics.drawOval(x, y, getWidth() - 1, getHeight() - 1);
}
}
shapes/Rectangle.java: 三角形
import java.awt.*;
public class Rectangle extends BaseShape {
public int width;
public int height;
public Rectangle(int x, int y, int width, int height, Color color) {
super(x, y, color);
this.width = width;
this.height = height;
}
@Override
public int getWidth() {
return width;
}
@Override
public int getHeight() {
return height;
}
@Override
public void paint(Graphics graphics) {
super.paint(graphics);
graphics.drawRect(x, y, getWidth() - 1, getHeight() - 1);
}
}
shapes/CompoundShape.java: 由其他形狀物件組成的複合形狀
import java.awt.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class CompoundShape extends BaseShape {
protected List<Shape> children = new ArrayList<>();
public CompoundShape(Shape... components) {
super(0, 0, Color.BLACK);
add(components);
}
public void add(Shape component) {
children.add(component);
}
public void add(Shape... components) {
children.addAll(Arrays.asList(components));
}
public void remove(Shape child) {
children.remove(child);
}
public void remove(Shape... components) {
children.removeAll(Arrays.asList(components));
}
public void clear() {
children.clear();
}
@Override
public int getX() {
if (children.size() == 0) {
return 0;
}
int x = children.get(0).getX();
for (Shape child : children) {
if (child.getX() < x) {
x = child.getX();
}
}
return x;
}
@Override
public int getY() {
if (children.size() == 0) {
return 0;
}
int y = children.get(0).getY();
for (Shape child : children) {
if (child.getY() < y) {
y = child.getY();
}
}
return y;
}
@Override
public int getWidth() {
int maxWidth = 0;
int x = getX();
for (Shape child : children) {
int childsRelativeX = child.getX() - x;
int childWidth = childsRelativeX + child.getWidth();
if (childWidth > maxWidth) {
maxWidth = childWidth;
}
}
return maxWidth;
}
@Override
public int getHeight() {
int maxHeight = 0;
int y = getY();
for (Shape child : children) {
int childsRelativeY = child.getY() - y;
int childHeight = childsRelativeY + child.getHeight();
if (childHeight > maxHeight) {
maxHeight = childHeight;
}
}
return maxHeight;
}
@Override
public void move(int x, int y) {
for (Shape child : children) {
child.move(x, y);
}
}
@Override
public boolean isInsideBounds(int x, int y) {
for (Shape child : children) {
if (child.isInsideBounds(x, y)) {
return true;
}
}
return false;
}
@Override
public void unSelect() {
super.unSelect();
for (Shape child : children) {
child.unSelect();
}
}
public boolean selectChildAt(int x, int y) {
for (Shape child : children) {
if (child.isInsideBounds(x, y)) {
child.select();
return true;
}
}
return false;
}
@Override
public void paint(Graphics graphics) {
if (isSelected()) {
enableSelectionStyle(graphics);
graphics.drawRect(getX() - 1, getY() - 1, getWidth() + 1, getHeight() + 1);
disableSelectionStyle(graphics);
}
for (refactoring_guru.composite.example.shapes.Shape child : children) {
child.paint(graphics);
}
}
}
editor
editor/ImageEditor.java: 形狀編輯器
import javax.swing.*;
import javax.swing.border.Border;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
public class ImageEditor {
private EditorCanvas canvas;
private CompoundShape allShapes = new CompoundShape();
public ImageEditor() {
canvas = new EditorCanvas();
}
public void loadShapes(Shape... shapes) {
allShapes.clear();
allShapes.add(shapes);
canvas.refresh();
}
private class EditorCanvas extends Canvas {
JFrame frame;
private static final int PADDING = 10;
EditorCanvas() {
createFrame();
refresh();
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
allShapes.unSelect();
allShapes.selectChildAt(e.getX(), e.getY());
e.getComponent().repaint();
}
});
}
void createFrame() {
frame = new JFrame();
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
JPanel contentPanel = new JPanel();
Border padding = BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING);
contentPanel.setBorder(padding);
frame.setContentPane(contentPanel);
frame.add(this);
frame.setVisible(true);
frame.getContentPane().setBackground(Color.LIGHT_GRAY);
}
public int getWidth() {
return allShapes.getX() + allShapes.getWidth() + PADDING;
}
public int getHeight() {
return allShapes.getY() + allShapes.getHeight() + PADDING;
}
void refresh() {
this.setSize(getWidth(), getHeight());
frame.pack();
}
public void paint(Graphics graphics) {
allShapes.paint(graphics);
}
}
}
Demo.java: 客戶端程式碼
import java.awt.*;
public class Demo {
public static void main(String[] args) {
ImageEditor editor = new ImageEditor();
editor.loadShapes(
new Circle(10, 10, 10, Color.BLUE),
new CompoundShape(
new Circle(110, 110, 50, Color.RED),
new Dot(160, 160, Color.RED)
),
new CompoundShape(
new Rectangle(250, 250, 100, 100, Color.GREEN),
new Dot(240, 240, Color.GREEN),
new Dot(240, 360, Color.GREEN),
new Dot(360, 360, Color.GREEN),
new Dot(360, 240, Color.GREEN)
)
);
}
}
與其他模式的關係
-
橋接模式、 狀態模式和策略模式 (在某種程度上包括介面卡模式) 模式的介面非常相似。 實際上, 它們都基於組合模式——即將工作委派給其他物件, 不過也各自解決了不同的問題。 模式並不只是以特定方式組織程式碼的配方, 你還可以使用它們來和其他開發者討論模式所解決的問題。
-
責任鏈模式通常和組合模式結合使用。 在這種情況下, 葉元件接收到請求後, 可以將請求沿包含全體父元件的鏈一直傳遞至物件樹的底部。
-
組合和裝飾模式的結構圖很相似, 因為兩者都依賴遞迴組合來組織無限數量的物件。
裝飾類似於組合, 但其只有一個子元件。 此外還有一個明顯不同: 裝飾為被封裝物件添加了額外的職責, 組合僅對其子節點的結果進行了 “求和”。
但是, 模式也可以相互合作: 你可以使用裝飾來擴充套件組合樹中特定物件的行為。
Decorator
裝飾模式是一種結構型設計模式, 允許你通過將物件放入包含行為的特殊封裝物件中來為原物件繫結新的行為。
背景
適用場景
如果你希望在無需修改程式碼的情況下即可使用物件, 且希望在執行時為物件新增額外的行為, 可以使用裝飾模式。
- 裝飾能將業務邏輯組織為層次結構, 你可為各層建立一個裝飾, 在執行時將各種不同邏輯組合成物件。 由於這些物件都遵循通用介面, 客戶端程式碼能以相同的方式使用這些物件。
如果用繼承來擴充套件物件行為的方案難以實現或者根本不可行, 你可以使用該模式。
- 許多程式語言使用
final
最終關鍵字來限制對某個類的進一步擴充套件。 複用最終類已有行為的唯一方法是使用裝飾模式: 用封裝器對其進行封裝。
例項-實現
使用示例: 裝飾在 Java 程式碼中可謂是標準配置, 尤其是在與流式載入相關的程式碼中。
Java 核心程式庫中有一些關於裝飾的示例:
java.io.InputStream
、OutputStream
、Reader
和Writer
的所有程式碼都有以自身型別的物件作為引數的建構函式。java.util.Collections
;checkedXXX()
、synchronizedXXX()
和unmodifiableXXX()
方法。javax.servlet.http.HttpServletRequestWrapper
和HttpServletResponseWrapper
識別方法: 裝飾可通過以當前類或物件為引數的建立方法或建構函式來識別。
編碼和壓縮裝飾
本例展示瞭如何在不更改物件程式碼的情況下調整其行為。
最初的業務邏輯類僅能讀取和寫入純文字的資料。 此後, 我們建立了幾個小的封裝器類, 以便在執行標準操作後新增新的行為。
第一個封裝器負責加密和解密資料, 而第二個則負責壓縮和解壓資料。
你甚至可以讓這些封裝器巢狀封裝以將它們組合起來。
decorators
decorators/DataSource.java: 定義了讀取和寫入操作的通用資料介面
public interface DataSource {
void writeData(String data);
String readData();
}
decorators/FileDataSource.java: 簡單資料讀寫器
import java.io.*;
public class FileDataSource implements DataSource {
private String name;
public FileDataSource(String name) {
this.name = name;
}
@Override
public void writeData(String data) {
File file = new File(name);
try (OutputStream fos = new FileOutputStream(file)) {
fos.write(data.getBytes(), 0, data.length());
} catch (IOException ex) {
System.out.println(ex.getMessage());
}
}
@Override
public String readData() {
char[] buffer = null;
File file = new File(name);
try (FileReader reader = new FileReader(file)) {
buffer = new char[(int) file.length()];
reader.read(buffer);
} catch (IOException ex) {
System.out.println(ex.getMessage());
}
return new String(buffer);
}
}
decorators/DataSourceDecorator.java: 抽象基礎裝飾
public class DataSourceDecorator implements DataSource {
private DataSource wrappee;
DataSourceDecorator(DataSource source) {
this.wrappee = source;
}
@Override
public void writeData(String data) {
wrappee.writeData(data);
}
@Override
public String readData() {
return wrappee.readData();
}
}
decorators/EncryptionDecorator.java: 加密裝飾
import java.util.Base64;
public class EncryptionDecorator extends DataSourceDecorator {
public EncryptionDecorator(DataSource source) {
super(source);
}
@Override
public void writeData(String data) {
super.writeData(encode(data));
}
@Override
public String readData() {
return decode(super.readData());
}
private String encode(String data) {
byte[] result = data.getBytes();
for (int i = 0; i < result.length; i++) {
result[i] += (byte) 1;
}
return Base64.getEncoder().encodeToString(result);
}
private String decode(String data) {
byte[] result = Base64.getDecoder().decode(data);
for (int i = 0; i < result.length; i++) {
result[i] -= (byte) 1;
}
return new String(result);
}
}
decorators/CompressionDecorator.java: 壓縮裝飾
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Base64;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
public class CompressionDecorator extends DataSourceDecorator {
private int compLevel = 6;
public CompressionDecorator(DataSource source) {
super(source);
}
public int getCompressionLevel() {
return compLevel;
}
public void setCompressionLevel(int value) {
compLevel = value;
}
@Override
public void writeData(String data) {
super.writeData(compress(data));
}
@Override
public String readData() {
return decompress(super.readData());
}
private String compress(String stringData) {
byte[] data = stringData.getBytes();
try {
ByteArrayOutputStream bout = new ByteArrayOutputStream(512);
DeflaterOutputStream dos = new DeflaterOutputStream(bout, new Deflater(compLevel));
dos.write(data);
dos.close();
bout.close();
return Base64.getEncoder().encodeToString(bout.toByteArray());
} catch (IOException ex) {
return null;
}
}
private String decompress(String stringData) {
byte[] data = Base64.getDecoder().decode(stringData);
try {
InputStream in = new ByteArrayInputStream(data);
InflaterInputStream iin = new InflaterInputStream(in);
ByteArrayOutputStream bout = new ByteArrayOutputStream(512);
int b;
while ((b = iin.read()) != -1) {
bout.write(b);
}
in.close();
iin.close();
bout.close();
return new String(bout.toByteArray());
} catch (IOException ex) {
return null;
}
}
}
Demo.java: 客戶端程式碼
package refactoring_guru.decorator.example;
import refactoring_guru.decorator.example.decorators.*;
public class Demo {
public static void main(String[] args) {
String salaryRecords = "Name,Salary\nJohn Smith,100000\nSteven Jobs,912000";
DataSourceDecorator encoded = new CompressionDecorator(
new EncryptionDecorator(
new FileDataSource("out/OutputDemo.txt")));
encoded.writeData(salaryRecords);
DataSource plain = new FileDataSource("out/OutputDemo.txt");
System.out.println("- Input ----------------");
System.out.println(salaryRecords);
System.out.println("- Encoded --------------");
System.out.println(plain.readData());
System.out.println("- Decoded --------------");
System.out.println(encoded.readData());
}
}
與其他模式的關係
-
介面卡模式可以對已有物件的介面進行修改, 裝飾模式則能在不改變物件介面的前提下強化物件功能。 此外, 裝飾 還支援遞迴組合, 介面卡 則無法實現。
-
責任鏈模式和裝飾模式的類結構非常相似。 兩者都依賴遞迴組合將需要執行的操作傳遞給一系列物件。 但是, 兩者有幾點重要的不同之處。
責任鏈的管理者可以相互獨立地執行一切操作, 還可以隨時停止傳遞請求。 另一方面, 各種裝飾 可以在遵循基本介面的情況下擴充套件物件的行為。 此外, 裝飾無法中斷請求的傳遞。
-
組合模式和裝飾的結構圖很相似, 因為兩者都依賴遞迴組合來組織無限數量的物件。
裝飾類似於組合, 但其只有一個子元件。 此外還有一個明顯不同: 裝飾為被封裝物件添加了額外的職責, 組合僅對其子節點的結果進行了 “求和”。
但是, 模式也可以相互合作: 你可以使用裝飾來擴充套件組合樹中特定物件的行為。
-
裝飾和代理有著相似的結構, 但是其意圖卻非常不同。 這兩個模式的構建都基於組合原則, 也就是說一個物件應該將部分工作委派給另一個物件。 兩者之間的不同之處在於代理 通常自行管理其服務物件的生命週期, 而裝飾 的生成則總是由客戶端進行控制。
Facade
背景
外觀模式是一種結構型設計模式, 能為程式庫、 框架或其他複雜類提供一個簡單的介面。
問題:
假設你必須在程式碼中使用某個複雜的庫或框架中的眾多物件。 正常情況下, 你需要負責所有物件的初始化工作、 管理其依賴關係並按正確的順序執行方法等。
最終, 程式中類的業務邏輯將與第三方類的實現細節緊密耦合, 使得理解和維護程式碼的工作很難進行。
解決:
外觀類為包含許多活動部件的複雜子系統提供一個簡單的介面。 與直接呼叫子系統相比, 外觀提供的功能可能比較有限, 但它卻包含了客戶端真正關心的功能。
如果你的程式需要與包含幾十種功能的複雜庫整合, 但只需使用其中非常少的功能, 那麼使用外觀模式會非常方便,
例如, 上傳貓咪搞笑短視訊到社交媒體網站的應用可能會用到專業的視訊轉換庫, 但它只需使用一個包含 encode(filename, format)
方法 (以檔名與檔案格式為引數進行編碼的方法) 的類即可。 在建立這個類並將其連線到視訊轉換庫後, 你就擁有了自己的第一個外觀。
與真實世界的類比:
你通過手機網購時, 該商店的所有服務和部門的外觀。 程式設計師為你提供了一個同購物系統、 支付閘道器和各種送貨服務進行互動的簡單語音介面。
使用場景
如果你需要一個指向複雜子系統的直接介面, 且該介面的功能有限, 則可以使用外觀模式。
- 子系統通常會隨著時間的推進變得越來越複雜。 即便是應用了設計模式, 通常你也會建立更多的類。 儘管在多種情形中子系統可能是更靈活或易於複用的, 但其所需的配置和樣板程式碼數量將會增長得更快。 為了解決這個問題, 外觀將會提供指向子系統中最常用功能的快捷方式, 能夠滿足客戶端的大部分需求。
如果需要將子系統組織為多層結構, 可以使用外觀。
- 建立外觀來定義子系統中各層次的入口。 你可以要求子系統僅使用外觀來進行互動, 以減少子系統之間的耦合。
讓我們回到視訊轉換框架的例子。 該框架可以拆分為兩個層次: 音訊相關和視訊相關。 你可以為每個層次建立一個外觀, 然後要求各層的類必須通過這些外觀進行互動。 這種方式看上去與中介者模式非常相似。
例項-實現
下面是一些核心 Java 程式庫中的外觀示例:
javax.faces.context.FacesContext
在底層使用了LifeCycle
、ViewHandler
和NavigationHandler
這幾個類, 但絕大多數客戶端不知道。javax.faces.context.ExternalContext
在內部使用了ServletContext
、HttpSession
、HttpServletRequest
、HttpServletResponse
和其他一些類。
識別方法: 外觀可以通過使用簡單介面, 但將絕大部分工作委派給其他類的類來識別。 通常情況下, 外觀管理著其所使用的物件的完整生命週期。
複雜視訊轉換庫的簡單介面
some_complex_media_library/VideoFile.java
public class VideoFile {
private String name;
private String codecType;
public VideoFile(String name) {
this.name = name;
this.codecType = name.substring(name.indexOf(".") + 1);
}
public String getCodecType() {
return codecType;
}
public String getName() {
return name;
}
}
some_complex_media_library/Codec.java
public interface Codec {
}
some_complex_media_library/MPEG4CompressionCodec.java
public class MPEG4CompressionCodec implements Codec {
public String type = "mp4";
}
some_complex_media_library/OggCompressionCodec.java
public class OggCompressionCodec implements Codec {
public String type = "ogg";
}
some_complex_media_library/CodecFactory.java
public class CodecFactory {
public static Codec extract(VideoFile file) {
String type = file.getCodecType();
if (type.equals("mp4")) {
System.out.println("CodecFactory: extracting mpeg audio...");
return new MPEG4CompressionCodec();
}
else {
System.out.println("CodecFactory: extracting ogg audio...");
return new OggCompressionCodec();
}
}
}
some_complex_media_library/BitrateReader.java
public class BitrateReader {
public static VideoFile read(VideoFile file, Codec codec) {
System.out.println("BitrateReader: reading file...");
return file;
}
public static VideoFile convert(VideoFile buffer, Codec codec) {
System.out.println("BitrateReader: writing file...");
return buffer;
}
}
some_complex_media_library/AudioMixer.java
import java.io.File;
public class AudioMixer {
public File fix(VideoFile result){
System.out.println("AudioMixer: fixing audio...");
return new File("tmp");
}
}
facade
facade/VideoConversionFacade.java: 外觀提供了進行視訊轉換的簡單介面
import java.io.File;
public class VideoConversionFacade {
public File convertVideo(String fileName, String format) {
System.out.println("VideoConversionFacade: conversion started.");
VideoFile file = new VideoFile(fileName);
Codec sourceCodec = CodecFactory.extract(file);
Codec destinationCodec;
if (format.equals("mp4")) {
destinationCodec = new OggCompressionCodec();
} else {
destinationCodec = new MPEG4CompressionCodec();
}
VideoFile buffer = BitrateReader.read(file, sourceCodec);
VideoFile intermediateResult = BitrateReader.convert(buffer, destinationCodec);
File result = (new AudioMixer()).fix(intermediateResult);
System.out.println("VideoConversionFacade: conversion completed.");
return result;
}
}
Demo.java: 客戶端程式碼
import java.io.File;
public class Demo {
public static void main(String[] args) {
VideoConversionFacade converter = new VideoConversionFacade();
File mp4Video = converter.convertVideo("youtubevideo.ogg", "mp4");
// ...
}
}
與其他模式的關係
- 外觀模式為現有物件定義了一個新介面, 介面卡模式則會試圖運用已有的介面。 介面卡 通常只封裝一個物件, 外觀 通常會作用於整個物件子系統上。
- 當只需對客戶端程式碼隱藏子系統建立物件的方式時, 你可以使用抽象工廠模式來代替外觀。
- 享元模式展示瞭如何生成大量的小型物件, 外觀則展示瞭如何用一個物件來代表整個子系統。
- 外觀和中介者模式的職責類似: 它們都嘗試在大量緊密耦合的類中組織起合作。
- 外觀 為子系統中的所有物件定義了一個簡單介面, 但是它不提供任何新功能。 子系統本身不會意識到外觀的存在。 子系統中的物件可以直接進行交流。
- 中介者 將系統中元件的溝通行為中心化。 各元件只知道中介者物件, 無法直接相互交流。
- 外觀類通常可以轉換為單例模式類, 因為在大部分情況下一個外觀物件就足夠了。
- 外觀與代理模式的相似之處在於它們都快取了一個複雜實體並自行對其進行初始化。 代理 與其服務物件遵循同一介面, 使得自己和服務物件可以互換, 在這一點上它與外觀 不同。
Flyweight
背景
享元模式是一種結構型設計模式, 它摒棄了在每個物件中儲存所有資料的方式, 通過共享多個物件所共有的相同狀態, 讓你能在有限的記憶體容量中載入更多物件。
享元模式通過共享多個物件的部分狀態來實現上述功能。 換句話來說, 享元會將不同物件的相同資料進行快取以節省記憶體。
問題
開發了一款簡單的遊戲: 玩家們在地圖上移動並相互射擊。 你決定實現一個真實的粒子系統, 並將其作為遊戲的特色。 大量的子彈、 導彈和爆炸彈片會在整個地圖上穿行, 為玩家提供緊張刺激的遊戲體驗。
遊戲總是會在他的電腦上執行幾分鐘後崩潰。 在研究了幾個小時的除錯訊息記錄後, 你發現導致遊戲崩潰的原因是記憶體容量不足。 真正的問題與粒子系統有關。 每個粒子 (一顆子彈、 一枚導彈或一塊彈片) 都由包含完整資料的獨立物件來表示。 當玩家在遊戲中鏖戰進入高潮後的某一時刻, 遊戲將無法在剩餘記憶體中載入新建粒子, 於是程式就崩潰了。
解決方案
每個粒子的只有一些狀態,例如: (座標、 移動向量和速度) 是不同的。
物件的常量資料通常被稱為內在狀態, 其位於物件中, 其他物件只能讀取但不能修改其數值。 而物件的其他狀態常常能被其他物件 “從外部” 改變, 因此被稱為外在狀態。
享元模式建議不在物件中儲存外在狀態, 而是將其傳遞給依賴於它的一個特殊方法。 程式只在物件中儲存內在狀態, 以方便在不同情景下重用。 這些物件的區別僅在於其內在狀態 (與外在狀態相比, 內在狀態的變體要少很多), 因此你所需的物件數量會大大削減。
外在狀態儲存
為了能將外在狀態移動到這個類中, 你需要建立多個數組成員變數來儲存每個粒子的座標、 方向向量和速度。 除此之外, 你還需要另一個數組來儲存指向代表粒子的特定享元的引用。 這些陣列必須保持同步, 這樣你才能夠使用同一索引來獲取關於某個粒子的所有資料。
更優雅的解決方案是建立獨立的情景類來儲存外在狀態和對享元物件的引用。 在該方法中, 容器類只需包含一個數組。
這樣的話情景物件數量不是會和不採用該模式時的物件數量一樣多嗎? 的確如此, 但這些物件要比之前小很多。 消耗記憶體最多的成員變數已經被移動到很少的幾個享元物件中了。 現在, 一個享元大物件會被上千個情境小物件複用, 因此無需再重複儲存數千個大物件的資料。
享元與不可變性
由於享元物件可在不同的情景中使用, 你必須確保其狀態不能被修改。 享元類的狀態只能由建構函式的引數進行一次性初始化, 它不能對其他物件公開其設定器或公有成員變數。
享元工廠
為了能更方便地訪問各種享元, 你可以建立一個工廠方法來管理已有享元物件的快取池。 工廠方法從客戶端處接收目標享元物件的內在狀態作為引數, 如果它能在快取池中找到所需享元, 則將其返回給客戶端; 如果沒有找到, 它就會新建一個享元, 並將其新增到快取池中。
你可以選擇在程式的不同地方放入該函式。 最簡單的選擇就是將其放置在享元容器中。 除此之外, 你還可以新建一個工廠類, 或者建立一個靜態的工廠方法並將其放入實際的享元類中。
使用場景
僅在程式必須支援大量物件且沒有足夠的記憶體容量時使用享元模式。
應用該模式所獲的收益大小取決於使用它的方式和情景。 它在下列情況中最有效:
- 程式需要生成數量巨大的相似物件
- 這將耗盡目標裝置的所有記憶體
- 物件中包含可抽取且能在多個物件間共享的重複狀態。
例項-實現
享元模式在核心 Java 程式庫中的示例:
識別方法: 享元可以通過構建方法來識別, 它會返回快取物件而不是建立新的物件。
渲染一片森林
本例中, 我們將渲染一片森林 (1,000,000 棵樹)! 每棵樹都由包含一些狀態的物件來表示 (座標和紋理等)。 儘管程式能夠完成其主要工作, 但很顯然它需要消耗大量記憶體。
原因很簡單: 太多樹物件包含重複資料 (名稱、 紋理和顏色)。 因此我們可用享元模式來將這些數值儲存在單獨的享元物件中 ( TreeType
類)。 現在我們不再將相同資料儲存在數千個 Tree
物件中, 而是使用一組特殊的數值來引用其中一個享元物件。
客戶端程式碼不會知道任何事情, 因為重用享元物件的複雜機制隱藏在了享元工廠中。
trees
trees/Tree.java: 包含每棵樹的獨特狀態
import java.awt.*;
public class Tree {
private int x;
private int y;
private TreeType type;
public Tree(int x, int y, TreeType type) {
this.x = x;
this.y = y;
this.type = type;
}
public void draw(Graphics g) {
type.draw(g, x, y);
}
}
trees/TreeType.java: 包含多棵樹共享的狀態
import java.awt.*;
public class TreeType {
private String name;
private Color color;
private String otherTreeData;
public TreeType(String name, Color color, String otherTreeData) {
this.name = name;
this.color = color;
this.otherTreeData = otherTreeData;
}
public void draw(Graphics g, int x, int y) {
g.setColor(Color.BLACK);
g.fillRect(x - 1, y, 3, 5);
g.setColor(color);
g.fillOval(x - 5, y - 10, 10, 10);
}
}
trees/TreeFactory.java: 封裝建立享元的複雜機制
import java.awt.*;
import java.util.HashMap;
import java.util.Map;
public class TreeFactory {
static Map<String, TreeType> treeTypes = new HashMap<>();
public static TreeType getTreeType(String name, Color color, String otherTreeData) {
TreeType result = treeTypes.get(name);
if (result == null) {
result = new TreeType(name, color, otherTreeData);
treeTypes.put(name, result);
}
return result;
}
}
forest
forest/Forest.java: 我們繪製的森林
import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
public class Forest extends JFrame {
private List<Tree> trees = new ArrayList<>();
public void plantTree(int x, int y, String name, Color color, String otherTreeData) {
TreeType type = TreeFactory.getTreeType(name, color, otherTreeData);
Tree tree = new Tree(x, y, type);
trees.add(tree);
}
@Override
public void paint(Graphics graphics) {
for (Tree tree : trees) {
tree.draw(graphics);
}
}
}
Demo.java: 客戶端程式碼
import refactoring_guru.flyweight.example.forest.Forest;
import java.awt.*;
public class Demo {
static int CANVAS_SIZE = 500;
static int TREES_TO_DRAW = 1000000;
static int TREE_TYPES = 2;
public static void main(String[] args) {
Forest forest = new Forest();
for (int i = 0; i < Math.floor(TREES_TO_DRAW / TREE_TYPES); i++) {
forest.plantTree(random(0, CANVAS_SIZE), random(0, CANVAS_SIZE),
"Summer Oak", Color.GREEN, "Oak texture stub");
forest.plantTree(random(0, CANVAS_SIZE), random(0, CANVAS_SIZE),
"Autumn Oak", Color.ORANGE, "Autumn Oak texture stub");
}
forest.setSize(CANVAS_SIZE, CANVAS_SIZE);
forest.setVisible(true);
System.out.println(TREES_TO_DRAW + " trees drawn");
System.out.println("---------------------");
System.out.println("Memory usage:");
System.out.println("Tree size (8 bytes) * " + TREES_TO_DRAW);
System.out.println("+ TreeTypes size (~30 bytes) * " + TREE_TYPES + "");
System.out.println("---------------------");
System.out.println("Total: " + ((TREES_TO_DRAW * 8 + TREE_TYPES * 30) / 1024 / 1024) +
"MB (instead of " + ((TREES_TO_DRAW * 38) / 1024 / 1024) + "MB)");
}
private static int random(int min, int max) {
return min + (int) (Math.random() * ((max - min) + 1));
}
}
與其他模式的關係
- 你可以使用享元模式實現組合模式樹的共享葉節點以節省記憶體。
- 享元展示瞭如何生成大量的小型物件, 外觀模式則展示瞭如何用一個物件來代表整個子系統。
- 如果你能將物件的所有共享狀態簡化為一個享元物件, 那麼享元就和單例模式類似了。 但這兩個模式有兩個根本性的不同。
- 只會有一個單例實體, 但是享元類可以有多個實體, 各實體的內在狀態也可以不同。
- 單例 物件可以是可變的。 享元物件是不可變的。
Proxy
背景
代理模式是一種結構型設計模式, 讓你能夠提供物件的替代品或其佔位符。 代理控制著對於原物件的訪問, 並允許在將請求提交給物件前後進行一些處理。
代理是一種結構型設計模式, 讓你能提供真實服務物件的替代品給客戶端使用。 代理接收客戶端的請求並進行一些處理 (訪問控制和快取等), 然後再將請求傳遞給服務物件。
代理物件擁有和服務物件相同的介面, 這使得當其被傳遞給客戶端時可與真實物件互換。
於真實世界類比
信用卡是銀行賬戶的代理, 銀行賬戶則是一大捆現金的代理。
它們都實現了同樣的介面, 均可用於進行支付。 消費者會非常滿意, 因為不必隨身攜帶大量現金; 商店老闆同樣會十分高興, 因為交易收入能以電子化的方式進入商店的銀行賬戶中, 無需擔心存款時出現現金丟失或被搶劫的情況。
使用場景
延遲初始化 (虛擬代理)。 如果你有一個偶爾使用的重量級服務物件, 一直保持該物件執行會消耗系統資源時, 可使用代理模式。
- 你無需在程式啟動時就建立該物件, 可將物件的初始化延遲到真正有需要的時候。
訪問控制 (保護代理)。 如果你只希望特定客戶端使用服務物件, 這裡的物件可以是作業系統中非常重要的部分, 而客戶端則是各種已啟動的程式 (包括惡意程式), 此時可使用代理模式。
- 代理可僅在客戶端憑據滿足要求時將請求傳遞給服務物件。
本地執行遠端服務 (遠端代理)。 適用於服務物件位於遠端伺服器上的情形。
- 這種情形中, 代理通過網路傳遞客戶端請求, 負責處理所有與網路相關的複雜細節。
記錄日誌請求 (日誌記錄代理)。 適用於當你需要儲存對於服務物件的請求歷史記錄時。 代理可以在向服務傳遞請求前進行記錄。
快取請求結果 (快取代理)。 適用於需要快取客戶請求結果並對快取生命週期進行管理時, 特別是當返回結果的體積非常大時。
- 代理可對重複請求所需的相同結果進行快取, 還可使用請求引數作為索引快取的鍵值。
例項-實現
使用示例: 儘管代理模式在絕大多數 Java 程式中並不常見, 但它在一些特殊情況下仍然非常方便。 當你希望在無需修改客戶程式碼的前提下於已有類的物件上增加額外行為時, 該模式是無可替代的。
Java 標準程式庫中的一些代理模式的示例:
java.lang.reflect.Proxy
java.rmi.*
javax.ejb.EJB
(檢視評論)javax.inject.Inject
(檢視評論)javax.persistence.PersistenceContext
識別方法: 代理模式會將所有實際工作委派給一些其他物件。 除非代理是某個服務的子類, 否則每個代理方法最後都應該引用一個服務物件。
快取代理
在本例中, 代理模式有助於實現延遲初始化, 並對低效的第三方 YouTube 整合程式庫進行快取。
當你需要在無法修改程式碼的類上新增一些額外行為時, 代理模式的價值無可估量。
some_cool_media_library
some_cool_media_library/ThirdPartyYouTubeLib.java: 遠端服務介面
import java.util.HashMap;
public interface ThirdPartyYouTubeLib {
HashMap<String, Video> popularVideos();
Video getVideo(String videoId);
}
some_cool_media_library/ThirdPartyYouTubeClass.java: 遠端服務實現
import java.util.HashMap;
public class ThirdPartyYouTubeClass implements ThirdPartyYouTubeLib {
@Override
public HashMap<String, Video> popularVideos() {
connectToServer("http://www.youtube.com");
return getRandomVideos();
}
@Override
public Video getVideo(String videoId) {
connectToServer("http://www.youtube.com/" + videoId);
return getSomeVideo(videoId);
}
// -----------------------------------------------------------------------
// Fake methods to simulate network activity. They as slow as a real life.
private int random(int min, int max) {
return min + (int) (Math.random() * ((max - min) + 1));
}
private void experienceNetworkLatency() {
int randomLatency = random(5, 10);
for (int i = 0; i < randomLatency; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
private void connectToServer(String server) {
System.out.print("Connecting to " + server + "... ");
experienceNetworkLatency();
System.out.print("Connected!" + "\n");
}
private HashMap<String, Video> getRandomVideos() {
System.out.print("Downloading populars... ");
experienceNetworkLatency();
HashMap<String, Video> hmap = new HashMap<String, Video>();
hmap.put("catzzzzzzzzz", new Video("sadgahasgdas", "Catzzzz.avi"));
hmap.put("mkafksangasj", new Video("mkafksangasj", "Dog play with ball.mp4"));
hmap.put("dancesvideoo", new Video("asdfas3ffasd", "Dancing video.mpq"));
hmap.put("dlsdk5jfslaf", new Video("dlsdk5jfslaf", "Barcelona vs RealM.mov"));
hmap.put("3sdfgsd1j333", new Video("3sdfgsd1j333", "Programing lesson#1.avi"));
System.out.print("Done!" + "\n");
return hmap;
}
private Video getSomeVideo(String videoId) {
System.out.print("Downloading video... ");
experienceNetworkLatency();
Video video = new Video(videoId, "Some video title");
System.out.print("Done!" + "\n");
return video;
}
}
some_cool_media_library/Video.java: 視訊檔案
public class Video {
public String id;
public String title;
public String data;
Video(String id, String title) {
this.id = id;
this.title = title;
this.data = "Random video.";
}
}
proxy
proxy/YouTubeCacheProxy.java: 快取代理
import java.util.HashMap;
public class YouTubeCacheProxy implements ThirdPartyYouTubeLib {
private ThirdPartyYouTubeLib youtubeService;
private HashMap<String, Video> cachePopular = new HashMap<String, Video>();
private HashMap<String, Video> cacheAll = new HashMap<String, Video>();
public YouTubeCacheProxy() {
this.youtubeService = new ThirdPartyYouTubeClass();
}
@Override
public HashMap<String, Video> popularVideos() {
if (cachePopular.isEmpty()) {
cachePopular = youtubeService.popularVideos();
} else {
System.out.println("Retrieved list from cache.");
}
return cachePopular;
}
@Override
public Video getVideo(String videoId) {
Video video = cacheAll.get(videoId);
if (video == null) {
video = youtubeService.getVideo(videoId);
cacheAll.put(videoId, video);
} else {
System.out.println("Retrieved video '" + videoId + "' from cache.");
}
return video;
}
public void reset() {
cachePopular.clear();
cacheAll.clear();
}
}
downloader
downloader/YouTubeDownloader.java: 媒體下載應用
import java.util.HashMap;
public class YouTubeDownloader {
private ThirdPartyYouTubeLib api;
public YouTubeDownloader(ThirdPartyYouTubeLib api) {
this.api = api;
}
public void renderVideoPage(String videoId) {
Video video = api.getVideo(videoId);
System.out.println("\n-------------------------------");
System.out.println("Video page (imagine fancy HTML)");
System.out.println("ID: " + video.id);
System.out.println("Title: " + video.title);
System.out.println("Video: " + video.data);
System.out.println("-------------------------------\n");
}
public void renderPopularVideos() {
HashMap<String, Video> list = api.popularVideos();
System.out.println("\n-------------------------------");
System.out.println("Most popular videos on YouTube (imagine fancy HTML)");
for (Video video : list.values()) {
System.out.println("ID: " + video.id + " / Title: " + video.title);
}
System.out.println("-------------------------------\n");
}
}
Demo.java: 初始化程式碼
public class Demo {
public static void main(String[] args) {
YouTubeDownloader naiveDownloader = new YouTubeDownloader(new ThirdPartyYouTubeClass());
YouTubeDownloader smartDownloader = new YouTubeDownloader(new YouTubeCacheProxy());
long naive = test(naiveDownloader);
long smart = test(smartDownloader);
System.out.print("Time saved by caching proxy: " + (naive - smart) + "ms");
}
private static long test(YouTubeDownloader downloader) {
long startTime = System.currentTimeMillis();
// User behavior in our app:
downloader.renderPopularVideos();
downloader.renderVideoPage("catzzzzzzzzz");
downloader.renderPopularVideos();
downloader.renderVideoPage("dancesvideoo");
// Users might visit the same page quite often.
downloader.renderVideoPage("catzzzzzzzzz");
downloader.renderVideoPage("someothervid");
long estimatedTime = System.currentTimeMillis() - startTime;
System.out.print("Time elapsed: " + estimatedTime + "ms\n");
return estimatedTime;
}
}