1. 程式人生 > >設計模式之策略模式(轉)

設計模式之策略模式(轉)


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.EtchedBorderpaintBorder()方法:

// 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()方法使用它的元件引用獲取元件的陰影和高亮顏色資訊。

實現策略模式

策略模式相對比較簡單,在軟體中容易實現:

  1. 為你的策略物件定義Strategy介面
  2. 編寫ConcreteStrategy類實現Strategy介面
  3. 在你的Context類中,保持對“`Strategy“物件的私有引用。
  4. 在你的Context類中,實現Strategy物件的settter和getter方法。

Strategy介面定義了Strategy物件的行為;比如Swing邊框的Strategy介面為javax.swing.Border介面。

具體的ConcreteStrategy類實現了Strategy介面;比如,Swing邊框的LineBorderEtchedBorder類為ConcreteStrategy類。Context類使用Strategy物件;比如JComponent類為Context物件。

你也可以檢查一下你現有的類,看看它們是否是緊耦合的,這時可以考慮使用策略物件。通常情況下,這些包括switch語句的需要改進的地方與我在文章開頭討論的非常相似。

作業

一些Swing元件的渲染和編輯條件比其他的更加複雜。討論如何在列表類(javax.swing.JList)使用策略模式渲染列表項。


原文連結: javaworld 翻譯: ImportNew.com hejiani
譯文連結: http://www.importnew.com/12752.html
轉載請保留原文出處、譯者和譯文連結。]