1. Adapter模式的宗旨就是:保留现在类所提供的服务,向客户提供接口,以满足客户的期望。
2. 当客户在接口中定义了他期望的行为时,我们就可以应用适配器模式,提供一个实现该接口的类,并且扩展已有的类。已有的类中有客户需要的信息。这时我们使用的是“类适配器”,即通过创建现有能提供客户需求的类的子类,并且该子类实现客户期望的接口的方式来实现适配。
例如:客户需要一个接口RocketSim,其代码如下:
package com.oozinoz.simulation;
public interface RocketSim {
abstract double getMass();
public double getThrust();
void setSimTime(double t);
}
现在有一个现有类PhysicalRocket拥有上面接口所需的信息,其代码如下:
package com.oozinoz.firework;
public class PhysicalRocket {
private double burnArea;
private double burnRate;
private double initialFuelMass;
private double totalMass;
private double totalBurnTime;
private static double SPECIFIC_IMPULSE = 620; // Newtons/Kg
private static double FUEL_DENSITY = 1800; // Kg/M**3
public PhysicalRocket(double burnArea, double burnRate, double fuelMass, double totalMass) {
this.burnArea = burnArea;
this.burnRate = burnRate;
this.initialFuelMass = fuelMass;
this.totalMass = totalMass;
double initialFuelVolume = fuelMass / FUEL_DENSITY;
this.totalBurnTime = initialFuelVolume / (burnRate * burnArea);
}
public double getMass(double t) {
if (t > totalBurnTime) return totalMass - initialFuelMass;
double burntFuelVolume = burnRate * burnArea * t;
return totalMass - burntFuelVolume * FUEL_DENSITY;
}
public double getThrust(double time) {
if (time > totalBurnTime) return 0;
return FUEL_DENSITY * SPECIFIC_IMPULSE * burnRate * burnArea;
}
public double getBurnTime() {
return totalBurnTime;
}
}
这时可以写一个如下的类来进行适配,可以将新类的实现接口的方法的调用请求委托给现有类的相关方法处理,如下:
package com.oozinoz.firework;
import com.oozinoz.simulation.*;
public class OozinozRocket extends PhysicalRocket implements RocketSim {
private double time;
public OozinozRocket(
double burnArea,
double burnRate,
double fuelMass,
double totalMass) {
super(burnArea, burnRate, fuelMass, totalMass);
}
public double getMass() {
return getMass(time);
}
public double getThrust() {
return getThrust(time);
}
public void setSimTime(double time) {
this.time = time;
}
}
3. 即使没有定义客户期望的接口,我们也可以应用Adapter模式,这种情况下,我们必须使用“对象适配器”。该适配器使用委托方式,而不是创建子类方式。通过创建所需类(此类能满足客户的需求)的子类,依靠现有类的对象实现需要的方法,就可以创建一个对象适配器。
例如,现在客户类需要一个SkyRocket类的功能,此类如下:
package com.oozinoz.simulation;
public class Skyrocket {
private double mass;
private double thrust;
private double burnTime;
protected double simTime;
public Skyrocket(double mass, double thrust, double burnTime) {
this.mass = mass;
this.thrust = thrust;
this.burnTime = burnTime;
}
public double getMass() {
if (simTime > burnTime) return 0;
return mass * (1 - (simTime / burnTime));
}
public double getThrust() {
if (simTime > burnTime) return 0;
return thrust;
}
public void setSimTime(double t) {
simTime = t;
}
}
现有的能提供所需Skyrocket类功能的PhysicalRockt类的代码与上面类似。这时我们可以如下建立一个对象适配器,此适配器不再是PhysicalRocket类的子类,而是Skyrocket类的子类,并且此类中用到了PhysicalRocket对象。如下:
package com.oozinoz.firework;
import com.oozinoz.simulation.*;
public class OozinozSkyrocket extends Skyrocket {
private PhysicalRocket rocket;
public OozinozSkyrocket(PhysicalRocket r) {
super(r.getMass(0), r.getThrust(0), r.getBurnTime());
rocket = r;
}
public double getMass() {
return rocket.getMass(simTime);
}
public double getThrust() {
return rocket.getThrust(simTime);
}
}
OozinozSkyrocket 类允许我们在任何需要Skyrocket对象的地方提供一个OozinozSkyrocket 对象。通常情况下,使用对象适配器部分解决了一个对象到一个没有明确定义的接口的问题。
4. OozinozSkyrocket类使用的对象适配器设计也许比类适配器方法更加脆弱,原因如下:
(1). 不存在OozinozSkyrocket类所提供接口的规范。因此,Skyrocket的变化方式也许与在运行时创建问题有关,而与编译时未被发现的方式无关。
(2). OozinozSkyrocket对象期望能够访问其父类的SimTime变量,尽管不能保证这个变量始终被声明为protected,也不能保证这个变量在Skyrocket类中的含义(我们不希望提供者超越常规来修改我们依赖的Skyrocket对象,但在另一方面,我们对他们没有约束力).
5. 为JTable适配数据:
提到要在表格中显示数据,就要提到一个的对象适配器的例子。Swing提供JTable小工具来显示表格。但这个小工具的设计者并不知道我们需要显示什么数据。他们没有把一些复杂的数据结构硬编码到小工具中,而是提供了一个接口,即TableModel,这个方法更好。借助于这个接口,JTable就可以实现所需功能,而且更加灵活。接下来就可以提供一个适配器,使数据和TableModel的数据格式保持一致。
javax.swing.table的AbstractTableModel抽象类为TableModel接口中的大部分方法提供了默认的实现。
如果要使用swing用户接口在一个表格中显示几种火箭,我们可以创建一个RocketTableModel类,把火箭数组适配到TableModel期望的接口。
RocketTableModel类必须扩展AbstractTableModel,因为AbstractTableModel是类,而不是接口。无论何时,只要我们所适配的接口可以得到抽象类的支持,就必须使用对象适配器模式。在这个例子中,还有另外一个原因导致我们无法使用类适配器:RocketTableModel不是由Rocket派生的。简言之,如果适配器类需要从多个对象中提取信息,那么就应该使用对象适配器。
请注意:类适配器扩展一个现有的类,并实现一个目标接口;而对象适配器扩展一个目标类,并把它委托给一个现有的类。
创建RocketTableModel类之后,我们就可以很容易地在Swing JTable对象中显示各种火箭的信息。RocketTableModel代码如下:
package app.adapter;
import javax.swing.table.*;
import com.oozinoz.firework.Rocket;
public class RocketTableModel extends AbstractTableModel {
protected Rocket[] rockets;
protected String[] columnNames = new String[] { "Name", "Price", "Apogee" };
public RocketTableModel(Rocket[] rockets) {
this.rockets = rockets;
}
public int getColumnCount() {
return columnNames.length;
}
public String getColumnName(int i) {
return columnNames[i];
}
public int getRowCount() {
return rockets.length;
}
public Object getValueAt(int row, int col) {
switch (col) {
case 0:
return rockets[row].getName();
case 1:
return rockets[row].getPrice();
case 2:
return new Double(rockets[row].getApogee());
default:
return null;
}
}
}
若要继续完成这个应用程序,从而显示所需表格,我们还要创建一组Rocket对象,然后将它们填入数组中,再用该数组初始化RocketTableModel实例,最后用Swing类将图表显示出来。下面了ShowRocketTable类的代码作为示范:
package app.adapter;
import java.awt.Component;
import java.awt.Font;
import javax.swing.*;
import com.oozinoz.firework.Rocket;
import com.oozinoz.utility.Dollars;
public class ShowRocketTable {
public static void main(String[] args) {
setFonts();
JTable table = new JTable(getRocketTable());
table.setRowHeight(36);
JScrollPane pane = new JScrollPane(table);
pane.setPreferredSize(new java.awt.Dimension(300, 100));
display(pane, " Rockets");
}
public static void display(Component c, String title) {
JFrame frame = new JFrame(title);
frame.getContentPane().add(c);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
private static RocketTableModel getRocketTable() {
Rocket r1 = new Rocket("Shooter", 1.0, new Dollars(3.95), 50.0, 4.5);
Rocket r2 = new Rocket("Orbit", 2.0, new Dollars(29.03), 5000, 3.2);
return new RocketTableModel(new Rocket[] { r1, r2 });
}
private static void setFonts() {
Font font = new Font("Dialog", Font.PLAIN, 18);
UIManager.put("Table.font", font);
UIManager.put("TableHeader.font", font);
}
}
其中所引用到的com.oozinoz.utility.Dollars类的代码如下:
package com.oozinoz.utility;
public class Dollars {
public static final Dollars cent = new Dollars(0.01);
static final int CENTS_PER_DOLLAR = 100;
long cents;
public Dollars(double value) {
this.cents = Math.round(value * CENTS_PER_DOLLAR);
}
public boolean isZero() {
return cents == 0;
}
public Dollars plus(Dollars that) {
return new Dollars(1.0 * (this.cents + that.cents) / CENTS_PER_DOLLAR);
}
public Dollars times(int multiplier) {
return new Dollars((this.cents * multiplier) / CENTS_PER_DOLLAR);
}
public Dollars dividedBy(int divisor) {
double newCents = (1.0 * cents / divisor) / CENTS_PER_DOLLAR;
return new Dollars(newCents);
}
public double dividedBy(Dollars that) {
return (1.0 * this.cents) / that.cents;
}
public boolean isLessThan(Dollars that) {
return this.cents < that.cents;
}
public String toString() {
StringBuffer result = new StringBuffer("$");
long dollars = cents / CENTS_PER_DOLLAR;
result.append(dollars);
result.append('.');
long pennies = cents % CENTS_PER_DOLLAR;
if (pennies < 10) result.append('0');
result.append(pennies);
return result.toString();
}
public boolean equals(Object obj) {
if (!obj.getClass().equals(this.getClass()))
return false;
Dollars that = (Dollars) obj;
return this.cents == that.cents;
}
public int hashCode() {
return (int) cents;
}
public long asCents() {
return cents;
}
}
ShowRocketTable类本身的代码不到20条语句。要在GUI上显示出表格,更多地是依赖Swing表格组件包中成百上千的语句相互协作。JTable类几乎可处理显示表格所需的各项工作,但是无法提前得知用户将要提供的数据。JTable类要求用户采用Adapter模式来提供自己需要的表格数据。因而,如果要用JTable,我们必须实现JTable期望的TableModel接口,并创建一个类来提供要在表格中显示的数据。
6. 请判断我们在使用MouseAdapter类的时候,是否在应用Adapter模式?如果不是,请给出理由。
答:从赞成的角度来看,当图形用户单击鼠标时,我们需要将相关的Swing调用转化为适当的行为。换言之,当我们需要根据应用程序的接口对GUI事件进行适配的时候,我们可以使用Swing适配类。我们将一个接口转化为另一个接口,这种思想符合设计模式的意图。
从反对的角度来讲,Swing中的"适配类"属于存根类,它们并不能转换或适配什么。我们对这些存根类进行扩展,重写其中我们需要的方法。我们扩展的类和具体实现的方法才是适配器模式的实例。
7. 小结:
Adapter模式允许我们使用现有类来满足客户类的需求。当客户指定接口中的需求时,我们通常可以创建一个新类来实现接口和扩展现有类。这种方法会创建一个类适配器,它将把客户的调用转变为调用现有类的方法。
当客户没有指定所需要的接口时,我们仍然可以应用Adapter模式,并使用现有类的实例来创建一个新的客户子类。这种方法会创建一个对象适配器,将客户调用转发给现有类的实例。但是这种方法也危险,尤其是当我们没有(或者不可以)重写客户可能调用的所有方法时。
Swing中的JTable组件是应用Adapter模式的一个很好的例子。JTable组件通过调用TableModel接口中定义的方法获取表格的相关信息,这样就使我们可以很容易地写出一个适配器类,从域对象(如多个Rocket类实例对象)中获取用于填充表格的数据。
若要使用JTable,我们通常写一个对象适配器类,由它将JTable对象上的操作委托给某个现有类的多个实例。JTable类有两方面原因导致我们不能使用类适配器。第一,表适配器类必须扩展AbstractTableModel类,因此它就不能再扩展现有的类;第二,JTable类需要收集多个对象的数据,而只有对象适配器类才可以对多个对象进行适配。