读书频道 > 网站 > 网页设计 > Java设计模式(第2版)
重构到外观模式
12-08-27    叶孤城
收藏    我要投稿   

本文所属图书 > Java设计模式(第2版)

本书通过一个完整的Java项目对经典著作Design Patterns一书介绍的23种设计模式进行了深入分析与讲解,实践性强,却又不失对模式本质的探讨。本书创造性地将这些模式分为5大类别,以充分展现各个模式的重要特征,...立即去当当网订购
外观模式常见于一些常规的应用程序开发。如果根据关注点将代码分解为不同的类,就可以提取一个类,它的主要职责是为子系统提供简便的访问方式,从而完成对系统的重构。考虑Oozinoz公司早期的一个例子,当时并没有GUI开发的规范。假设你读到程序员写的一段程序,用于展示一颗哑弹的飞行路径,如图4.2所示。

设计的焰火弹会在高空爆炸,发出绚烂的光芒。但是偶尔,某个焰火弹却不会爆炸(即哑弹),而是缓慢坠落到地面。与火箭不同,焰火弹是没有动力的。倘若忽略风和空气的阻力,一颗哑弹的飞行路径就是一条简单

的抛物线。图4.3展示了执行ShowFlight.main()后的一个窗口截图。




图4.2 ShowFlight类显示了哑弹的飞行路径




图4.3  ShowFlight应用展示了一颗哑弹将在何处落地
ShowFlight类存在一个问题:它混杂了三个功能。它的首要功能是为飞行路径提供一个面板,第二个功能是作为一个完整的应用程序,需要将飞行路径的面板显示在一个具有标题的边框内,最后一个功能是计算飞行路径的抛物线。ShowFlight类定义了paintComponent()方法执行计算,代码如下。
protected void paintComponent(Graphics g) {
   super.paintComponent(g); // 绘制背景
   int nPoint = 101;
   double w = getWidth() - 1;
   double h = getHeight() - 1;
   int[] x = new int[nPoint];
   int[] y = new int[nPoint];
   for (int i = 0; i < nPoint; i++) {
       // t从0到1
       double t = ((double) i) / (nPoint - 1);
       // x从0到w
       x[i] = (int) (t * w);
       // 当t=0和1时,y为h;当t=0.5时,y为0
       y[i] = (int) (4 * h * (t - .5) * (t - .5));
   }
   g.drawPolyline(x, y, nPoint);
}
如需了解代码中如何建立哑弹轨迹坐标的xy值,请参考下页关于“参数方程”的介绍。
这里无须类的构造函数。它使用静态工具方法去包装面板的标题,并定义了标准字体。
public static TitledBorder createTitledBorder(String title){
   TitledBorder tb = BorderFactory.createTitledBorder(
       BorderFactory.createBevelBorder(BevelBorder.RAISED),
       title,
       TitledBorder.LEFT,
       TitledBorder.TOP);
   tb.setTitleColor(Color.black);
   tb.setTitleFont(getStandardFont());
   return tb;
}
public static JPanel createTitledPanel(
   String title, JPanel in) {
   JPanel out = new JPanel();
   out.add(in);
   out.setBorder(createTitledBorder(title));
   return out;
}
 
public static Font getStandardFont() {
   return new Font("Dialog", Font.PLAIN, 18);
}
注意,createTitledPanel()方法把提供的控件放进一个斜边框里,以提供一个小的间隙(padding),保证飞行曲线不碰到容器的边缘。Main()方法也为包含了应用程序控件的表单对象增加了间隙。
public static void main(String[] args) {
   ShowFlight flight = new ShowFlight();
   flight.setPreferredSize(new Dimension(300, 200));
   JPanel panel = createTitledPanel("Flight Path", flight);
 
   JFrame frame = new JFrame("Flight Path for Shell Duds");
   frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
   frame.getContentPane().add(panel);
 
   frame.pack();
   frame.setVisible(true);
}
图4.3展示了该程序的执行过程。


