設計模式-超有趣的例子幫助你理解!附專案地址!
閱讀推薦:設計模式-簡單篇
專案地址: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)
軟體複用時,要先儘量使用組合或者聚合等關聯關係實現,其次才考慮使用繼承。即在一個新物件裡通過關聯的方式使用已有物件的一些方法和功能。
Creational Pattems
Singleton
背景
保證一個類只有一個例項, 並提供一個訪問該例項的全域性節點。
政府是單例模式的一個很好的示例。 一個國家只有一個官方政府。 不管組成政府的每個人的身份是什麼, “某政府” 這一稱謂總是鑑別那些掌權者的全域性訪問節點。
適用場景
- 如果程式中的某個類對於所有客戶端只有一個可用的例項,可以使用單例模式。
- 如果你需要更加嚴格地控制全域性變數,可以使用單例模式 。
例項-實現
Java 核心程式庫中仍有相當多的單例示例:
基礎單例(單執行緒)
package com.zwt.BasicSingleton;
/**
* @author ML李嘉圖
* @version createtime: 2021-11-09
* Blog: https://www.cnblogs.com/zwtblog/
*/
public final class Singleton {
private static Singleton instance;
public String value;
private Singleton(String value) {
// The following code emulates slow initialization.
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
this.value = value;
}
public static Singleton getInstance(String value) {
if (instance == null) {
instance = new Singleton(value);
}
return instance;
}
}
package com.zwt.BasicSingleton;
/**
* @author ML李嘉圖
* @version createtime: 2021-11-09
* Blog: https://www.cnblogs.com/zwtblog/
*/
public class DemoSingleThread {
public static void main(String[] args) {
System.out.println("If you see the same value, then singleton was reused (yay!)" + "\n" +
"If you see different values, then 2 singletons were created (booo!!)" + "\n\n" +
"RESULT:" + "\n");
Singleton anotherSingleton = Singleton.getInstance("wb");
Singleton singleton = Singleton.getInstance("zwt");
System.out.println(singleton.value);
System.out.println(anotherSingleton.value);
}
}
If you see the same value, then singleton was reused (yay!)
If you see different values, then 2 singletons were created (booo!!)
RESULT:
wb
wb
基礎單例(多執行緒)
相同的類在多執行緒環境中會出錯。 多執行緒可能會同時呼叫構建方法並獲取多個單例類的例項。
package com.zwt.BasicSingleton;
/**
* @author ML李嘉圖
* @version createtime: 2021-11-09
* Blog: https://www.cnblogs.com/zwtblog/
*/
public class DemoMultiThread {
public static void main(String[] args) {
System.out.println("If you see the same value, then singleton was reused (yay!)" + "\n" +
"If you see different values, then 2 singletons were created (booo!!)" + "\n\n" +
"RESULT:" + "\n");
Thread threadFoo = new Thread(new Threadwb());
Thread threadBar = new Thread(new Threadzwt());
threadFoo.start();
threadBar.start();
}
static class Threadwb implements Runnable {
@Override
public void run() {
Singleton singleton = Singleton.getInstance("wb");
System.out.println(singleton.value);
}
}
static class Threadzwt implements Runnable {
@Override
public void run() {
Singleton singleton = Singleton.getInstance("zwt");
System.out.println(singleton.value);
}
}
}
If you see the same value, then singleton was reused (yay!)
If you see different values, then 2 singletons were created (booo!!)
RESULT:
wb
zwt
採用延遲載入的執行緒安全單例
為了解決這個問題, 你必須在建立首個單例物件時對執行緒進行同步。
package com.zwt.Singleton;
/**
* @author ML李嘉圖
* @version createtime: 2021-11-09
* Blog: https://www.cnblogs.com/zwtblog/
*/
public final class Singleton {
private static volatile Singleton instance;
public String value;
private Singleton(String value) {
this.value = value;
}
public static Singleton getInstance(String value) {
Singleton result = instance;
if (result != null) {
return result;
}
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton(value);
}
return instance;
}
}
}
package com.zwt.Singleton;
/**
* @author ML李嘉圖
* @version createtime: 2021-11-09
* Blog: https://www.cnblogs.com/zwtblog/
*/
public class DemoMultiThread {
public static void main(String[] args) {
System.out.println("If you see the same value, then singleton was reused (yay!)" + "\n" +
"If you see different values, then 2 singletons were created (booo!!)" + "\n\n" +
"RESULT:" + "\n");
Thread threadFoo = new Thread(new ThreadFoo());
Thread threadBar = new Thread(new ThreadBar());
threadFoo.start();
threadBar.start();
}
static class ThreadFoo implements Runnable {
@Override
public void run() {
Singleton singleton = Singleton.getInstance("FOO");
System.out.println(singleton.value);
}
}
static class ThreadBar implements Runnable {
@Override
public void run() {
Singleton singleton = Singleton.getInstance("BAR");
System.out.println(singleton.value);
}
}
}
If you see the same value, then singleton was reused (yay!)
If you see different values, then 2 singletons were created (booo!!)
RESULT:
FOO
FOO
理論-實現
Eager initialization
在預先初始化中,單例類的例項是在類載入時建立的,這是建立單例類的最簡單方法,但它有一個缺點,即即使客戶端應用程式可能不會使用它,也會建立例項。
如果您的單例類沒有使用大量資源,則可以使用這種方法。但是在大多數情況下,Singleton 類是為檔案系統、資料庫連線等資源建立的。除非客戶端呼叫該getInstance
方法,否則我們應該避免例項化。此外,此方法不提供任何異常處理選項。
public class EagerInitializedSingleton {
private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();
//private constructor to avoid client applications to use constructor
private EagerInitializedSingleton(){}
public static EagerInitializedSingleton getInstance(){
return instance;
}
}
Static block initialization
public class StaticBlockSingleton {
private static StaticBlockSingleton instance;
private StaticBlockSingleton(){}
//static block initialization for exception handling
static{
try{
instance = new StaticBlockSingleton();
}catch(Exception e){
throw new RuntimeException("Exception occured in creating singleton instance");
}
}
public static StaticBlockSingleton getInstance(){
return instance;
}
}
Lazy Initialization
在單執行緒環境下工作正常,但是當涉及到多執行緒系統時,如果多個執行緒同時處於 if 條件內,則可能會導致問題。它將破壞單例模式,兩個執行緒將獲得單例類的不同例項。
public class LazyInitializedSingleton {
private static LazyInitializedSingleton instance;
private LazyInitializedSingleton(){}
public static LazyInitializedSingleton getInstance(){
if(instance == null){
instance = new LazyInitializedSingleton();
}
return instance;
}
}
Thread Safe Singleton
public class ThreadSafeSingleton {
private static ThreadSafeSingleton instance;
private ThreadSafeSingleton(){}
public static synchronized ThreadSafeSingleton getInstance(){
if(instance == null){
instance = new ThreadSafeSingleton();
}
return instance;
}
}
但由於與同步方法相關的成本,它降低了效能,儘管我們只需要它用於可能建立單獨例項的前幾個執行緒(閱讀:Java 同步)。為了每次都避免這種額外的開銷,使用了雙重檢查鎖定原則。在這種方法中,同步塊在 if 條件中使用,並進行額外檢查以確保僅建立單例類的一個例項。
public static ThreadSafeSingleton getInstanceUsingDoubleLocking(){
if(instance == null){
synchronized (ThreadSafeSingleton.class) {
if(instance == null){
instance = new ThreadSafeSingleton();
}
}
}
return instance;
}
Bill Pugh Singleton Implementation
在 Java 5 之前,java 記憶體模型有很多問題,並且在某些場景中,如果太多執行緒試圖同時獲取 Singleton 類的例項,上述方法會失敗。
請注意包含單例類例項的私有內部靜態類。當載入單例類時,SingletonHelper
類不會載入到記憶體中,只有當有人呼叫getInstance方法時,才會載入這個類並建立單例類例項。
這是 Singleton 類最廣泛使用的方法,因為它不需要同步。我在我的許多專案中都使用了這種方法,而且它也很容易理解和實現。
public class BillPughSingleton {
private BillPughSingleton(){}
private static class SingletonHelper{
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance(){
return SingletonHelper.INSTANCE;
}
}
Using Reflection to destroy Singleton Pattern
反射可用於破壞上述所有單例實現方法。讓我們用一個示例類來看看這個。
您執行上面的測試類時,您會注意到兩個例項的 hashCode 不相同,這破壞了單例模式。反射非常強大,並在很多框架中使用,如 Spring 和 Hibernate。
import java.lang.reflect.Constructor;
/**
* @author ML李嘉圖
* @version createtime: 2021-11-09
* Blog: https://www.cnblogs.com/zwtblog/
*/
public class ReflectionSingletonTest {
public static void main(String[] args) {
EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
EagerInitializedSingleton instanceTwo = null;
EagerInitializedSingleton instanceThree = null;
try {
Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
for (Constructor constructor : constructors) {
//Below code will destroy the singleton pattern
//關閉了檢查
constructor.setAccessible(true);
instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
instanceThree = (EagerInitializedSingleton) constructor.newInstance();
break;
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(instanceOne.hashCode());
System.out.println(instanceTwo.hashCode());
System.out.println(instanceThree.hashCode());
}
}
Enum Singleton
public enum EnumSingleton {
INSTANCE;
public static void doSomething(){
//do something
}
}
Serialization and Singleton
有時在分散式系統中,我們需要在 Singleton 類中實現 Serializable 介面,以便我們可以將其狀態儲存在檔案系統中並在以後的某個時間點檢索它。這是一個也實現了 Serializable 介面的小型單例類。
package com.zwt.SerializableSingleton;
import java.io.Serializable;
/**
* @author ML李嘉圖
* @version createtime: 2021-11-09
* Blog: https://www.cnblogs.com/zwtblog/
*/
public class SerializedSingleton implements Serializable {
private static final long serialVersionUID = -7604766932017737115L;
private SerializedSingleton() {
}
private static class SingletonHelper {
private static final SerializedSingleton instance = new SerializedSingleton();
}
public static SerializedSingleton getInstance() {
return SingletonHelper.instance;
}
//這樣當JVM從記憶體中反序列化地"組裝"一個新物件時
//就會自動呼叫這個 readResolve方法來返回我們指定好的物件了,
protected Object readResolve() {
return getInstance();
}
}
序列化單例類的問題在於,無論何時反序列化它,它都會建立該類的一個新例項。讓我們用一個簡單的程式來看看它。
package com.zwt.SerializableSingleton;
import java.io.*;
/**
* @author ML李嘉圖
* @version createtime: 2021-11-09
* Blog: https://www.cnblogs.com/zwtblog/
*/
public class SingletonSerializedTest {
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
SerializedSingleton instanceOne = SerializedSingleton.getInstance();
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
"filename.ser"));
out.writeObject(instanceOne);
out.close();
//deserailize from file to object
ObjectInput in = new ObjectInputStream(new FileInputStream(
"filename.ser"));
SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject();
in.close();
System.out.println("instanceOne hashCode=" + instanceOne.hashCode());
System.out.println("instanceTwo hashCode=" + instanceTwo.hashCode());
System.out.println("===============");
}
}
所以它破壞了單例模式,為了克服這種情況,我們需要做的就是提供readResolve()
方法的實現。
protected Object readResolve() {
return getInstance();
}
與其他模式關係
- 外觀模式類通常可以轉換為單例模式類, 因為在大部分情況下一個外觀物件就足夠了。
- 如果你能將物件的所有共享狀態簡化為一個享元物件, 那麼享元模式就和單例類似了。 但這兩個模式有兩個根本性的不同。
- 只會有一個單例實體, 但是享元類可以有多個實體, 各實體的內在狀態也可以不同。
- 單例物件可以是可變的。 享元物件是不可變的。
- 抽象工廠模式、 生成器模式和原型模式都可以用單例來實現。
Factory
背景
工廠方法模式是一種建立型設計模式, 其在父類中提供一個建立物件的方法, 允許子類決定例項化物件的型別。
假設你正在開發一款物流管理應用。 最初版本只能處理卡車運輸, 因此大部分程式碼都在位於名為 卡車的類中。
一段時間後, 這款應用變得極受歡迎。 你每天都能收到十幾次來自海運公司的請求, 希望應用能夠支援海上物流功能。
如果程式碼其餘部分與現有類已經存在耦合關係, 那麼向程式中新增新類其實並沒有那麼容易。
工廠方法模式建議使用特殊的工廠方法代替對於物件建構函式的直接呼叫 (即使用 new運算子)。
不用擔心, 物件仍將通過 new運算子建立, 只是該運算子改在工廠方法中呼叫罷了。 工廠方法返回的物件通常被稱作 “產品”。
乍看之下, 這種更改可能毫無意義:
我們只是改變了程式中呼叫建構函式的位置而已。 但是, 仔細想一下, 現在你可以在子類中重寫工廠方法, 從而改變其建立產品的型別。
但有一點需要注意:僅當這些產品具有共同的基類或者介面時, 子類才能返回不同型別的產品, 同時基類中的工廠方法還應將其返回型別宣告為這一共有介面。
呼叫工廠方法的程式碼 (通常被稱為客戶端程式碼) 無需瞭解不同子類返回實際物件之間的差別。 客戶端將所有產品視為抽象的 運輸
。 客戶端知道所有運輸物件都提供 交付
方法, 但是並不關心其具體實現方式。
適用場景
- 當你在編寫程式碼的過程中, 如果無法預知物件確切類別及其依賴關係時, 可使用工廠方法。
- 如果你希望使用者能擴充套件你軟體庫或框架的內部元件, 可使用工廠方法。
- 如果你希望複用現有物件來節省系統資源, 而不是每次都重新建立物件, 可使用工廠方法。
例項-實現
工廠方法模式在 Java 程式碼中得到了廣泛使用。 當你需要在程式碼中提供高層次的靈活性時, 該模式會非常實用。
- java.util.Calendar、ResourceBundle 和 NumberFormat
getInstance()
方法使用工廠模式。 valueOf()
Boolean、Integer 等包裝類中的方法。
核心 Java 程式庫中有該模式的應用:
java.util.Calendar#getInstance()
java.util.ResourceBundle#getBundle()
java.text.NumberFormat#getInstance()
java.nio.charset.Charset#forName()
java.net.URLStreamHandlerFactory#createURLStreamHandler(String)
(根據協議返回不同的單例物件)java.util.EnumSet#of()
javax.xml.bind.JAXBContext#createMarshaller()
及其他類似的方法。
識別方法: 工廠方法可通過構建方法來識別, 它會建立具體類的物件, 但以抽象型別或介面的形式返回這些物件。
生成跨平臺的 GUI 元素
buttons
buttons/Button.java: 通用產品介面
package com.zwt.buttons;
/**
* @author ML李嘉圖
* @version createtime: 2021-11-10
* Blog: https://www.cnblogs.com/zwtblog/
*/
/**
* Common interface for all buttons.
*/
public interface Button {
void render();
void onClick();
}
buttons/HtmlButton.java: 具體產品
package com.zwt.buttons;
/**
* @author ML李嘉圖
* @version createtime: 2021-11-10
* Blog: https://www.cnblogs.com/zwtblog/
*/
/**
* HTML button implementation.
*/
public class HtmlButton implements Button {
public void render() {
System.out.println("<button>Test Button</button>");
onClick();
}
public void onClick() {
System.out.println("Click! Button says - 'Hello World!'");
}
}
buttons/WindowsButton.java: 另一個具體產品
package com.zwt.buttons;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
/**
* @author ML李嘉圖
* @version createtime: 2021-11-10
* Blog: https://www.cnblogs.com/zwtblog/
*/
/**
* Windows button implementation.
*/
public class WindowsButton implements Button {
JPanel panel = new JPanel();
JFrame frame = new JFrame();
JButton button;
public void render() {
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JLabel label = new JLabel("Hello World!");
label.setOpaque(true);
label.setBackground(new Color(235, 233, 126));
label.setFont(new Font("Dialog", Font.BOLD, 44));
label.setHorizontalAlignment(SwingConstants.CENTER);
panel.setLayout(new FlowLayout(FlowLayout.CENTER));
frame.getContentPane().add(panel);
panel.add(label);
onClick();
panel.add(button);
frame.setSize(320, 200);
frame.setVisible(true);
onClick();
}
public void onClick() {
button = new JButton("Exit");
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
frame.setVisible(false);
System.exit(0);
}
});
}
}
factory
factory/Dialog.java: 基礎建立者
package com.zwt.factory;
import com.zwt.buttons.Button;
/**
* @author ML李嘉圖
* @version createtime: 2021-11-10
* Blog: https://www.cnblogs.com/zwtblog/
*/
/**
* Base factory class. Note that "factory" is merely a role for the class. It
* should have some core business logic which needs different products to be
* created.
*/
public abstract class Dialog {
public void renderWindow() {
// ... other code ...
Button okButton = createButton();
okButton.render();
}
/**
* Subclasses will override this method in order to create specific button
* objects.
*/
public abstract Button createButton();
}
factory/HtmlDialog.java: 具體建立者
package com.zwt.factory;
/**
* @author ML李嘉圖
* @version createtime: 2021-11-10
* Blog: https://www.cnblogs.com/zwtblog/
*/
import com.zwt.buttons.Button;
import com.zwt.buttons.HtmlButton;
/**
* HTML Dialog will produce HTML buttons.
*/
public class HtmlDialog extends Dialog {
@Override
public Button createButton() {
return new HtmlButton();
}
}
factory/WindowsDialog.java: 另一個具體建立者
package com.zwt.factory;
import com.zwt.buttons.Button;
import com.zwt.buttons.WindowsButton;
/**
* @author ML李嘉圖
* @version createtime: 2021-11-10
* Blog: https://www.cnblogs.com/zwtblog/
*/
/**
* Windows Dialog will produce Windows buttons.
*/
public class WindowsDialog extends Dialog {
@Override
public Button createButton() {
return new WindowsButton();
}
}
Demo.java: 客戶端程式碼
package com.zwt;
/**
* @author ML李嘉圖
* @version createtime: 2021-11-10
* Blog: https://www.cnblogs.com/zwtblog/
*/
import com.zwt.factory.Dialog;
import com.zwt.factory.HtmlDialog;
import com.zwt.factory.WindowsDialog;
/**
* Demo class. Everything comes together here.
*/
public class Demo {
private static Dialog dialog;
public static void main(String[] args) {
configure();
runBusinessLogic();
}
/**
* The concrete factory is usually chosen depending on configuration or
* environment options.
*/
static void configure() {
if (System.getProperty("os.name").equals("Windows 10")) {
dialog = new WindowsDialog();
} else {
dialog = new HtmlDialog();
}
}
/**
* All of the client code should work with factories and products through
* abstract interfaces. This way it does not care which factory it works
* with and what kind of product it returns.
*/
static void runBusinessLogic() {
dialog.renderWindow();
}
}
OutputDemo.txt: 執行結果 (HtmlDialog)
<button>Test Button</button>
Click! Button says - 'Hello World!'
OutputDemo.png: 執行結果 (WindowsDialog)
與其他模式的關係
- 在許多設計工作的初期都會使用工廠方法模式 (較為簡單, 而且可以更方便地通過子類進行定製), 隨後演化為使用抽象工廠模式、 原型模式或生成器模式 (更靈活但更加複雜)。
- 抽象工廠模式通常基於一組工廠方法, 但你也可以使用原型模式來生成這些類的方法。
- 你可以同時使用工廠方法和迭代器模式來讓子類集合返回不同型別的迭代器, 並使得迭代器與集合相匹配。
- 原型並不基於繼承, 因此沒有繼承的缺點。 另一方面, 原型需要對被複制物件進行復雜的初始化。 工廠方法基於繼承, 但是它不需要初始化步驟。
- 工廠方法是模板方法模式的一種特殊形式。 同時, 工廠方法可以作為一個大型模板方法中的一個步驟。
Abstract Factory
背景
抽象工廠模式是一種建立型設計模式, 它能建立一系列相關的物件, 而無需指定其具體類。
假設你正在開發一款傢俱商店模擬器。 你的程式碼中包括一些類, 用於表示:
一系列相關產品, 例如 椅子Chair 、 沙發Sofa 和 咖啡桌Coffee Table 。
系列產品的不同變體。 例如, 你可以使用 現代Modern 、 維多利亞Victorian 、 裝飾風藝術ArtDeco等風格生成 椅子 、 沙發 和 咖啡桌 。
適用場景
- 如果程式碼需要與多個不同系列的相關產品互動, 但是由於無法提前獲取相關資訊, 或者出於對未來擴充套件性的考慮, 你不希望程式碼基於產品的具體類進行構建, 在這種情況下, 你可以使用抽象工廠。
- 如果你有一個基於一組抽象方法的類, 且其主要功能因此變得不明確, 那麼在這種情況下可以考慮使用抽象工廠模式。
例項-實現
使用示例: 抽象工廠模式在 Java 程式碼中很常見。 許多框架和程式庫會將它作為擴充套件和自定義其標準組件的一種方式。
以下是來自核心 Java 程式庫的一些示例:
javax.xml.parsers.DocumentBuilderFactory#newInstance()
javax.xml.transform.TransformerFactory#newInstance()
javax.xml.xpath.XPathFactory#newInstance()
識別方法: 我們可以通過方法來識別該模式——其會返回一個工廠物件。 接下來, 工廠將被用於建立特定的子元件。
跨平臺 GUI 元件系列及其建立方式
在本例中, 按鈕和複選框將被作為產品。 它們有兩個變體: macOS 版和 Windows 版。
抽象工廠定義了用於建立按鈕和複選框的介面。 而兩個具體工廠都會返回同一變體的兩個產品。
客戶端程式碼使用抽象介面與工廠和產品進行互動。 同樣的程式碼能與依賴於不同工廠物件型別的多種產品變體進行互動。
buttons: 第一個產品層次結構
buttons/Button.java
package com.zwt.buttons;
/**
* @author ML李嘉圖
* @version createtime: 2021-11-10
* Blog: https://www.cnblogs.com/zwtblog/
*/
/**
* Abstract Factory assumes that you have several families of products,
* structured into separate class hierarchies (Button/Checkbox). All products of
* the same family have the common interface.
* <p>
* This is the common interface for buttons family.
*/
public interface Button {
void paint();
}
buttons/MacOSButton.java
package com.zwt.buttons;
/**
* @author ML李嘉圖
* @version createtime: 2021-11-10
* Blog: https://www.cnblogs.com/zwtblog/
*/
/**
* All products families have the same varieties (MacOS/Windows).
* <p>
* This is a MacOS variant of a button.
*/
public class MacOSButton implements Button {
@Override
public void paint() {
System.out.println("You have created MacOSButton.");
}
}
buttons/WindowsButton.java
package refactoring_guru.abstract_factory.example.buttons;
package com.zwt.buttons;
/**
* @author ML李嘉圖
* @version createtime: 2021-11-10
* Blog: https://www.cnblogs.com/zwtblog/
*/
/**
* All products families have the same varieties (MacOS/Windows).
* <p>
* This is another variant of a button.
*/
public class WindowsButton implements Button {
@Override
public void paint() {
System.out.println("You have created WindowsButton.");
}
}
checkboxes: 第二個產品層次結構
checkboxes/Checkbox.java
package com.zwt.checkboxes;
/**
* @author ML李嘉圖
* @version createtime: 2021-11-10
* Blog: https://www.cnblogs.com/zwtblog/
*/
/**
* Checkboxes is the second product family. It has the same variants as buttons.
*/
public interface Checkbox {
void paint();
}
checkboxes/MacOSCheckbox.java
/**
* All products families have the same varieties (MacOS/Windows).
*
* This is a variant of a checkbox.
*/
public class MacOSCheckbox implements Checkbox {
@Override
public void paint() {
System.out.println("You have created MacOSCheckbox.");
}
}
checkboxes/WindowsCheckbox.java
/**
* All products families have the same varieties (MacOS/Windows).
*
* This is another variant of a checkbox.
*/
public class WindowsCheckbox implements Checkbox {
@Override
public void paint() {
System.out.println("You have created WindowsCheckbox.");
}
}
factories
factories/GUIFactory.java: 抽象工廠
package com.zwt.factories;
/**
* @author ML李嘉圖
* @version createtime: 2021-11-10
* Blog: https://www.cnblogs.com/zwtblog/
*/
import com.zwt.buttons.Button;
import com.zwt.checkboxes.Checkbox;
/**
* Abstract factory knows about all (abstract) product types.
*/
public interface GUIFactory {
Button createButton();
Checkbox createCheckbox();
}
factories/MacOSFactory.java: 具體工廠 ( macOS)
package com.zwt.factories;
/**
* @author ML李嘉圖
* @version createtime: 2021-11-10
* Blog: https://www.cnblogs.com/zwtblog/
*/
import com.zwt.buttons.Button;
import com.zwt.buttons.MacOSButton;
import com.zwt.checkboxes.Checkbox;
import com.zwt.checkboxes.MacOSCheckbox;
/**
* Each concrete factory extends basic factory and responsible for creating
* products of a single variety.
*/
public class MacOSFactory implements GUIFactory {
@Override
public Button createButton() {
return new MacOSButton();
}
@Override
public Checkbox createCheckbox() {
return new MacOSCheckbox();
}
}
factories/WindowsFactory.java: 具體工廠 (Windows)
package com.zwt.factories;
import com.zwt.buttons.Button;
import com.zwt.buttons.WindowsButton;
import com.zwt.checkboxes.Checkbox;
import com.zwt.checkboxes.WindowsCheckbox;
/**
* Each concrete factory extends basic factory and responsible for creating
* products of a single variety.
*/
public class WindowsFactory implements GUIFactory {
@Override
public Button createButton() {
return new WindowsButton();
}
@Override
public Checkbox createCheckbox() {
return new WindowsCheckbox();
}
}
app
app/Application.java: 客戶端程式碼
package com.zwt.app;
import com.zwt.buttons.Button;
import com.zwt.checkboxes.Checkbox;
import com.zwt.factories.GUIFactory;
/**
* Factory users don't care which concrete factory they use since they work with
* factories and products through abstract interfaces.
*/
public class Application {
private Button button;
private Checkbox checkbox;
public Application(GUIFactory factory) {
button = factory.createButton();
checkbox = factory.createCheckbox();
}
public void paint() {
button.paint();
checkbox.paint();
}
}
Demo.java: 程式配置
package com.zwt;
import com.zwt.app.Application;
import com.zwt.factories.GUIFactory;
import com.zwt.factories.MacOSFactory;
import com.zwt.factories.WindowsFactory;
/**
* Demo class. Everything comes together here.
*/
public class Demo {
/**
* Application picks the factory type and creates it in run time (usually at
* initialization stage), depending on the configuration or environment
* variables.
*/
private static Application configureApplication() {
Application app;
GUIFactory factory;
String osName = System.getProperty("os.name").toLowerCase();
if (osName.contains("mac")) {
factory = new MacOSFactory();
app = new Application(factory);
} else {
factory = new WindowsFactory();
app = new Application(factory);
}
return app;
}
public static void main(String[] args) {
Application app = configureApplication();
app.paint();
}
}
與其他模式的關係
- 在許多設計工作的初期都會使用工廠方法模式 (較為簡單, 而且可以更方便地通過子類進行定製), 隨後演化為使用抽象工廠模式、 原型模式或生成器模式 (更靈活但更加複雜)。
- 生成器重點關注如何分步生成複雜物件。 抽象工廠專門用於生產一系列相關物件。 抽象工廠會馬上返回產品, 生成器則允許你在獲取產品前執行一些額外構造步驟。
- 抽象工廠模式通常基於一組工廠方法, 但你也可以使用原型模式來生成這些類的方法。
- 當只需對客戶端程式碼隱藏子系統建立物件的方式時, 你可以使用抽象工廠來代替外觀模式。
- 你可以將抽象工廠和橋接模式搭配使用。 如果由橋接定義的抽象只能與特定實現合作, 這一模式搭配就非常有用。 在這種情況下, 抽象工廠可以對這些關係進行封裝, 並且對客戶端程式碼隱藏其複雜性。
- 抽象工廠、 生成器和原型都可以用單例模式來實現。
Builder
亦稱:建造者模式、Builder
背景
假設有這樣一個複雜物件, 在對其進行構造時需要對諸多成員變數和巢狀物件進行繁複的初始化工作。
這些初始化程式碼通常深藏於一個包含眾多引數且讓人基本看不懂的建構函式中;
甚至還有更糟糕的情況, 那就是這些程式碼散落在客戶端程式碼的多個位置。
生成器模式建議將物件構造程式碼從產品類中抽取出來, 並將其放在一個名為生成器的獨立物件中。
適用場景
- 使用生成器模式可避免 “重疊建構函式 (telescopic constructor)” 的出現。
- 當你希望使用程式碼建立不同形式的產品 (例如石頭或木頭房屋) 時, 可使用生成器模式。
- 使用生成器構造組合樹或其他複雜物件。
例項-實現
生成器在 Java 核心程式庫中得到了廣泛的應用:
java.lang.StringBuilder#append()
(非同步
)java.lang.StringBuffer#append()
(同步
)java.nio.ByteBuffer#put()
(還有CharBuffer
、ShortBuffer
、IntBuffer
、LongBuffer
、FloatBuffer
和DoubleBuffer
)javax.swing.GroupLayout.Group#addComponent()
java.lang.Appendable
的所有實現
識別方法: 生成器模式可以通過類來識別, 它擁有一個構建方法和多個配置結果物件的方法。 生成器方法通常支援鏈式程式設計 (例如 someBuilder->setValueA(1)->setValueB(2)->create()
)。
分步製造汽車
在本例中, 生成器模式允許你分步驟地製造不同型號的汽車。
示例還展示了生成器如何使用相同的生產過程製造不同型別的產品 (汽車手冊)。
主管控制著構造順序。 它知道製造各種汽車型號需要呼叫的生產步驟。 它僅與汽車的通用介面進行互動。 這樣就能將不同型別的生成器傳遞給主管了。
最終結果將從生成器物件中獲得, 因為主管不知道最終產品的型別。 只有生成器物件知道自己生成的產品是什麼。
builders
builders/Builder.java: 通用生成器介面
/**
* Builder interface defines all possible ways to configure a product.
*/
public interface Builder {
void setCarType(CarType type);
void setSeats(int seats);
void setEngine(Engine engine);
void setTransmission(Transmission transmission);
void setTripComputer(TripComputer tripComputer);
void setGPSNavigator(GPSNavigator gpsNavigator);
}
builders/CarBuilder.java: 汽車生成器
/**
* Concrete builders implement steps defined in the common interface.
*/
public class CarBuilder implements Builder {
private CarType type;
private int seats;
private Engine engine;
private Transmission transmission;
private TripComputer tripComputer;
private GPSNavigator gpsNavigator;
public void setCarType(CarType type) {
this.type = type;
}
@Override
public void setSeats(int seats) {
this.seats = seats;
}
@Override
public void setEngine(Engine engine) {
this.engine = engine;
}
@Override
public void setTransmission(Transmission transmission) {
this.transmission = transmission;
}
@Override
public void setTripComputer(TripComputer tripComputer) {
this.tripComputer = tripComputer;
}
@Override
public void setGPSNavigator(GPSNavigator gpsNavigator) {
this.gpsNavigator = gpsNavigator;
}
public Car getResult() {
return new Car(type, seats, engine, transmission, tripComputer, gpsNavigator);
}
}
builders/CarManualBuilder.java: 汽車手冊生成器
/**
* Unlike other creational patterns, Builder can construct unrelated products,
* which don't have the common interface.
*
* In this case we build a user manual for a car, using the same steps as we
* built a car. This allows to produce manuals for specific car models,
* configured with different features.
*/
public class CarManualBuilder implements Builder{
private CarType type;
private int seats;
private Engine engine;
private Transmission transmission;
private TripComputer tripComputer;
private GPSNavigator gpsNavigator;
@Override
public void setCarType(CarType type) {
this.type = type;
}
@Override
public void setSeats(int seats) {
this.seats = seats;
}
@Override
public void setEngine(Engine engine) {
this.engine = engine;
}
@Override
public void setTransmission(Transmission transmission) {
this.transmission = transmission;
}
@Override
public void setTripComputer(TripComputer tripComputer) {
this.tripComputer = tripComputer;
}
@Override
public void setGPSNavigator(GPSNavigator gpsNavigator) {
this.gpsNavigator = gpsNavigator;
}
public Manual getResult() {
return new Manual(type, seats, engine, transmission, tripComputer, gpsNavigator);
}
}
cars
cars/Car.java: 汽車產品
/**
* Car is a product class.
*/
public class Car {
private final CarType carType;
private final int seats;
private final Engine engine;
private final Transmission transmission;
private final TripComputer tripComputer;
private final GPSNavigator gpsNavigator;
private double fuel = 0;
public Car(CarType carType, int seats, Engine engine, Transmission transmission,
TripComputer tripComputer, GPSNavigator gpsNavigator) {
this.carType = carType;
this.seats = seats;
this.engine = engine;
this.transmission = transmission;
this.tripComputer = tripComputer;
if (this.tripComputer != null) {
this.tripComputer.setCar(this);
}
this.gpsNavigator = gpsNavigator;
}
public CarType getCarType() {
return carType;
}
public double getFuel() {
return fuel;
}
public void setFuel(double fuel) {
this.fuel = fuel;
}
public int getSeats() {
return seats;
}
public Engine getEngine() {
return engine;
}
public Transmission getTransmission() {
return transmission;
}
public TripComputer getTripComputer() {
return tripComputer;
}
public GPSNavigator getGpsNavigator() {
return gpsNavigator;
}
}
cars/Manual.java: 手冊產品
/**
* Car manual is another product. Note that it does not have the same ancestor
* as a Car. They are not related.
*/
public class Manual {
private final CarType carType;
private final int seats;
private final Engine engine;
private final Transmission transmission;
private final TripComputer tripComputer;
private final GPSNavigator gpsNavigator;
public Manual(CarType carType, int seats, Engine engine, Transmission transmission,
TripComputer tripComputer, GPSNavigator gpsNavigator) {
this.carType = carType;
this.seats = seats;
this.engine = engine;
this.transmission = transmission;
this.tripComputer = tripComputer;
this.gpsNavigator = gpsNavigator;
}
public String print() {
String info = "";
info += "Type of car: " + carType + "\n";
info += "Count of seats: " + seats + "\n";
info += "Engine: volume - " + engine.getVolume() + "; mileage - " + engine.getMileage() + "\n";
info += "Transmission: " + transmission + "\n";
if (this.tripComputer != null) {
info += "Trip Computer: Functional" + "\n";
} else {
info += "Trip Computer: N/A" + "\n";
}
if (this.gpsNavigator != null) {
info += "GPS Navigator: Functional" + "\n";
} else {
info += "GPS Navigator: N/A" + "\n";
}
return info;
}
}
cars/CarType.java
public enum CarType {
CITY_CAR, SPORTS_CAR, SUV
}
components
components/Engine.java: 產品特徵 1
/**
* Just another feature of a car.
*/
public class Engine {
private final double volume;
private double mileage;
private boolean started;
public Engine(double volume, double mileage) {
this.volume = volume;
this.mileage = mileage;
}
public void on() {
started = true;
}
public void off() {
started = false;
}
public boolean isStarted() {
return started;
}
public void go(double mileage) {
if (started) {
this.mileage += mileage;
} else {
System.err.println("Cannot go(), you must start engine first!");
}
}
public double getVolume() {
return volume;
}
public double getMileage() {
return mileage;
}
}
components/GPSNavigator.java: 產品特徵 2
/**
* Just another feature of a car.
*/
public class GPSNavigator {
private String route;
public GPSNavigator() {
this.route = "221b, Baker Street, London to Scotland Yard, 8-10 Broadway, London";
}
public GPSNavigator(String manualRoute) {
this.route = manualRoute;
}
public String getRoute() {
return route;
}
}
components/Transmission.java: 產品特徵 3
/**
* Just another feature of a car.
*/
public enum Transmission {
SINGLE_SPEED, MANUAL, AUTOMATIC, SEMI_AUTOMATIC
}
components/TripComputer.java: 產品特徵 4
/**
* Just another feature of a car.
*/
public class TripComputer {
private Car car;
public void setCar(Car car) {
this.car = car;
}
public void showFuelLevel() {
System.out.println("Fuel level: " + car.getFuel());
}
public void showStatus() {
if (this.car.getEngine().isStarted()) {
System.out.println("Car is started");
} else {
System.out.println("Car isn't started");
}
}
}
director
director/Director.java: 主管控制生成器
/**
* Director defines the order of building steps. It works with a builder object
* through common Builder interface. Therefore it may not know what product is
* being built.
*/
public class Director {
public void constructSportsCar(Builder builder) {
builder.setCarType(CarType.SPORTS_CAR);
builder.setSeats(2);
builder.setEngine(new Engine(3.0, 0));
builder.setTransmission(Transmission.SEMI_AUTOMATIC);
builder.setTripComputer(new TripComputer());
builder.setGPSNavigator(new GPSNavigator());
}
public void constructCityCar(Builder builder) {
builder.setCarType(CarType.CITY_CAR);
builder.setSeats(2);
builder.setEngine(new Engine(1.2, 0));
builder.setTransmission(Transmission.AUTOMATIC);
builder.setTripComputer(new TripComputer());
builder.setGPSNavigator(new GPSNavigator());
}
public void constructSUV(Builder builder) {
builder.setCarType(CarType.SUV);
builder.setSeats(4);
builder.setEngine(new Engine(2.5, 0));
builder.setTransmission(Transmission.MANUAL);
builder.setGPSNavigator(new GPSNavigator());
}
}
Demo.java: 客戶端程式碼
/**
* Demo class. Everything comes together here.
*/
public class Demo {
public static void main(String[] args) {
Director director = new Director();
// Director gets the concrete builder object from the client
// (application code). That's because application knows better which
// builder to use to get a specific product.
CarBuilder builder = new CarBuilder();
director.constructSportsCar(builder);
// The final product is often retrieved from a builder object, since
// Director is not aware and not dependent on concrete builders and
// products.
Car car = builder.getResult();
System.out.println("Car built:\n" + car.getCarType());
CarManualBuilder manualBuilder = new CarManualBuilder();
// Director may know several building recipes.
director.constructSportsCar(manualBuilder);
Manual carManual = manualBuilder.getResult();
System.out.println("\nCar manual built:\n" + carManual.print());
}
}
OutputDemo.txt: 執行結果
Car built:
SPORTS_CAR
Car manual built:
Type of car: SPORTS_CAR
Count of seats: 2
Engine: volume - 3.0; mileage - 0.0
Transmission: SEMI_AUTOMATIC
Trip Computer: Functional
GPS Navigator: Functional
與其他模式的關係
- 在許多設計工作的初期都會使用工廠方法模式 (較為簡單, 而且可以更方便地通過子類進行定製), 隨後演化為使用抽象工廠模式、 原型模式或生成器模式 (更靈活但更加複雜)。
- 生成器重點關注如何分步生成複雜物件。 抽象工廠專門用於生產一系列相關物件。 抽象工廠會馬上返回產品, 生成器則允許你在獲取產品前執行一些額外構造步驟。
- 你可以在建立複雜組合模式樹時使用生成器, 因為這可使其構造步驟以遞迴的方式執行。
- 你可以結合使用生成器和橋接模式: 主管類負責抽象工作, 各種不同的生成器負責實現工作。
- 抽象工廠、 生成器和原型都可以用單例模式來實現。
Prototype
原型模式是一種建立型設計模式, 使你能夠複製已有物件, 而又無需使程式碼依賴它們所屬的類。
背景
如果你有一個物件, 並希望生成與其完全相同的一個複製品, 你該如何實現呢?
首先, 你必須新建一個屬於相同類的物件。
然後, 你必須遍歷原始物件的所有成員變數, 並將成員變數值複製到新物件中。
不錯! 但有個小問題。
並非所有物件都能通過這種方式進行復制, 因為有些物件可能擁有私有成員變數, 它們在物件本身以外是不可見的。
直接複製還有另外一個問題。
因為你必須知道物件所屬的類才能建立複製品, 所以程式碼必須依賴該類。
即使你可以接受額外的依賴性,
那還有另外一個問題: 有時你只知道物件所實現的介面, 而不知道其所屬的具體類,
比如可向方法的某個引數傳入實現了某個介面的任何物件。
原型模式將克隆過程委派給被克隆的實際物件。 模式為所有支援克隆的物件聲明瞭一個通用介面, 該介面讓你能夠克隆物件, 同時又無需將程式碼和物件所屬類耦合。 通常情況下, 這樣的介面中僅包含一個 克隆
方法。
所有的類對 克隆
方法的實現都非常相似。 該方法會建立一個當前類的物件, 然後將原始物件所有的成員變數值複製到新建的類中。 你甚至可以複製私有成員變數, 因為絕大部分程式語言都允許物件訪問其同類物件的私有成員變數。
支援克隆的物件即為原型。 當你的物件有幾十個成員變數和幾百種類型時, 對其進行克隆甚至可以代替子類的構造。
其運作方式如下: 建立一系列不同型別的物件並不同的方式對其進行配置。 如果所需物件與預先配置的物件相同, 那麼你只需克隆原型即可, 無需新建一個物件。
適用場景
- 如果你需要複製一些物件, 同時又希望程式碼獨立於這些物件所屬的具體類, 可以使用原型模式。
- 如果子類的區別僅在於其物件的初始化方式, 那麼你可以使用該模式來減少子類的數量。 別人建立這些子類的目的可能是為了建立特定型別的物件。
例項-實現
使用示例: Java 的 Cloneable
(可克隆) 介面就是立即可用的原型模式。
任何類都可通過實現該介面來實現可被克隆的性質。
- java.lang.Object#clone() (類必須實現
java.lang.Cloneable
介面)
識別方法: 原型可以簡單地通過 clone
或 copy
等方法來識別。
複製圖形
讓我們來看看在不使用標準 Cloneable
介面的情況下如何實現原型模式。
shapes: 形狀列表
shapes/Shape.java: 通用形狀介面
import java.util.Objects;
public abstract class Shape {
public int x;
public int y;
public String color;
public Shape() {
}
public Shape(Shape target) {
if (target != null) {
this.x = target.x;
this.y = target.y;
this.color = target.color;
}
}
public abstract Shape clone();
@Override
public boolean equals(Object object2) {
if (!(object2 instanceof Shape)) return false;
Shape shape2 = (Shape) object2;
return shape2.x == x && shape2.y == y && Objects.equals(shape2.color, color);
}
}
shapes/Circle.java: 簡單形狀
public class Circle extends Shape {
public int radius;
public Circle() {
}
public Circle(Circle target) {
super(target);
if (target != null) {
this.radius = target.radius;
}
}
@Override
public Shape clone() {
return new Circle(this);
}
@Override
public boolean equals(Object object2) {
if (!(object2 instanceof Circle) || !super.equals(object2)) return false;
Circle shape2 = (Circle) object2;
return shape2.radius == radius;
}
}
shapes/Rectangle.java: 另一個形狀
public class Rectangle extends Shape {
public int width;
public int height;
public Rectangle() {
}
public Rectangle(Rectangle target) {
super(target);
if (target != null) {
this.width = target.width;
this.height = target.height;
}
}
@Override
public Shape clone() {
return new Rectangle(this);
}
@Override
public boolean equals(Object object2) {
if (!(object2 instanceof Rectangle) || !super.equals(object2)) return false;
Rectangle shape2 = (Rectangle) object2;
return shape2.width == width && shape2.height == height;
}
}
Demo.java: 克隆示例
import java.util.ArrayList;
import java.util.List;
public class Demo {
public static void main(String[] args) {
List<Shape> shapes = new ArrayList<>();
List<Shape> shapesCopy = new ArrayList<>();
Circle circle = new Circle();
circle.x = 10;
circle.y = 20;
circle.radius = 15;
circle.color = "red";
shapes.add(circle);
Circle anotherCircle = (Circle) circle.clone();
shapes.add(anotherCircle);
Rectangle rectangle = new Rectangle();
rectangle.width = 10;
rectangle.height = 20;
rectangle.color = "blue";
shapes.add(rectangle);
cloneAndCompare(shapes, shapesCopy);
}
private static void cloneAndCompare(List<Shape> shapes, List<Shape> shapesCopy) {
for (Shape shape : shapes) {
shapesCopy.add(shape.clone());
}
for (int i = 0; i < shapes.size(); i++) {
if (shapes.get(i) != shapesCopy.get(i)) {
System.out.println(i + ": Shapes are different objects (yay!)");
if (shapes.get(i).equals(shapesCopy.get(i))) {
System.out.println(i + ": And they are identical (yay!)");
} else {
System.out.println(i + ": But they are not identical (booo!)");
}
} else {
System.out.println(i + ": Shape objects are the same (booo!)");
}
}
}
}
OutputDemo.txt: 執行結果
0: Shapes are different objects (yay!)
0: And they are identical (yay!)
1: Shapes are different objects (yay!)
1: And they are identical (yay!)
2: Shapes are different objects (yay!)
2: And they are identical (yay!)
與其他模式的關係
- 在許多設計工作的初期都會使用工廠方法模式 (較為簡單, 而且可以更方便地通過子類進行定製), 隨後演化為使用抽象工廠模式、 原型模式或生成器模式 (更靈活但更加複雜)。
- 抽象工廠模式通常基於一組工廠方法, 但你也可以使用原型模式來生成這些類的方法。
- 原型可用於儲存命令模式的歷史記錄。
- 大量使用組合模式和裝飾模式的設計通常可從對於原型的使用中獲益。 你可以通過該模式來複制複雜結構, 而非從零開始重新構造。
- 原型並不基於繼承, 因此沒有繼承的缺點。 另一方面, 原型需要對被複制物件進行復雜的初始化。 工廠方法基於繼承, 但是它不需要初始化步驟。
- 有時候原型可以作為備忘錄模式的一個簡化版本, 其條件是你需要在歷史記錄中儲存的物件的狀態比較簡單, 不需要連結其他外部資源, 或者連結可以方便地重建。
- 抽象工廠、 生成器和原型都可以用單例模式來實現。
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;
}
}