外观模式常见于一些常规的应用程序开发。如果根据关注点将代码分解为不同的类,就可以提取一个类,它的主要职责是为子系统提供简便的访问方式,从而完成对系统的重构。考虑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);
}
如需了解代码中如何建立哑弹轨迹坐标的x和y值,请参考下页关于“参数方程”的介绍。
这里无须类的构造函数。它使用静态工具方法去包装面板的标题,并定义了标准字体。
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对象来获取x和y的值。定义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应用程序中图形用户界面的创建