参数方程
当你需要画一条曲线时,很难把y值表示为x值的函数。参数方程可以让你把x和y定义成其他参数的函数。特别的,你可以将曲线的绘制时间表示为0~1之间的参数t,并且可以将x和y定义成参数t的函数。
例如,你想要哑弹的飞行抛物线穿过Graphics对象,假设该对象的宽度是w,那么关于x的参数方程可以简单表示为:
x= w * t
注意,t的变化范围是0~1,x的变化范围是0~w。
抛物线的y值随着t的平方值变化而变化,方向越往下,y值就会随之而递增。对于抛物线,当t=0.5时,y应该为0。
因此我们能够得到如下方程:
y= k * (t - .5) * (t - .5)
这里,k代表一个指定的常量。该方程规定了当t=0.5时,y=0,并且当t=0或t=1时,y=h,也就是显示区域的高度。经过一些代数运算,可以推导出如下完整的关于y的方程:
y= 4 * h * (t - .5) * (t - .5)
图4.3显示了方程式绘出的抛物线。
参数方程的另一个优点是,我们可以用它绘制出一个给定x值多个y值的曲线。比如绘制一个圆。半径为1的圆的方程为:
x2+ y2 = r2
或者
y= +- sqrt (r2- x2)
如果每个x值对应两个y值,情况会更加复杂。要正确地调整Graphics对象的高和宽,并进行绘制,则较为困难。因此,可以利用极坐标来简化绘制圆的功能。
x= r * cos(theta)
y= r * sin(theta)
这两个参数方程将x和y表示成新参数theta的函数。Theta表示绘制圆的过程中扫过的弧度,范围是0~2*pi。可以设置一个圆的半径,使它能塞在高度为h,宽度为w的图形对象中。下面是一些在Graphics对象中绘制圆的参数方程:
theta= 2 * pi * t
r= min(w, h)/2
x= w/2 + r * cos(theta)
y= h/2 - r * sin(theta)
将这些方程转换为代码后,就可以产生如图4.4所示的圆(可以在oozinoz.com获得生成这一显示效果的ShowCircle应用程序代码)。
 
 
 
 
图4.4  当一个x值对应多个y值时,参数方程可以简化对曲线的建模
 
下面绘制圆的代码是对数学公式的直接翻译。由于像素在水平方向和垂直方向的范围分别是0到width-1和0到height-1,因此我们在代码中相应减小了Graphics对象的高和宽。
package app.facade;
 
import javax.swing.*;
import java.awt.*;
 
import com.oozinoz.ui.SwingFacade;
 
public class ShowCircle extends JPanel {
     public static void main(String[] args) {
         ShowCircle sc = new ShowCircle();
         sc.setPreferredSize(new Dimension(300, 300));
         SwingFacade.launch(sc, "Circle");
     }
protected void paintComponent(Graphics g) {
         super.paintComponent(g);
         int nPoint = 101;
         double w = getWidth() - 1;
         double h = getHeight() - 1;
         double r = Math.min(w, h) / 2.0;
         int[] x = new int[nPoint];
         int[] y = new int[nPoint];
         for (int i = 0; i < nPoint; i++) {
              double t = ((double) i) / (nPoint - 1);
              double theta = Math.PI * 2.0 * t;
              x[i] = (int) (w / 2 + r * Math.cos(theta));
              y[i] = (int) (h / 2 - r * Math.sin(theta));
         }
         g.drawPolyline(x, y, nPoint);
     }
}
根据t去定义x和y函数,可以分别计算x和y的值。这比把y定义成x的函数要简单,也便于将x和y映射到Graphics对象坐标中。另外,当y是x的非单值函数时,参数方程也可以简化曲线的绘制。
 


ShowFlight类可以工作,然而根据分离关注点的原则,我们应将其重构为多个单独的类,以提高可维护性以及可重用性。假设你正在进行设计评审,并且决定做出如下改变:

—   引入一个Function类,它定义了f()方法,接收一个double值(时间值),返回一个double值(函数的值)。
—   将ShowFlight类移入PlotPanel类,改为使用Function对象来获取xy的值。定义PlotPanel构造函数接收两个Function实例和绘图所需的点数。

