Java設計模式之策略模式詳解
1984年,我以機械工程學位從大學畢業,開始了軟體工程師的職業生涯。自學C語言之後,1985年從事了用於Unix的50,000行使用者圖形介面(GUI)開發。整個過程非常輕鬆愉快。
1985年底,我的編碼工作完成了,之後我考慮開始其他的專案——或者說我認為我可以進行新的專案了。但很快我收到了一系列bug報告和新增的需求,為修正錯誤我開始努力閱讀這50,000行程式碼。這個工作卻非常艱難。
整個程式就像真正用卡片做成的房子一樣,幾乎每天都會轟然倒下。即使是最微小的變化我也要花費幾個小時來恢復程式的穩定性。
可能我碰巧發現了一個重要的軟體工程原則:開發階段輕鬆愉快,然後專案部署後就去找下一份工作。然而,實際上我的困難源於我對物件導向(object-oriented,OO)軟體開發基本原則——封裝的無知。我的程式就是個大型的switch語句集合,在不同情況下呼叫不同的函式——這導致了程式碼的緊耦合以及整個軟體難以適應變化。
在Java設計模式這篇文章,我會討論策略模式,它可能是最基礎的設計模式吧。如果在1984年的時候我知道策略模式的話,有很大一部分工作就可以避免了。
策略模式
在GOF的設計模式一書的第一章,作者討論了若干條OO設計原則,這些原則包括了很多設計模式的核心。策略模式體現了這樣兩個原則——封裝變化和對介面程式設計而不是對實現程式設計。設計模式的作者把策略模式定義如下:
Define a family of algorithms, encapsulate each one, and make them interchangeable. [The] Strategy [pattern] lets the algorithm vary independently from clients that use it.(策略模式定義了一系列的演算法,並將每一個演算法封裝起來,而且使它們還可以相互替換。策略模式讓演算法獨立於使用它的客戶而變化。)
策略模式將整個軟體構建為可互換部分的鬆耦合的集合,而不是單一的緊耦合系統。鬆耦合的軟體可擴充套件性更好,更易於維護且重用性好。
為理解策略模式,我們首先看一下Swing如何使用策略模式繪製元件周圍的邊框。接著討論Swing使用策略模式帶來的好處,最後說明在你的軟體中如何實現策略模式。
Swing 邊框
幾乎所有的Swing元件都可以繪製邊框,包括皮膚、按鈕、列表等等。Swing也提供了元件的多種邊框型別:bevel(斜面邊框),etched(浮雕化邊框),line(線邊框),titled(標題邊框)以及compound(複合邊框)等。Swing元件的邊框使用JComponent
類繪製,它是所有Swing元件的基類,實現了所有Swing元件的常用功能。
JComponent
實現了paintBorder()
,該方法用來繪製元件周圍的邊框。假如Swing的建立者使用類似示例1的方法實現paintBorder()
:
// A hypothetical JComponent.paintBorder method protected void paintBorder(Graphics g) { switch(getBorderType()) { case LINE_BORDER: paintLineBorder(g); break; case ETCHED_BORDER: paintEtchedBorder(g); break; case TITLED_BORDER: paintTitledBorder(g); break; ... } }
示例1 繪製Swing邊框的錯誤方式
示例1中JComponent.paintBorder()
方法在JComponent
硬編碼了邊框的繪製。
如果你想實現一種新的邊框型別,可以想見這樣的結果——需要修改JComponent
類的至少三個地方:首先,新增與新邊框型別相關的新的整數值。第二,switch語句中新增case語句。第三,實現paintXXXBorder()
方法,XXX
表示邊框型別。
很顯然,擴充套件前面的paintBorder()
吃力不討好。你會發現不僅paintBorder()
很難擴充套件新型別,而且JComponent
類不是你首先要修改的位置,它是Swing工具包的一部分,這意味著你將不得不重新編譯類和重建全部工具包。你也必須要求你的使用者使用你自己的Swing版本而不是標準版,Swing下一次釋出後這些工作依然要做。此外,因為你為JComponent
類新增了新的邊框繪製功能,無論你是否喜歡每個Swing元件都可以訪問該功能的現狀——你不能把你的新邊框限制到具體的元件型別。
可見,如果JComponent
類使用示例1中的switch語句實現其功能,Swing元件就不能被擴充套件。
那麼運用OO思想如何實現呢?使用策略模式解耦JComponent
與邊框繪製的程式碼,這樣無需修改JComponent
類就實現了邊框繪製演算法的多樣性。使用策略模式封裝變化,即繪製邊框方法的變化,以及對介面程式設計而不是對實現程式設計,提供一個Border
介面。接下來就看看JComponent
如何使用策略模式繪製邊框。示例2為JComponent.paintBorder()
方法:
// The actual implementation of the JComponent.paintBorder() method protected void paintBorder(Graphics g) { Border border = getBorder(); if (border != null) { border.paintBorder(this, g, 0, 0, getWidth(), getHeight()); } }
示例2 繪製Swing邊框的正確方式
前面的paintBorder()
方法繪製了有邊框物體的邊框。在這種情況下,邊框物件封裝了邊框繪製演算法,而不是JComponent
類。
注意JComponent
把自身的引用傳遞給Border.paintBorder()
,這樣邊框物件就可以從元件獲取資訊,這種方式通常稱為委託。通過傳遞自身的引用,一個物件將功能委託給另一物件。
JComponent
類引用了邊框物件,作為JComponent.getBorder()
方法的返回值,示例3為相關的setter方法。
... private Border border; ... public void setBorder(Border border) { Border oldBorder = this.border; this.border = border; firePropertyChange("border", oldBorder, border); if (border != oldBorder) { if (border == null || oldBorder == null || !(border.getBorderInsets(this). equals(oldBorder.getBorderInsets(this)))) { revalidate(); } repaint(); } } ... public Border getBorder() { return border; }
示例3 Swing元件邊框的setter和getter方法
使用JComponent.setBorder()
設定元件的邊框時,JComponent
類觸發屬性改變事件,如果新的邊框與舊邊框不同,元件重新繪製。getBorder()
方法簡單返回Border
引用。
圖1為邊框和JComponent
類之間關係的類圖。
圖1 Swing邊框
JComponent
類包含Border
物件的私有引用。注意由於Border
是介面不是類,Swing元件可以擁有任意型別的實現了Border
介面的邊框(這就是對介面程式設計而不是對實現程式設計的含義)。
我們已經知道了JComponent
是如何通過策略模式實現邊框繪製的,下面建立一種新邊框型別來測試一下它的可擴充套件性。
建立新的邊框型別
圖2 新邊框型別
圖2顯示了具有三個皮膚的Swing應用。每個皮膚設定自定義的邊框,每個邊框對應一個HandleBorder
例項。繪圖程式通常使用handleBorder物件來移動物件和改變物件大小。
示例4為HandleBorder
類:
import java.awt.*; import javax.swing.*; import javax.swing.border.*; public class HandleBorder extends AbstractBorder { protected Color lineColor; protected int thick; public HandleBorder() { this(Color.black, 6); } public HandleBorder(Color lineColor, int thick) { this.lineColor = lineColor; this.thick = thick; } public void paintBorder(Component component, Graphics g, int x, int y, int w, int h) { Graphics copy = g.create(); if(copy != null) { try { copy.translate(x,y); paintRectangle(component,copy,w,h); paintHandles(component,copy,w,h); } finally { copy.dispose(); } } } public Insets getBorderInsets() { return new Insets(thick,thick,thick,thick); } protected void paintRectangle(Component c, Graphics g, int w, int h) { g.setColor(lineColor); g.drawRect(thick/2,thick/2,w-thick-1,h-thick-1); } protected void paintHandles(Component c, Graphics g, int w, int h) { g.setColor(lineColor); g.fillRect(0,0,thick,thick); // upper left g.fillRect(w-thick,0,thick,thick); // upper right g.fillRect(0,h-thick,thick,thick); // lower left g.fillRect(w-thick,h-thick,thick,thick); // lower right g.fillRect(w/2-thick/2,0,thick,thick); // mid top g.fillRect(0,h/2-thick/2,thick,thick); // mid left g.fillRect(w/2-thick/2,h-thick,thick,thick); // mid bottom g.fillRect(w-thick,h/2-thick/2,thick,thick); // mid right } }
示例4 HandleBorder類
HandleBorder
類繼承自javax.swing.border.AbstractBorder
,覆蓋paintBorder()
和getBorderInsets()
方法。儘管HandleBorder
的實現不太重要,但是我們可以容易地建立新邊框型別,因為Swing使用了策略模式繪製元件邊框。
示例5為Swing應用。
import javax.swing.*; import javax.swing.border.*; import java.awt.*; import java.awt.event.*; public class Test extends JFrame { public static void main(String[] args) { JFrame frame = new Test(); frame.setBounds(100, 100, 500, 200); frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); frame.show(); } public Test() { super("Creating a New Border Type"); Container contentPane = getContentPane(); JPanel[] panels = { new JPanel(), new JPanel(), new JPanel() }; Border[] borders = { new HandleBorder(), new HandleBorder(Color.red, 8), new HandleBorder(Color.blue, 10) }; contentPane.setLayout( new FlowLayout(FlowLayout.CENTER,20,20)); for(int i=0; i < panels.length; ++i) { panels[i].setPreferredSize(new Dimension(100,100)); panels[i].setBorder(borders[i]); contentPane.add(panels[i]); } } }
示例5 使用handleBorder
前面的應用建立了三個皮膚(javax.swing.JPanel
例項)和三個邊框(HandleBorder
例項)。注意通過呼叫JComponent.setBorder()
可以為皮膚簡單設定具體的邊框。
回想一下示例2,當JComponent
呼叫Border.paintBorder()
時,元件引用傳遞給元件的邊框——一種委託方式。正如我前面提到的,開發人員經常將策略模式與委託共同使用。該HandleBorder
類未使用元件引用,但是其他邊框會用到引用從元件獲取資訊。比如示例6為這種型別邊框javax.swing.border.EtchedBorder
的paintBorder()
方法:
// The following listing is from // javax.swing.border.EtchedBorder public void paintBorder(Component component, Graphics g, int x, int y, int width, int height) { int w = width; int h = height; g.translate(x, y); g.setColor(etchType == LOWERED? getShadowColor(component) : getHighlightColor(component)); g.drawRect(0, 0, w-2, h-2); g.setColor(etchType == LOWERED? getHighlightColor(component) : getShadowColor(component)); g.drawLine(1, h-3, 1, 1); g.drawLine(1, 1, w-3, 1); g.drawLine(0, h-1, w-1, h-1); g.drawLine(w-1, h-1, w-1, 0); g.translate(-x, -y); }
示例6 從元件獲取資訊的Swing邊框
javax.swing.border.EtchedBorder.paintBorder()
方法使用它的元件引用獲取元件的陰影和高亮顏色資訊。
實現策略模式
策略模式相對比較簡單,在軟體中容易實現:
- 為你的策略物件定義
Strategy
介面 - 編寫
ConcreteStrategy
類實現Strategy
介面 - 在你的
Context
類中,保持對“`Strategy“物件的私有引用。 - 在你的
Context
類中,實現Strategy
物件的settter和getter方法。
Strategy
介面定義了Strategy
物件的行為;比如Swing邊框的Strategy
介面為javax.swing.Border
介面。
具體的ConcreteStrategy
類實現了Strategy
介面;比如,Swing邊框的LineBorder
和EtchedBorder
類為ConcreteStrategy
類。Context
類使用Strategy
物件;比如JComponent
類為Context
物件。
你也可以檢查一下你現有的類,看看它們是否是緊耦合的,這時可以考慮使用策略物件。通常情況下,這些包括switch語句的需要改進的地方與我在文章開頭討論的非常相似。
作業
一些Swing元件的渲染和編輯條件比其他的更加複雜。討論如何在列表類(javax.swing.JList
)使用策略模式渲染列表項。
上一次的作業
上一次的作業要求重新實現TableBubbleSortDecorator
。在“裝飾你的程式碼”一文首先討論了JDK內建的對代理模式的支援。
簡單來說,我建立了抽象類Decorator
實現java.lang.reflect.InvocationHandler
介面。Decorator
類引用了裝飾物件(或者說代理模式中的真實物件)。示例1H為Decorator
類。
import java.lang.reflect.InvocationHandler; public abstract class Decorator implements InvocationHandler { // The InvocationHandler interface defines one method: // invoke(Object proxy, Method method, Object[] args). That // method must be implemented by concrete (meaning not // abstract) extensions of this class. private Object decorated; protected Decorator(Object decorated) { this.decorated = decorated; } protected synchronized Object getDecorated() { return decorated; } protected synchronized void setDecorated(Object decorated) { this.decorated = decorated; } }
示例1H 抽象裝飾器類
儘管Decorator
類實現了InvocationHandler
介面,但是它沒有實現該介面的唯一方法invoke(Object proxy, Method method, Object[] methodArguments)
。因為Decorator
類是抽象的,Decorator
的擴充套件是具體類的話必須實現invoke()
方法。
Decorator
類是所有裝飾器的基類。示例2H為Decorator
類的擴充套件,具體的表排序裝飾器。注意TableSortDecorator
沒有實現invoke()
方法,它是抽象的。
import javax.swing.table.TableModel; import javax.swing.event.TableModelListener; public abstract class TableSortDecorator extends Decorator implements TableModelListener { // Concrete extensions of this class must implement // tableChanged from TableModelListener abstract public void sort(int column); public TableSortDecorator(TableModel realModel) { super(realModel); } }
示例2H 修正的TableSortDecorator
現在可以使用JDK內建的對代理模式的支援實現TableBubbleSortDecorator
:
import java.lang.reflect.Method; import javax.swing.table.TableModel; import javax.swing.event.TableModelEvent; public class TableBubbleSortDecorator extends TableSortDecorator { private int indexes[]; private static String GET_VALUE_AT = "getValueAt"; private static String SET_VALUE_AT = "setValueAt"; public TableBubbleSortDecorator(TableModel model) { super(model); allocate(); } // tableChanged is defined in TableModelListener, which // is implemented by TableSortDecorator. public void tableChanged(TableModelEvent e) { allocate(); } // invoke() is defined by the java.lang.reflect.InvocationHandler // interface; that interface is implemented by the // (abstract) Decorator class. Decorator is the superclass // of TableSortDecorator. public Object invoke(Object proxy, Method method, Object[] args) { Object result = null; TableModel model = (TableModel)getDecorated(); if(GET_VALUE_AT.equals(method.getName())) { Integer row = (Integer)args[0], col = (Integer)args[1]; result = model.getValueAt(indexes[row.intValue()], col.intValue()); } else if(SET_VALUE_AT.equals(method.getName())) { Integer row = (Integer)args[1], col = (Integer)args[2]; model.setValueAt(args[0], indexes[row.intValue()], col.intValue()); } else { try { result = method.invoke(model, args); } catch(Exception ex) { ex.printStackTrace(System.err); } } return result; } // The following methods perform the bubble sort ... public void sort(int column) { TableModel model = (TableModel)getDecorated(); int rowCount = model.getRowCount(); for(int i=0; i < rowCount; i++) { for(int j = i+1; j < rowCount; j++) { if(compare(indexes[i], indexes[j], column) < 0) { swap(i,j); } } } } private void swap(int i, int j) { int tmp = indexes[i]; indexes[i] = indexes[j]; indexes[j] = tmp; } private int compare(int i, int j, int column) { TableModel realModel = (TableModel)getDecorated(); Object io = realModel.getValueAt(i,column); Object jo = realModel.getValueAt(j,column); int c = jo.toString().compareTo(io.toString()); return (c < 0) ? -1 : ((c > 0) ? 1 : 0); } private void allocate() { indexes = new int[((TableModel)getDecorated()). getRowCount()]; for(int i=0; i < indexes.length; ++i) { indexes[i] = i; } } }
示例3H 修正的TableBubbleSortDecorator
使用JDK內建的對代理模式的支援和設計良好的基類,通過繼承Decorator
及實現invoke()
方法很容易實現裝飾器。
郵件
給我的一封郵件裡這樣寫到:
根據我在樹上選擇的節點工具欄要顯示特定的按鈕。我建立了工具欄裝飾器,它的建構函式引數為JToolBar
工具欄。裝飾器包含一個showButtonForNode()
方法根據節點改變按鈕。我呼叫在樹的選擇監聽器的valueChanged()
方法中呼叫showButtonForNode()
方法。
這樣使用裝飾器模式正確嗎?
很多設計模式可以達到功能擴充套件的目的;比如在Java設計模式中,你已經知道如何使用代理模式,裝飾器模式和策略模式來擴充套件功能。由於他們都可以實現相同的目標(功能擴充套件),在具體情況下使用哪個模式就很難判斷。
裝飾器模式的主要解決問題的點在於:在執行時結合多種行為;比如理解代理設計模式一文的“上一次得作業”部分,我展示了Swing表格排序和過濾相結合的方法。
TableSortDecorator sortDecorator = new TableBubbleSortDecorator(table.getModel()); TableFilterDecorator filterDecorator = new TableHighPriceFilter(sortDecorator); table.setModel(filterDecorator);
前面的程式碼中,過濾裝飾器裝飾了排序裝飾器,排序裝飾器裝飾了表格模型;結果表格模型可以排序和過濾資料。
對於郵件中的問題,使用工具欄按鈕與其他行為組合不太合適,所以裝飾器模式可能不合適。這種情況代理模式看來更好,在編譯階段而不是執行時就可以獲取代理和真實物件的關係,從而擴充套件功能。
相關文章
- JAVA設計模式之策略模式Java設計模式
- Java設計模式之策略模式示例Java設計模式
- Java設計模式之(十四)——策略模式Java設計模式
- Java設計模式之策略模式(Strategy)Java設計模式
- Python 中的設計模式詳解之:策略模式Python設計模式
- 設計模式之策略模式設計模式
- 設計模式之【策略模式】設計模式
- 【設計模式之策略模式】設計模式
- Java設計模式-策略模式分析Java設計模式
- Java設計模式6:策略模式Java設計模式
- PHP 設計模式之策略模式PHP設計模式
- JavaScript 設計模式之策略模式JavaScript設計模式
- Javascript設計模式之策略模式JavaScript設計模式
- 略懂設計模式之策略模式設計模式
- 設計模式系列之「策略模式」設計模式
- 設計模式之單例模式詳解設計模式單例
- 【經典案例】Python詳解設計模式:策略模式Python設計模式
- 我的Java設計模式-策略模式Java設計模式
- python設計模式之策略模式Python設計模式
- 設計模式漫談之策略模式設計模式
- javascript設計模式 之 2 策略模式JavaScript設計模式
- 23種設計模式之策略模式設計模式
- 我學設計模式 之 策略模式設計模式
- 【java設計模式】(7)---策略模式(案例解析)Java設計模式
- Java ”框架 = 註解 + 反射 + 設計模式“ 之 註解詳解Java框架反射設計模式
- JavaScript設計模式經典之策略模式JavaScript設計模式
- 1/24 設計模式之策略設計模式 Strategy Pattern設計模式
- 設計模式——策略模式設計模式
- 設計模式(策略模式)設計模式
- 設計模式-策略模式設計模式
- 設計模式🔫---策略模式設計模式
- Java中的設計模式詳解Java設計模式
- Java程式設計之設計模式之工廠方法模式全解Java程式設計設計模式
- 設計模式 - 代理模式詳解設計模式
- 設計模式:代理模式詳解設計模式
- Java設計模式實現之二--策略模式Java設計模式
- Java設計模式之builder模式Java設計模式UI
- Java設計模式之代理模式Java設計模式