首頁  >  文章  >  Java  >  Java設計模式之策略模式的詳細介紹

Java設計模式之策略模式的詳細介紹

黄舟
黄舟原創
2017-03-23 11:09:421575瀏覽

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)使用策略模式渲染列表项。

上一次的作业

上一次的作业要求重新实现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設計模式之策略模式的詳細介紹的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn