UDN-企业互联网技术人气社区

板块导航

浏览  : 1120
回复  : 0

[讨论交流] Java设计模式之策略模式详解

[复制链接]
哥屋恩的头像 楼主
发表于 2016-11-1 21:23:56 | 显示全部楼层 |阅读模式
  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():

  1.   // A hypothetical JComponent.paintBorder method

  2.   protected void paintBorder(Graphics g) {

  3.   switch(getBorderType()) {

  4.   case LINE_BORDER: paintLineBorder(g);

  5.   break;

  6.   case ETCHED_BORDER: paintEtchedBorder(g);

  7.   break;

  8.   case TITLED_BORDER: paintTitledBorder(g);

  9.   break;

  10.   ...

  11.   }

  12.   }
复制代码


  示例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()方法:

  1.   // The actual implementation of the JComponent.paintBorder() method

  2.   protected void paintBorder(Graphics g) {

  3.   Border border = getBorder();

  4.   if (border != null) {

  5.   border.paintBorder(this, g, 0, 0, getWidth(), getHeight());

  6.   }

  7.   }
复制代码


  示例2 绘制Swing边框的正确方式

  前面的paintBorder()方法绘制了有边框物体的边框。在这种情况下,边框对象封装了边框绘制算法,而不是JComponent类。

  注意JComponent把自身的引用传递给Border.paintBorder(),这样边框对象就可以从组件获取信息,这种方式通常称为委托。通过传递自身的引用,一个对象将功能委托给另一对象。

  JComponent类引用了边框对象,作为JComponent.getBorder()方法的返回值,示例3为相关的setter方法。

  1.   ...

  2.   private Border border;

  3.   ...

  4.   public void setBorder(Border border) {

  5.   Border oldBorder = this.border;

  6.   this.border = border;

  7.   firePropertyChange("border", oldBorder, border);

  8.   if (border != oldBorder) {

  9.   if (border == null || oldBorder == null || !(border.getBorderInsets(this).

  10.   equals(oldBorder.getBorderInsets(this)))) {

  11.   revalidate();

  12.   }

  13.   repaint();

  14.   }

  15.   }

  16.   ...

  17.   public Border getBorder() {

  18.   return border;

  19.   }
复制代码


  示例3 Swing组件边框的setter和getter方法

  使用JComponent.setBorder()设置组件的边框时,JComponent类触发属性改变事件,如果新的边框与旧边框不同,组件重新绘制。getBorder()方法简单返回Border引用。

  图1为边框和JComponent类之间关系的类图。

b.jpg
  图1 Swing边框

  JComponent类包含Border对象的私有引用。注意由于Border是接口不是类,Swing组件可以拥有任意类型的实现了Border接口的边框(这就是对接口编程而不是对实现编程的含义)。

  我们已经知道了JComponent是如何通过策略模式实现边框绘制的,下面创建一种新边框类型来测试一下它的可扩展性。

  创建新的边框类型

