学习设计模式不光要学习设计模式的思想,还要去深入理解,为什么要用这个设计模式。
如何深入理解?读优秀的框架代码,看别人代码,了解它们的使用场景。 - - - 博主老师(感谢他)
本文先介绍了适配器模式的概念及简单实现。介绍了适配器模式在netty中的使用,最后总结了一点点思考。
1、概念
定义:适配器模式把一个类的接口变换成客户端所期待的另一个接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。
假设我们公司有一个商城,商城里的商品都是我们预先定义好的一个类(classA)。现在突然和某公司谈了合作,要把他们的商品也放到我们商城卖,而他们的商品肯定和我们的商品不是一个类,他们的可能是classB。
我们现在要适配classB, 让它伪装成classA,提供和classA一样的功能
适配器做的主要工作就是为了让目标角色的API可以调用到源角色的API,适配器在中间做的是一个类似的中转作用,并且不影响源角色和目标角色原有的功能和逻辑。
2、实现
实现上面的例子:
首先定义我们的产品
public interface IProduct {
void getName();
void getPrice();
}
public class Product implements IProduct {
@Override
public void getName() {
System.out.println("产品名称");
}
@Override
public void getPrice() {
System.out.println("产品价格");
}
}
合作公司的产品
public interface IOther {
// 友方公司代码写得比较烂... 他们的产品信息都放在了map里面
Map<String, Object> getMsg();
}
public class Other implements IOther {
@Override
public Map<String, Object> getMsg() {
Map<String, Object> map = new HashMap<>();
map.put("name", "其它公司产品名称");
map.put("price", "其它公司产品价格");
return map;
}
}
2.1 类适配器模式
类适配器通过继承实现,由于Java不支持多继承,所以我们实现一个,继承一个。
public class ProductAdapter extends Other implements IProduct {
@Override
public void getName() {
Map<String, Object> map = getMsg();
System.out.println(map.get("name"));
}
@Override
public void getPrice() {
Map<String, Object> map = getMsg();
System.out.println(map.get("price"));
}
}
测试:
public static void main(String[] args) {
IProduct product = new ProductAdapter();
product.getName();
product.getPrice();
}
2.2 对象适配器模式
对象适配器模式通过聚合的方式实现
public class ProductAdapter2 implements IProduct {
private IOther iOther;
public ProductAdapter2(IOther iOther) {
this.iOther = iOther;
}
@Override
public void getName() {
System.out.println(iOther.getMsg().get("name"));
}
@Override
public void getPrice() {
System.out.println(iOther.getMsg().get("price"));
}
}
IProduct product2 = new ProductAdapter2(new Other());
product2.getName();
product2.getPrice();
其它公司产品名称
其它公司产品价格
2.3 接口适配器模式
在学习适配器模式的过程中,网上有些文章还写到了接口适配器模式(缺省适配模式)。博主它并不属于23种设计模式中适配器模式的范畴。(个人观点,仅供参考。如果有误,敬请指正!)
虽然博主认为不是,但不妨碍我们介绍它:
接口适配器模式的思想是为一个接口提供缺省实现,这样子类可以从这个缺省实现进行扩展,而不必从原有接口进行扩展。
在jdk源码中有个典型的例子
package java.awt.event;
import java.util.EventListener;
public interface KeyListener extends EventListener {
public void keyTyped(KeyEvent e);
public void keyPressed(KeyEvent e);
public void keyReleased(KeyEvent e);
}
package java.awt.event;
public abstract class KeyAdapter implements KeyListener {
/**
* Invoked when a key has been typed.
* This event occurs when a key press is followed by a key release.
*/
public void keyTyped(KeyEvent e) {}
/**
* Invoked when a key has been pressed.
*/
public void keyPressed(KeyEvent e) {}
/**
* Invoked when a key has been released.
*/
public void keyReleased(KeyEvent e) {}
}
KeyAdapter空实现了KeyListener接口。为了子类实现接口的时候,只需要重写自己想要的方法,而不需要把接口的所有方法都写一遍。例如,我们只想实现keyPressed方法:
frame.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
System.out.println("fxcku!");
}
});
在Java8之后,其实并不需要这么麻烦。我们可以用default关键字为接口写默认的实现,可以改造成:
package java.awt.event;
import java.util.EventListener;
public interface KeyListener extends EventListener {
public default void keyTyped(KeyEvent e) {};
public default void keyPressed(KeyEvent e) {};
public default void keyReleased(KeyEvent e) {};
}
使用
frame.addKeyListener(new KeyListener() {
@Override
public void keyPressed(KeyEvent e) {
System.out.println("fxcku!");
}
});
3、netty中的适配器模式
ScheduledFutureTask是Netty当中的异步定时任务。任务可以是Runnale, 也可以是带返回结果的Callable
而netty最终去执行任务的时候,其实调用的是Callable的call方法。如果用户传的是Runnale,netty使用了适配器模式让两个任务接口无缝衔接。
ScheduledFutureTask的两个构造方法,如果是Runnable,调用了父类的toCallable()
package io.netty.util.concurrent;
final class ScheduledFutureTask<V> extends PromiseTask<V> implements ScheduledFuture<V>, PriorityQueueNode {
ScheduledFutureTask(
AbstractScheduledEventExecutor executor,
Runnable runnable, V result, long nanoTime) {
this(executor, toCallable(runnable, result), nanoTime);
}
ScheduledFutureTask(
AbstractScheduledEventExecutor executor,
Callable<V> callable, long nanoTime, long period) {
super(executor, callable);
if (period == 0) {
throw new IllegalArgumentException("period: 0 (expected: != 0)");
}
deadlineNanos = nanoTime;
periodNanos = period;
}
}
父类的toCallable方法,返回了一个适配器,我们可以看到,它是一个对象适配器。
最终,在run方法里面,调用的是task.call()方法。
class PromiseTask<V> extends DefaultPromise<V> implements RunnableFuture<V> {
static <T> Callable<T> toCallable(Runnable runnable, T result) {
return new RunnableAdapter<T>(runnable, result);
}
private static final class RunnableAdapter<T> implements Callable<T> {
final Runnable task;
final T result;
RunnableAdapter(Runnable task, T result) {
this.task = task;
this.result = result;
}
@Override
public T call() {
task.run();
return result;
}
@Override
public String toString() {
return "Callable(task: " + task + ", result: " + result + ')';
}
}
protected final Callable<V> task;
@Override
public void run() {
try {
if (setUncancellableInternal()) {
V result = task.call();
setSuccessInternal(result);
}
} catch (Throwable e) {
setFailureInternal(e);
}
}
}
4、思考
个人认为适配器模式把握住一个核心:兼容两个不同的api
适配器模式在为不方便重构旧类(旧的类可能在jar包里)又需要做到兼容的情况提供了良好的解决方案。
但是正如很多书本、文章所说:过多的适配器会让系统造成凌乱(明明调用的是接口A, 被适配成了接口B)。还是灵活使用吧,一般是在稳定的系统基础上,去加的,而不是一开始这么设计。