适配器模式
这里我为您介绍了另一种有用的设计模式——适配器设计模式。我还将强调装饰器设计模式(请参阅我的上一篇文章,Java中的装饰器设计样式)和适配器设计模式之间的差异。
介绍
-
适配器设计模式是一种结构设计模式,允许两个不相关/不常见的接口一起工作。换句话说,适配器模式使两个不兼容的接口兼容,而不更改其现有代码。
-
接口可能不兼容,但内部功能应符合要求。
-
适配器模式通常用于在不修改源代码的情况下使现有类与其他类协同工作。
-
适配器模式使用单个类(适配器类)连接独立或不兼容接口/类的功能。
-
适配器模式也称为包装器,它是与装饰器设计模式共享的另一种命名方式。
-
此模式将类(适配器)的(不兼容)接口转换为客户端需要的另一个接口(目标)。
-
适配器模式还允许类一起工作,否则,由于接口不兼容,无法工作。
-
例如,让我们看看一个人带着笔记本电脑和移动设备在不同国家旅行。我们在不同的国家测量了不同的电源插座、电压和频率,这使得一个国家的任何设备都可以在另一个国家自由使用。在英国,我们使用230伏50赫兹的G型插座。在美国,我们使用120伏和60赫兹频率的A型和B型插座。在印度,我们使用230伏50赫兹的C型、D型和M型插座。最后,在日本,我们使用110伏和50赫兹频率的A型和B型插座。这使得我们携带的电器与我们在不同地方的电气规格不兼容。
-
这使得适配器工具至关重要,因为它可以将不兼容的代码转换为兼容的代码。请注意,我们在这里没有实现任何附加功能,只有兼容性。
实现
为了更好地理解这一点,让我们看一个几何形状的例子。我将示例保持相对简单,以保持对模式的关注。假设我们有一个绘图项目,在这个项目中,我们需要通过一个名为Shape
的通用接口开发不同类型的几何形状,这些形状将在绘图的时候使用。
下面是Shape
接口的例子:
public interface Shape {
void draw();
void resize();
String description();
boolean isHide();
}
下面是具体实现类的代码,Rectangle
:
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("画个长方形");
}
@Override
public void resize() {
System.out.println("调整长方形大小");
}
@Override
public String description() {
return "长方形";
}
@Override
public boolean isHide() {
return false;
}
}
下面是具体实现类的代码,Circle
:
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("画个圈圈");
}
@Override
public void resize() {
System.out.println("调整圆圈的大小");
}
@Override
public String description() {
return "圆形";
}
@Override
public boolean isHide() {
return false;
}
}
下面是Drawing
类的例子:
public class Drawing {
List<Shape> shapes = new ArrayList<>();
public Drawing() {
super();
}
public void addShape(Shape shape) {
shapes.add(shape);
}
public List<Shape> getShapes() {
return new ArrayList<>(shapes);
}
public void draw() {
if (shapes.isEmpty()) {
System.out.println("没什么可画的!");
} else {
shapes.forEach(Shape::draw);
}
}
public void resize() {
if (shapes.isEmpty()) {
System.out.println("没有可调整的!");
} else {
shapes.forEach(Shape::resize);
}
}
}
下面是Main
类的代码,来执行和测试Drawing
类:
public class Main {
public static void main(String[] args) {
System.out.println("开始绘制形状...");
Drawing drawing = new Drawing();
drawing.addShape(new Rectangle());
drawing.addShape(new Circle());
drawing.draw();
drawing.resize();
}
}
到目前为止,一切都很好。随着我们的进步,我们逐渐了解到有一些额外的几何形状已经由我们组织内的其他团队开发出来。或者,我们有一个可用的第三方API。下面就是可以使用的类。
下面是GeometricShape
接口的代码示例:
public interface GeometricShape {
double area();
double perimeter();
void drawShape();
}
下面是具体的实现类Triangle
:
public class Triangle implements GeometricShape {
/**
* 边长
*/
private final double a;
private final double b;
private final double c;
public Triangle() {
this(1.0d, 1.0d, 1.0d);
}
public Triangle(double a, double b, double c) {
this.a = a;
this.b = b;
this.c = c;
}
@Override
public double area() {
// Heron's formula:
// Area = SquareRoot(s * (s - a) * (s - b) * (s - c))
// where s = (a + b + c) / 2, or 1/2 of the perimeter of the triangle
double s = (a + b + c) / 2;
return Math.sqrt(s * (s - a) * (s - b) * (s - c));
}
@Override
public double perimeter() {
// P = a + b + c
return a + b + c;
}
@Override
public void drawShape() {
System.out.println("绘制三角形的面积: " + area() + " 和周长: " + perimeter());
}
}
下面是具体的实现类Rhombus
:
public class Rhombus implements GeometricShape {
/**
* 边
*/
private final double a;
private final double b;
public Rhombus() {
this(1.0d, 1.0d);
}
public Rhombus(double a, double b) {
this.a = a;
this.b = b;
}
@Override
public double area() {
return a * b;
}
@Override
public double perimeter() {
return 2 * (a + b);
}
@Override
public void drawShape() {
System.out.println("绘制菱形的面积: " + area() + " 和周长: " + perimeter());
}
}
由于这些工作是通过其他团队或组织完成的,因此他们很有可能使用自己的规范。所有这些现成的几何图形都没有实现我们的Shape
接口。显然,我们可以看到三角形和菱形实现的是GeometricShape
接口。而且,GeometricShape
接口与我们的Shape
接口不同(不兼容)。
我们的Drawing
客户端类只能使用Shape
而不能使用GeometricShape
。这使得GeometricShape
与我们的Drawing
类不兼容。
这意味着我们有一些现成的代码,这些代码与我们期望的非常相似,但并不符合我们的编码规范,就像不同国家的电气规范一样。
现在我们应该怎么做呢?
-
我们更改代码,更改/删除了
Shape
接口,并使用GeometricShape
接口。或者,我们可以将GeometricShape
接口转换为我们的Shape
接口,如果改动量较小的情况下。但是,由于其他功能和代码依赖性,这并不总是可能的。 -
我们是否应该不使用现成的代码或者第三方API?自己重新开发?
不需要。实际上,我们需要的只是一个适配器,它可以使这个现成的代码与我们的代码和本例中的Drawing
兼容。
现在,当我们弄清楚为什么需要适配器时,让我们仔细看看适配器的实际功能。在开始之前,下面是适配器模式中使用的类/对象列表:
Target :定义客户端使用的特定于域的接口。这是我们示例中的Shape
接口。
Adapter:将接口从适配器调整到目标接口。我将根据下面的不同方法指出适配器类。
Adaptee:定义了需要调整的现有接口。这是我们示例中的GeometricShape
界面。
Client:这与符合Target
接口的对象协作。Drawing
类是我们示例中的客户端。
我们有两种不同的方法来实现适配器模式。
⭐对象适配器模式
在这种方法中,我们将使用Java组合,并且我们的适配器包含源对象。组合被用作适配器内包装类的引用。在这种方法中,我们创建了一个实现目标(本例中为Shape
)的适配器类,并在本例中引用adaptee-GeometricShape
。我们实现了目标(Shape
)的所有必需方法,并进行了必要的转换以满足我们的要求。
下面是GeometricShapeObjectAdapter
的代码示例:
public class GeometricShapeObjectAdapter implements Shape {
private GeometricShape adaptee;
public GeometricShapeObjectAdapter(GeometricShape adaptee) {
super();
this.adaptee = adaptee;
}
@Override
public void draw() {
adaptee.drawShape();
}
@Override
public void resize() {
System.out.println(description() + " 不能被调整大小,请使用必须值创建新的");
}
@Override
public String description() {
if (adaptee instanceof Triangle) {
return "三角形";
} else if (adaptee instanceof Rhombus) {
return "菱形";
} else {
return "不知道的类型";
}
}
@Override
public boolean isHide() {
return false;
}
}
下面是ObjectAdapterMain
类,用来的执行和测试我们的适配器模式
public static void main(String[] args) {
Drawing drawing = new Drawing();
drawing.addShape(new Rectangle());
drawing.addShape(new Circle());
drawing.addShape(new GeometricShapeObjectAdapter(new Triangle()));
drawing.addShape(new GeometricShapeObjectAdapter(new Rhombus()));
System.out.println("开始绘制形状...");
drawing.draw();
System.out.println("开始调整大小...");
drawing.resize();
}
程序执行结果:
开始绘制形状...
画个长方形
画个圈圈
绘制三角形的面积: 0.4330127018922193 和周长: 3.0
绘制菱形的面积: 1.0 和周长: 4.0
开始调整大小...
调整长方形大小
调整圆圈的大小
三角形 不能被调整大小,请使用必须值创建新的
菱形 不能被调整大小,请使用必须值创建新的
⭐类适配器模式
在这种方法中,我们使用Java继承并扩展源类。因此,对于这种方法,我们必须为Triangle
和Rhombus
类创建单独的适配器,如下所示:
下面是TriangleAdapter
代码示例:
public class TriangleAdapter extends Triangle implements Shape {
public TriangleAdapter() {
super();
}
@Override
public void draw() {
this.drawShape();
}
@Override
public void resize() {
System.out.println("三角形不能被调整大小,请使用必须值创建新的");
}
@Override
public String description() {
return "三角形";
}
@Override
public boolean isHide() {
return false;
}
}
下面是RhombusAdapter
代码示例
public class RhombusAdapter extends Rhombus implements Shape {
public RhombusAdapter() {
super();
}
@Override
public void draw() {
this.drawShape();
}
@Override
public void resize() {
System.out.println("菱形不能被调整大小,请使用必须值创建新的");
}
@Override
public String description() {
return "菱形";
}
@Override
public boolean isHide() {
return false;
}
}
下面是ClassAdapterMain
类,用来的执行和测试我们的适配器模式
public class ClassAdapterMain {
public static void main(String[] args) {
Drawing drawing = new Drawing();
drawing.addShape(new Rectangle());
drawing.addShape(new Circle());
drawing.addShape(new TriangleAdapter());
drawing.addShape(new RhombusAdapter());
System.out.println("开始绘制形状...");
drawing.draw();
System.out.println("开始调整大小...");
drawing.resize();
}
}
程序输出结果:
开始绘制形状...
画个长方形
画个圈圈
绘制三角形的面积: 0.4330127018922193 和周长: 3.0
绘制菱形的面积: 1.0 和周长: 4.0
开始调整大小...
调整长方形大小
调整圆圈的大小
三角形不能被调整大小,请使用必须值创建新的
菱形不能被调整大小,请使用必须值创建新的
Process finished with exit code 0
📌这两种方法都有相同的输出。但是:
- 类适配器使用继承,并且只能包装类。我不能包装接口,因为根据定义,它必须从某个基类派生。
- 对象适配器使用组合,可以包装类和接口。它包含对类或接口对象实例的引用。对象适配器是比较简单的,可以应用于大多数场景。
我们还可以通过实现目标(Shape
)和被适配器(GeometricalShape
)来创建适配器。这种方法称为双向适配器。
⭐双向适配器
双向适配器是实现目标和适配器接口的适配器。适应对象可以在处理目标类的新系统中用作目标,也可以在处理适应对象类的其他系统中用作适应对象。双向适配器的使用很少,我从来没有机会在项目中编写这样的适配器。但是,下面提供的代码探索了双向适配器的可能实现。
以下是各种形状对象类型的ShapeType
枚举的代码:
public enum ShapeType {
/**
* 各种形状对象类型的枚举
*/
CIRCLE,
RECTANGLE,
TRIANGLE,
RHOMBUS
}
下面是TwoWaysAdapter
的代码,它可以用作三角形、菱形、圆形或矩形。
public class TwoWaysAdapter implements Shape, GeometricShape {
private ShapeType shapeType;
public TwoWaysAdapter() {
this(ShapeType.TRIANGLE);
}
public TwoWaysAdapter(ShapeType shapeType) {
super();
this.shapeType = shapeType;
}
@Override
public void draw() {
switch (shapeType) {
case CIRCLE:
new Circle().draw();
break;
case RECTANGLE:
new Rectangle().draw();
break;
case TRIANGLE:
new Triangle().drawShape();
break;
case RHOMBUS:
new Rhombus().drawShape();
break;
default:
break;
}
}
@Override
public void resize() {
switch (shapeType) {
case CIRCLE:
new Circle().resize();
break;
case RECTANGLE:
new Rectangle().resize();
break;
case TRIANGLE:
System.out.println("三角形 不能被调整大小,请使用必须值创建新的");
break;
case RHOMBUS:
System.out.println("菱形 不能被调整大小,请使用必须值创建新的");
break;
default:
break;
}
}
@Override
public String description() {
switch (shapeType) {
case CIRCLE:
return new Circle().description();
case RECTANGLE:
return new Rectangle().description();
case TRIANGLE:
return "三角形";
case RHOMBUS:
return "菱形";
default:
break;
}
return "Unknown object";
}
@Override
public boolean isHide() {
return false;
}
@Override
public double area() {
switch (shapeType) {
case CIRCLE:
case RECTANGLE:
return 0.0d;
case TRIANGLE:
return new Triangle().area();
case RHOMBUS:
return new Rhombus().area();
}
return 0.0d;
}
@Override
public double perimeter() {
switch (shapeType) {
case CIRCLE:
case RECTANGLE:
return 0.0d;
case TRIANGLE:
return new Triangle().perimeter();
case RHOMBUS:
return new Rhombus().perimeter();
}
return 0.0d;
}
@Override
public void drawShape() {
draw();
}
}
下面是TwoWaysAdapterMain
类,用来的执行和测试我们的适配器模式
public class TwoWaysAdapterMain {
public static void main(String[] args) {
Drawing drawing = new Drawing();
drawing.addShape(new TwoWaysAdapter(ShapeType.RECTANGLE));
drawing.addShape(new TwoWaysAdapter(ShapeType.CIRCLE));
drawing.addShape(new TwoWaysAdapter(ShapeType.TRIANGLE));
drawing.addShape(new TwoWaysAdapter(ShapeType.RHOMBUS));
System.out.println("开始绘制形状...");
drawing.draw();
System.out.println("开始调整大小...");
drawing.resize();
}
}
程序输出结果
开始绘制形状...
画个长方形
画个圈圈
绘制三角形的面积: 0.4330127018922193 和周长: 3.0
绘制菱形的面积: 1.0 和周长: 4.0
开始调整大小...
调整长方形大小
调整圆圈的大小
三角形 不能被调整大小,请使用必须值创建新的
菱形 不能被调整大小,请使用必须值创建新的
Process finished with exit code 0
👉这里,我们也得到了相同的输出,因为我们以相同的方式将TwoWaysAdapter
用于客户端的图形绘制。这里唯一的区别是,通过使用TwoWaysAdapter
,我们的Shape
接口还可以与额外的几何图形APIs客户端类一起使用。因此,Shape
和GeometricalShape
都可以互换使用。
适配器与装饰器设计模式:
这里是区分适配器和装饰器模式的一些关键点(请参阅我的文章《Java中的装饰器设计模式》中的更多内容)。
适配器模式:
-
制作包装器(适配器)以创建从一个接口到另一个不兼容接口的兼容性/转换。
-
包装器(适配器)适用于两个不兼容的接口/类。
-
编写包装器类的目的是解决差异并使接口兼容。
-
我们很少在包装器类中添加任何功能。
装饰器模式:
-
制作一个包装器(Decorator)来添加/修改接口/类中的功能,而不更改类的原始代码。通常,我们使用抽象包装器来实现此模式。
-
Wraper(Decorator)在单个接口/类上工作。
-
编写包装器类的目的是添加/修改接口/类的功能。
-
不存在不兼容问题,因为我们一次只处理一个接口/类。
总结
适配器模式允许将本来不兼容的对象包装在适配器中,以使其与另一个类兼容。
优点:
- 可以让任何两个没有关联的类一起运行。
- 提高了类的复用。
- 增加了类的透明度。
- 灵活性好。
缺点:
- 过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。
- 由于 JAVA 至多继承一个类,所以至多只能适配一个适配者类,而且目标类必须是抽象类。
使用场景:
-
希望使用现有类,但其接口与你需要的接口不匹配
-
希望创建一个可重用的类,该类与不相关或不可预见的类协作,即不一定具有兼容接口的类
-
需要使用几个现有的子类,但通过对每个子类进行子类化来调整它们的接口是不切实际的。对象适配器可以调整其父类的接口。
-
大多数使用第三方库的应用程序都使用适配器作为应用程序和第三方程序库之间的中间层,以将应用程序与库解耦。如果必须使用另一个库,则只需要新库的适配器,而无需更改应用程序代码。
注意事项:
适配器不是在详细设计时添加的,而是解决正在服役的项目的问题。
经典例子
评论区