a.jpg
  图2 新边框类型

  图2显示了具有三个面板的Swing应用。每个面板设置自定义的边框,每个边框对应一个HandleBorder实例。绘图程序通常使用handleBorder对象来移动对象和改变对象大小。

  示例4为HandleBorder类:

  1.   import java.awt.*;

  2.   import javax.swing.*;

  3.   import javax.swing.border.*;

  4.   public class HandleBorder extends AbstractBorder {

  5.   protected Color lineColor;

  6.   protected int thick;

  7.   public HandleBorder() {

  8.   this(Color.black, 6);

  9.   }

  10.   public HandleBorder(Color lineColor, int thick) {

  11.   this.lineColor = lineColor;

  12.   this.thick = thick;

  13.   }

  14.   public void paintBorder(Component component,

  15.   Graphics g, int x, int y, int w, int h) {

  16.   Graphics copy = g.create();

  17.   if(copy != null) {

  18.   try {

  19.   copy.translate(x,y);

  20.   paintRectangle(component,copy,w,h);

  21.   paintHandles(component,copy,w,h);

  22.   }

  23.   finally {

  24.   copy.dispose();

  25.   }

  26.   }

  27.   }

  28.   public Insets getBorderInsets() {

  29.   return new Insets(thick,thick,thick,thick);

  30.   }

  31.   protected void paintRectangle(Component c, Graphics g,

  32.   int w, int h) {

  33.   g.setColor(lineColor);

  34.   g.drawRect(thick/2,thick/2,w-thick-1,h-thick-1);

  35.   }

  36.   protected void paintHandles(Component c, Graphics g,

  37.   int w, int h) {

  38.   g.setColor(lineColor);

  39.   g.fillRect(0,0,thick,thick); // upper left

  40.   g.fillRect(w-thick,0,thick,thick); // upper right

  41.   g.fillRect(0,h-thick,thick,thick); // lower left

  42.   g.fillRect(w-thick,h-thick,thick,thick); // lower right

  43.   g.fillRect(w/2-thick/2,0,thick,thick); // mid top

  44.   g.fillRect(0,h/2-thick/2,thick,thick); // mid left

  45.   g.fillRect(w/2-thick/2,h-thick,thick,thick); // mid bottom

  46.   g.fillRect(w-thick,h/2-thick/2,thick,thick); // mid right

  47.   }

  48.   }
复制代码


  示例4 HandleBorder类

  HandleBorder类继承自javax.swing.border.AbstractBorder,覆盖paintBorder()和getBorderInsets()方法。尽管HandleBorder的实现不太重要,但是我们可以容易地创建新边框类型,因为Swing使用了策略模式绘制组件边框。

  示例5为Swing应用。

  1.   import javax.swing.*;

  2.   import javax.swing.border.*;

  3.   import java.awt.*;

  4.   import java.awt.event.*;

  5.   public class Test extends JFrame {

  6.   public static void main(String[] args) {

  7.   JFrame frame = new Test();

  8.   frame.setBounds(100, 100, 500, 200);

  9.   frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

  10.   frame.show();

  11.   }

  12.   public Test() {

  13.   super("Creating a New Border Type");

  14.   Container contentPane = getContentPane();

  15.   JPanel[] panels = { new JPanel(),

  16.   new JPanel(), new JPanel() };

  17.   Border[] borders = { new HandleBorder(),

  18.   new HandleBorder(Color.red, 8),

  19.   new HandleBorder(Color.blue, 10) };

  20.   contentPane.setLayout(

  21.   new FlowLayout(FlowLayout.CENTER,20,20));

  22.   for(int i=0; i < panels.length; ++i) {

  23.   panels[i].setPreferredSize(new Dimension(100,100));

  24.   panels[i].setBorder(borders[i]);

  25.   contentPane.add(panels[i]);

  26.   }

  27.   }

  28.   }
复制代码


  示例5 使用handleBorder

  前面的应用创建了三个面板(javax.swing.JPanel实例)和三个边框(HandleBorder实例)。注意通过调用JComponent.setBorder()可以为面板简单设置具体的边框。

  回想一下示例2,当JComponent调用Border.paintBorder()时,组件引用传递给组件的边框——一种委托方式。正如我前面提到的,开发人员经常将策略模式与委托共同使用。该HandleBorder类未使用组件引用,但是其他边框会用到引用从组件获取信息。比如示例6为这种类型边框javax.swing.border.EtchedBorder的paintBorder()方法:

  1.   // The following listing is from

  2.   // javax.swing.border.EtchedBorder

  3.   public void paintBorder(Component component, Graphics g, int x, int y,

  4.   int width, int height) {

  5.   int w = width;

  6.   int h = height;

  7.   g.translate(x, y);

  8.   g.setColor(etchType == LOWERED? getShadowColor(component) : getHighlightColor(component));

  9.   g.drawRect(0, 0, w-2, h-2);

  10.   g.setColor(etchType == LOWERED? getHighlightColor(component) : getShadowColor(component));

  11.   g.drawLine(1, h-3, 1, 1);

  12.   g.drawLine(1, 1, w-3, 1);

  13.   g.drawLine(0, h-1, w-1, h-1);

  14.   g.drawLine(w-1, h-1, w-1, 0);

  15.   g.translate(-x, -y);

  16.   }