—   将createTitledPanel()方法移到已存在的UI工具类中,来实现一个像当前ShowFlight类那样带有标题的面板。
挑战4.4

完成图4.5所示的类图,用以展示将ShowFlight重构为三个类的代码:Function类、实现两个参数功能的PlotPanel类和一个UI外观类。在你的重新设计中,让类ShowFlight2为获取y值创建一个Function,并让main()方法启动程序。
答案参见第303页








图4.5  被重构为多个类的飞行轨迹应用程序,每个类都有各自的职责
重构之后,Function类定义了参数方程。倘若要创建一个包含Function类与其他类型的com.oozinoz.function包,则Function.java的核心代码可能是:
public abstract double f(double t);
经过重构,PlotPanel类只拥有了一个职责:显示一对参数方程,代码如下:
package com.oozinoz.ui;
 
import java.awt.Color;
import java.awt.Graphics;
 
import javax.swing.JPanel;
import com.oozinoz.function.Function;
 
public class PlotPanel extends JPanel {
   private int points;
   private int[] xPoints;
   private int[] yPoints;
 
   private Function xFunction;
   private Function yFunction;
 
   public PlotPanel(
           int nPoint, Function xFunc, Function yFunc) {
       points = nPoint;
       xPoints = new int[points];
       yPoints = new int[points];
       xFunction = xFunc;
       yFunction = yFunc;
       setBackground(Color.WHITE);
   }
   
   protected void paintComponent(Graphics graphics) {
       double w = getWidth() - 1;
       double h = getHeight() - 1;
 
       for (int i = 0; i < points; i++) {
           double t = ((double) i) / (points - 1);
           xPoints[i] = (int) (xFunction.f(t) * w);
           yPoints[i] = (int) (h * (1 - yFunction.f(t)));
       }
 
       graphics.drawPolyline(xPoints, yPoints, points);
   }
}
注意,PlotPanel类目前是com.oozinoz.ui包的一部分,和UI类处于同一位置。对ShowFlight类进行重构后,UI类也拥有了createTitledPanel()和createTitledBorder()方法。UI类逐步演化为外观类,使之更容易使用Java控件。

使用这些控件的应用程序可能是一个很小的类,该类唯一的职责就是对这些控件进行布局并显示它们。例如,类ShowFlight2的代码如下:
package app.facade;
 
import java.awt.Dimension;
import javax.swing.JFrame;
import com.oozinoz.function.Function;
import com.oozinoz.function.T;
import com.oozinoz.ui.PlotPanel;
import com.oozinoz.ui.UI;
 
public class ShowFlight2 {
   public static void main(String[] args) {
       PlotPanel p = new PlotPanel(
           101,
           new T(),
           new ShowFlight2().new YFunction());
       p.setPreferredSize(new Dimension(300, 200));
 
       JFrame frame = new JFrame(
           "Flight Path for Shell Duds");
       frame.setDefaultCloseOperation(
           JFrame.EXIT_ON_CLOSE);
       frame.getContentPane().add(
           UI.NORMAL.createTitledPanel("Flight Path", p));
 
       frame.pack();
       frame.setVisible(true);
   }
 
 
   private class YFunction extends Function {
       public YFunction() {
           super(new Function[] {});
       }
 
       public double f(double t) {
           // 当t等于0或1时,y为0;当t等于0.5时,y为1
           return 4 * t * (1 - t);
       }
   }
}

ShowFlight2类为哑弹的飞行路径提供了YFunction类。main()方法用来布局和显示用户界面。这个类的运行结果和最初的ShowFlight类相同。但是,现在你拥有了一个可重用的外观类,它可以简化Java应用程序中图形用户界面的创建
点击复制链接 与好友分享!回本站首页
分享到: 更多
您对本文章有什么意见或着疑问吗?请到论坛讨论您的关注和建议是我们前行的参考和动力  
上一篇:1.3 功能
下一篇:1.5 小结
相关文章
图文推荐
JavaScript网页动画设
1.9 响应式
1.8 登陆页式
1.7 主题式
排行
热门
文章
下载
读书

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训
版权所有: 红黑联盟--致力于做最好的IT技术学习网站