1. 前情提要
在状态模式中,我们实现了让计算器类根据自身状态的变化,动态地调整价格。且客户端无需关注状态的变化,和对应的价格设置行为的变化,直接调用设置价格的接口即可。客户端代码为:
public static void setPriceByState(Calculator calculator) {
int basePrice = 10;
calculator.setPriceByState(basePrice);
}
计算器类的代码为:
public class Calculator implements Cloneable {
private Date productionDate;
private int price;
private State state;
public Calculator() {
state = new NewProductState();
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public void setPriceByState(int basePrice) {
state.setPrice(this, basePrice);
}
public Date getProductionDate() {
return productionDate;
}
public void setProductionDate(Date productionDate) {
this.productionDate = productionDate;
}
public State getState() {
return state;
}
public void setState(State state) {
this.state = state;
}
}
计算器的具体状态类都继承了抽象类State,所以在设置计算器类的state时可以直接使用setState()方法,传入继承了State类的具体状态类来设置计算器的状态。
如果有一个现成的具体状态类SlightlyOldState,它实现的功能与NewProductState、OldProductState、DeadProductState类似,只是未继承抽象类State,函数的方法名、参数等也与State中定义的抽象方法不同。
SlightlyOldState类代码:
public class SlightlyOldState {
public void changePrice(Calculator calculator, int basePrice) {
Date productionDate = calculator.getProductionDate();
if (isSlightlyOld(productionDate)) {
calculator.setPrice(basePrice/2);
} else {
changeState(calculator,basePrice);
}
}
private boolean isSlightlyOld(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.YEAR,2);
Date dateCopy = calendar.getTime();
return dateCopy.compareTo(new Date()) >= 0;
}
protected void changeState(Calculator calculator,int basePrice) {
calculator.setState(new OldProductState());
calculator.setPriceByState(basePrice);
}
}
但我们又不能更改这个类的代码,又想像使用NewProductState等具体类一样使用它,就可以使用适配器模式。
即当要使用两个或多个已经存在的功能类似的类,但它们的接口不统一,你想直接使用统一的接口调用它们时,就可以使用适配器模式。
2. 适配器模式
适配器模式(Adapter),将一个类的接口转换成客户希望的另外一个接口。使得原本由于接口不兼容而不能一起工作的类可以一起工作。
根据适配器类与适配者类的关系不同,适配器模式可分为对象适配器和类适配器两种,在对象适配器模式中,适配器与适配者之间是关联关系;在类适配器模式中,适配器与适配者之间是继承(或实现)关系。
适配器模式的角色
使用适配器模式实现根据状态改变计算器价格的类图:
Target(目标抽象类):目标抽象类定义客户所需接口,可以是一个抽象类或接口,也可以是具体类。例如State类。
Adapter(适配器类):适配器可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配,适配器类是适配器模式的核心,在对象适配器中,它通过继承Target并关联一个Adaptee对象使二者产生联系。例如SlightlyOldStateAdapter类。
Adaptee(适配者类):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码。例如SlightlyOldState类。
缺省适配器模式(Default Adapter Pattern):当不需要实现一个接口所提供的所有方法时,可先设计一个抽象类实现该接口,并为接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可以选择性地覆盖父类的某些方法来实现需求,它适用于不想使用一个接口中的所有方法的情况,又称为单接口适配器模式。缺省适配器模式是适配器模式的一种变体,其应用也较为广泛。在JDK类库的事件处理包java.awt.event中广泛使用了缺省适配器模式,如WindowAdapter、KeyAdapter、MouseAdapter等。
3. 适配器类
对象适配器的适配类实现了目标接口或继承目标类(State抽象类),里面封装了一个被适配类的对象(SlightlyOldState),然后将被适配类的方法(changePrice)都包装成目标接口的方法(setPrice)。
public class SlightlyOldStateAdapter extends State {
private SlightlyOldState slightlyOldState = new SlightlyOldState();
@Override
public void setPrice(Calculator calculator, int basePrice) {
slightlyOldState.changePrice(calculator,basePrice);
}
@Override
protected void changeState(Calculator calculator,int basePrice) {
slightlyOldState.changeState(calculator,basePrice);
}
}
这样就可以像使用NewProductState、OldProductState、DeadProductState类一样使用SlightlyOldStateAdapter类了。
4. 在NewProductState中更改下一状态
public class NewProductState extends State {
@Override
public void setPrice(Calculator calculator,int basePrice) {
Date productionDate = calculator.getProductionDate();
if (isNew(productionDate)) {
calculator.setPrice(basePrice);
} else {
changeState(calculator,basePrice);
}
}
private boolean isNew(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.YEAR,1);
Date dateCopy = calendar.getTime();
return dateCopy.compareTo(new Date()) >= 0;
}
@Override
protected void changeState(Calculator calculator,int basePrice) {
calculator.setState(new SlightlyOldStateAdapter());
calculator.setPriceByState(basePrice);
}
}
这里可以发现状态模式的代码还不够好,每个具体状态类之间有耦合。由于在具体状态类中引用了下一状态的对象,在状态的改变流程中有状态要增加或删除时,都会影响引用它的那个状态类。要想让各个状态类各司其职,可以将某个状态类的下一状态的设置交给客户端,让每个状态类无需知道其他状态类的存在,可使用职责链模式实现。
但状态的增加不影响客户端代码,客户端代码不变。
5. 适配器模式的适用场景
适配器模式应是能不用则不用。如果能事先预防接口不统一,就不会有不匹配问题发生。当有接口不匹配的小问题发生时,应及时重构。只有在碰到原代码无法修改时,才考虑使用适配器模式。
适配器模式的应用有:
- spring AOP中使用 Advice(通知) 来增强被代理类的功能。
Advice的类型有:MethodBeforeAdvice
、AfterReturningAdvice
、ThrowsAdvice
在每个类型 Advice 都有对应的拦截器:MethodBeforeAdviceInterceptor
、AfterReturningAdviceInterceptor
、ThrowsAdviceInterceptor
Spring需要将每个 Advice 都封装成对应的拦截器类型,返回给容器,所以需要使用适配器模式对 Advice 进行转换
- spring JPA(Java持久层接口)中的适配器模式
在Spring的ORM包中,对于JPA的支持也是采用了适配器模式,首先定义了一个接口的 JpaVendorAdapter,然后不同的持久层框架都实现此接口。 - spring MVC中的适配器模式
Spring MVC中的适配器模式主要用于执行目标 Controller 中的请求处理方法。
在Spring MVC中,DispatcherServlet 作为用户,HandlerAdapter 作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller 作为需要适配的类。
为什么要在 Spring MVC 中使用适配器模式?Spring MVC 中的 Controller 种类众多,不同类型的 Controller 通过不同的方法来对请求进行处理。如果不利用适配器模式的话,DispatcherServlet 直接获取对应类型的 Controller,需要的自行来判断,像下面这段代码一样:
if(mappedHandler.getHandler() instanceof MultiActionController){
((MultiActionController)mappedHandler.getHandler()).xxx
}else if(mappedHandler.getHandler() instanceof XXX){
...
}else if(...){
...
}