复制代码


  示例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类。

  1.   import java.lang.reflect.InvocationHandler;

  2.   public abstract class Decorator implements InvocationHandler {

  3.   // The InvocationHandler interface defines one method:

  4.   // invoke(Object proxy, Method method, Object[] args). That

  5.   // method must be implemented by concrete (meaning not

  6.   // abstract) extensions of this class.

  7.   private Object decorated;

  8.   protected Decorator(Object decorated) {

  9.   this.decorated = decorated;

  10.   }

  11.   protected synchronized Object getDecorated() {

  12.   return decorated;

  13.   }

  14.   protected synchronized void setDecorated(Object decorated) {

  15.   this.decorated = decorated;

  16.   }

  17.   }
复制代码


  示例1H 抽象装饰器类

  尽管Decorator类实现了InvocationHandler接口,但是它没有实现该接口的唯一方法invoke(Object proxy, Method method, Object[] methodArguments)。因为Decorator类是抽象的,Decorator的扩展是具体类的话必须实现invoke()方法。

  Decorator类是所有装饰器的基类。示例2H为Decorator类的扩展,具体的表排序装饰器。注意TableSortDecorator没有实现invoke()方法,它是抽象的。

  1.   import javax.swing.table.TableModel;

  2.   import javax.swing.event.TableModelListener;

  3.   public abstract class TableSortDecorator

  4.   extends Decorator

  5.   implements TableModelListener {

  6.   // Concrete extensions of this class must implement

  7.   // tableChanged from TableModelListener

  8.   abstract public void sort(int column);

  9.   public TableSortDecorator(TableModel realModel) {

  10.   super(realModel);

  11.   }

  12.   }
复制代码


  示例2H 修正的TableSortDecorator

  现在可以使用JDK内建的对代理模式的支持实现TableBubbleSortDecorator:

  1.   import java.lang.reflect.Method;

  2.   import javax.swing.table.TableModel;

  3.   import javax.swing.event.TableModelEvent;

  4.   public class TableBubbleSortDecorator extends TableSortDecorator {

  5.   private int indexes[];

  6.   private static String GET_VALUE_AT = "getValueAt";

  7.   private static String SET_VALUE_AT = "setValueAt";

  8.   public TableBubbleSortDecorator(TableModel model) {

  9.   super(model);

  10.   allocate();

  11.   }

  12.   // tableChanged is defined in TableModelListener, which

  13.   // is implemented by TableSortDecorator.

  14.   public void tableChanged(TableModelEvent e) {

  15.   allocate();

  16.   }

  17.   // invoke() is defined by the java.lang.reflect.InvocationHandler

  18.   // interface; that interface is implemented by the

  19.   // (abstract) Decorator class. Decorator is the superclass

  20.   // of TableSortDecorator.

  21.   public Object invoke(Object proxy, Method method,

  22.   Object[] args) {

  23.   Object result = null;

  24.   TableModel model = (TableModel)getDecorated();

  25.   if(GET_VALUE_AT.equals(method.getName())) {

  26.   Integer row = (Integer)args[0],

  27.   col = (Integer)args[1];

  28.   result = model.getValueAt(indexes[row.intValue()],

  29.   col.intValue());

  30.   }

  31.   else if(SET_VALUE_AT.equals(method.getName())) {

  32.   Integer row = (Integer)args[1],

  33.   col = (Integer)args[2];

  34.   model.setValueAt(args[0], indexes[row.intValue()],

  35.   col.intValue());

  36.   }

  37.   else {

  38.   try {

  39.   result = method.invoke(model, args);

  40.   }

  41.   catch(Exception ex) {

  42.   ex.printStackTrace(System.err);

  43.   }

  44.   }

  45.   return result;

  46.   }

  47.   // The following methods perform the bubble sort ...

  48.   public void sort(int column) {

  49.   TableModel model = (TableModel)getDecorated();

  50.   int rowCount = model.getRowCount();

  51.   for(int i=0; i < rowCount; i++) {

  52.   for(int j = i+1; j < rowCount; j++) {

  53.   if(compare(indexes[i], indexes[j], column) < 0) {

  54.   swap(i,j);

  55.   }

  56.   }

  57.   }

  58.   }

  59.   private void swap(int i, int j) {

  60.   int tmp = indexes[i];

  61.   indexes[i] = indexes[j];

  62.   indexes[j] = tmp;

  63.   }

  64.   private int compare(int i, int j, int column) {

  65.   TableModel realModel = (TableModel)getDecorated();

  66.   Object io = realModel.getValueAt(i,column);

  67.   Object jo = realModel.getValueAt(j,column);

  68.   int c = jo.toString().compareTo(io.toString());

  69.   return (c < 0) ? -1 : ((c > 0) ? 1 : 0);

  70.   }

  71.   private void allocate() {

  72.   indexes = new int[((TableModel)getDecorated()).

  73.   getRowCount()];

  74.   for(int i=0; i < indexes.length; ++i) {

  75.   indexes[i] = i;

  76.   }

  77.   }

  78.   }
