软件构造:适配器模式

本文是软件构造系列的第三篇文章。其余文章在:
https://ruanx.net/tag/software-construction/

  适配器(Adapter)模式用于解决这样的问题:有一个已经封装好的类,实现了各种功能;现在客户端想要调用这个类,但接口不匹配。

  为了尽量少改动客户端和被调用者的代码,遵循开闭原则,我们倾向于用一个新的类,专门适配两种接口,使得:

  • 无需改动被调用者的代码。
  • 对于调用者而言,代码只有很微小的改动。

  适配器模式还有另一个优点:如果后端(被调用者)改动,也无须改动前端。只需要把适配器与后端通讯的部分改掉即可,从而适配器模式有高度的可扩展性。

  适配器模式分为类适配器、对象适配器,分别对应了继承、委托两种方式。前者需要了解被调用者的代码细节,所以使用较少;后者只需要委托被调用者类,应用广泛。

类适配器

  我们接下来考虑一个任务。手上已经有了绘图器 Drawer 的实现:指定 G、R、B,画出对应颜色的图。但客户端的颜色保存方式是一个 int 型数组,顺序为 R、G、B,不能直接调用 Drawer 的接口。

// client
public class Main {
    public static void main(String[] args) {
        int[] c = new int[]{0x12, 0x34, 0x56};
        // TODO: c 是 RGB 格式,想画出对应颜色
    }
}


// Drawer
class Drawer {
    int G, R, B;
    public Drawer(int G, int R, int B) {
        this.G = G;
        this.R = R;
        this.B = B;
    }
    public void drawGRB() {
        System.out.printf("Draw GRB #%x%x%x\n", G, R, B);
    }
}

  于是添加一个适配器类。它继承了 Drawer ,但可以通过 RGB 格式数组来指定颜色,可以正确调用父对象的绘图方法。最终代码如下:

package adapter;

public class Main {
    public static void main(String[] args) {
        int[] c = new int[]{0x12, 0x34, 0x56};

        new DrawerAdapter(c).draw();
        // 输出:Draw GRB #341256 (正确)
    }
}

class DrawerAdapter extends Drawer {
    public DrawerAdapter(int[] Colors) {
        super(Colors[1], Colors[0], Colors[2]);
    }
    public void draw() {
        super.drawGRB();
    }
}

class Drawer {
    int G, R, B;
    public Drawer(int G, int R, int B) {
        this.G = G;
        this.R = R;
        this.B = B;
    }
    public void drawGRB() {
        System.out.printf("Draw GRB #%x%x%x\n", G, R, B);
    }
}

对象适配器

  对象适配器不采取继承,而采取委托。此时无需知道 Drawer 的代码细节,只需要知道其接口。

package adapter;

public class Main {
    public static void main(String[] args) {
        int[] c = new int[]{0x12, 0x34, 0x56};

        new DrawerAdapter().draw(c);
        // 输出:Draw GRB #341256 (正确)
    }
}

class DrawerAdapter {
    public void draw(int[] Colors) {
        new Drawer(Colors[1], Colors[0], Colors[2]).drawGRB();
    }
}

class Drawer {
    int G, R, B;
    public Drawer(int G, int R, int B) {
        this.G = G;
        this.R = R;
        this.B = B;
    }
    public void drawGRB() {
        System.out.printf("Draw GRB #%x%x%x\n", G, R, B);
    }
}

  对象适配器相比于类适配器,还有另外一层好处:避免了不必要的接口暴露。在类适配器中,因为 DrawerAdapterDrawer 的子类,故 Drawer 的其他方法也可以调用,这可能是有害的。而对象适配器采取了委托的方式,封闭了 Drawer ,只调用其 drawGRB 一个接口。

结论

  Adapter 模式可以尽可能地遵循开闭原则(对扩展开放、对修改封闭),无需更改被调用者的代码,就能换一种接口来调用。Adapter 模式分为类适配器和对象适配器,建议采用对象适配器。