复制代码


  示例3H 修正的TableBubbleSortDecorator

  使用JDK内建的对代理模式的支持和设计良好的基类,通过继承Decorator及实现invoke()方法很容易实现装饰器。

  邮件

  给我的一封邮件里这样写到:

  根据我在树上选择的节点工具栏要显示特定的按钮。我创建了工具栏装饰器,它的构造函数参数为JToolBar工具栏。装饰器包含一个showButtonForNode()方法根据节点改变按钮。我调用在树的选择监听器的valueChanged()方法中调用showButtonForNode()方法。

  这样使用装饰器模式正确吗?

  很多设计模式可以达到功能扩展的目的;比如在Java设计模式中,你已经知道如何使用代理模式,装饰器模式和策略模式来扩展功能。由于他们都可以实现相同的目标(功能扩展),在具体情况下使用哪个模式就很难判断。

  装饰器模式的主要解决问题的点在于:在运行时结合多种行为;比如理解代理设计模式一文的“上一次得作业”部分,我展示了Swing表格排序和过滤相结合的方法。

  1.   TableSortDecorator sortDecorator = new TableBubbleSortDecorator(table.getModel());

  2.   TableFilterDecorator filterDecorator = new TableHighPriceFilter(sortDecorator);

  3.   table.setModel(filterDecorator);
复制代码


  前面的代码中,过滤装饰器装饰了排序装饰器,排序装饰器装饰了表格模型;结果表格模型可以排序和过滤数据。

  对于邮件中的问题,使用工具栏按钮与其他行为组合不太合适,所以装饰器模式可能不合适。这种情况代理模式看来更好,在编译阶段而不是运行时就可以获取代理和真实对象的关系,从而扩展功能。

原文作者:佚名  来源:开发者头条

相关帖子

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关于我们
联系我们
  • 电话:010-86393388
  • 邮件:udn@yonyou.com
  • 地址:北京市海淀区北清路68号
移动客户端下载
关注我们
  • 微信公众号:yonyouudn
  • 扫描右侧二维码关注我们
  • 专注企业互联网的技术社区
版权所有:用友网络科技股份有限公司82041 京ICP备05007539号-11 京公网网备安1101080209224 Powered by Discuz!
快速回复 返回列表 返回顶部