日常开发系统中通常需要对接多个系统,需要用到适配器模式。
例如:支付方式就涉及多个系统对接。
国际惯例,先引入概念。
适配器模式:
提到适配器自然就能想到手机用的电源适配器。
他的作用就是将220V交流电转换成手机使用的5V直流电。
适配器作用:将一个接口转换成另外一个接口,已符合客户的期望。
软件系统中,比如一期我们使用了阿里云的sdk包的一些功能接口。
但是二期我想换腾讯云sdk相同的功能。但是他们相同的功能,接口参数确不同。
我们不想按照腾讯云的接口修改我们的业务代码,毕竟业务逻辑已经经过反复测试验证了。可以把腾讯云的接口包装起来,实现一个一期的接口。这个工作叫【适配】。
在例如:
在软件系统中,你可能有很多种支付方式,微信支付,支付宝支付,各种银行。
但是他们的支付接口肯定都是不一样的,我不希望我新加一种支付方式就加去修改代码。
此时就需要有一个统一支付的适配器服务帮我们屏蔽各个支付方式的不同。
我的业务服务只和统一支付交互。统一支付向业务系统提供统一接口,统一支付负责路由不同支付系统后台,并屏蔽掉各系统差异。这个工作也叫【适配】
适配模式:
/** * 目标接口:提供5V电压的一个接口 */ public interface V5Power { public int provideV5Power(); } /** * 被适配者、已有功能:家用220V交流电 */ public class V220Power { /** * 提供220V电压 */ public int provideV220Power() { System.out.println("我提供220V交流电压。"); return 220 ; } } /** * 适配器,有已有对象已有功能实现接口,把220V电压变成5V */ public class V5PowerAdapter implements V5Power { /** * 组合的方式 */ private V220Power v220Power ; public V5PowerAdapter(V220Power v220Power) { this.v220Power = v220Power ; } @Override public int provideV5Power() { int power = v220Power.provideV220Power() ; //power经过各种操作-->5 System.out.println("适配器:我悄悄的适配了电压。"); return 5 ; } }适配器
public class Mobile { // 使用目标接口功能 public void inputPower(V5Power power) { int provideV5Power = power.provideV5Power(); System.out.println("手机(客户端):我需要5V电压充电,现在是-->" + provideV5Power + "V"); } } //测试 public class Test { public static void main(String[] args) { Mobile mobile = new Mobile(); V5Power v5Power = new V5PowerAdapter(new V220Power()) ; mobile.inputPower(v5Power); } }Test
定义:
将一个类的接口,转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作无间。
适配模式说的通俗点就是用一个已有的功能类,去实现一个接口。这样该功能类,和原来的客户端代码都不需要改变,对于客户端来说相当于换了一种实现方式。
好处就是让客户从实现中【解耦】,下次在换其他的功能类,我就再写一个适配器。有点像策略模式中,我们换一个实现类一样。
当然,我们适配器可以包装很多被适配对象,即可以组合很多已有功能类。因为很多接口很复杂需要使用很多类。
同样,适配器也可以不使用【组合】对象形式,而是使用【继承】。
示例:
以前java集合类都实现了Enumeration枚举接口,可以遍历集合中每个元素,而无需知道它们在集合内元素是如何被管理。
之后推出了新的Iterator迭代器接口,这个接口和枚举接口很像,都可以让你遍历集合中每个元素。
不同的是,迭代器还提供了删除元素的能力。
面对遗留代码,这些代码暴露出来的是枚举接口,他们是老版本的java只支持枚举,但是我们希望新的代码中使用迭代器。只能用枚举实现一个迭代器。
public class EnumerationIterator implements Iterator{ Enumeration enum; public EnumerationIterator(Enumeration enum){ this.enum = enum; } public boolean hasNext(){ return enum.hasMoreElements(); } public Object next(){ return enum.nextElement(); } public void remove(){ throw new UnsupportedOperationException(); } } /* 枚举接口是只读的,适配器无法实现一个有实际功能的remove()方法我们的实现方式并不完美,客户必须小心潜在的异常。只要客户足够小心,并且在适配器的文档中作出说明,这也算是一个合理的解决方案。 */用枚举实现迭代器
外观模式:
外观模式:提供一个统一接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统更容易使用。
例如:我们遥控器点击下自动就打开幕布、打开投影机、打开音响、开始放电影。而不是一步一步的自己操作。
不仅仅是简化了接口,也将客户从组建汇总解耦了出来。
目的就是让系统更加容易使用,也符合【最少知道】原则,因为客户只有【外观角色】一个朋友。
【最少知道】原则就是不要让太多类耦合在一起,免得修改系统时候,牵一发而动全身。对象尽量“少交朋友”。之和自己有关的交互即可。
就对象而言,在该对象的方法内,我们只应该调用属于以下范围的方法:
1.该对象本身
2.被当做方法的参数而传递进来的对象
3.此方法所创建或实例化的任何对象
4.对象的任何组建,即属性变量引用的对象
如果调用返回对象的方法,相当于向另外一个对象的子部分发出请求。我们就多依赖了一个对象。
// 不应用【最少知道】原则
public float getTemp(){
Thermometer thermometer = station.getThermometer();
return thermometer.getTemperature(); // 我们从气象站获取了温度计对象,然后从该对象获取了温度。
}
// 应用【最少知道】原则
public float getTemp(){
return station.getTemperature(); // 应该直接让气象站给我温度,我不想依赖温度计对象。
}
装饰模式:
用面向对象的方式对饮料进行描述。这种描述方式的局限在于。
我们饮料可以添加辅料。例如:大杯、加冰、加奶、双倍咖啡。
我们不可能一种组合就建一个类。那样类数量就爆炸了。
此时计算价格就非常困难。
将各种调料放在基类中。调料可以是boolean或者数据或者枚举类型。
每种饮料计算价钱的时候就看这些调料是否有,或者有多少。
public class Drink{ private boolean milk; private boolean sugar; public double cost(){ double price = 0d; if(milk){ price +=0.5; } if(sugar){price +=0.1; } return price; } public void addMilk(){ this.mocha=true; } public void addSugar(){ this.whip= true; } } public class DarkCoffee extends Drink{ public double cost(){ double price = super.cost(); price += 2.0; return price; } } 测试: public class Test{ public static void main(String[] args){ Drink coffee = new DarkCoffee(); coffee.addMocha(); coffee.addWhip(); System.out.println(coffee.cost()); } }通过设置属性解决调料问题
这种设计存在问题:
1.如果调味品很多,Drink类非常庞大,且新加和删除调味品都需要修改类,调料改价格需要调整价格。
2.双倍调味料的情况boolean值无法满足。
3.很多调料是互斥的,例如iceCoffee是不能加mocha的,但是他还是继承了父类的addMocha()方法。这个方法他不适用,他必须覆盖这个方法让它什么都不做。
不符合【开闭原则】
装饰模式解释:
调料可以包装基础饮料,因为装饰者和被装饰者有相同的超类型,所以可以套娃形式一直套下去。
装饰者可以在所委托被装饰者的行为前后,加上自己的行为,已达到特定目的。
计算价格类似于递归,不断委托给父类,最后统一回溯。
abstract class Drink{ public String description = "Unknown beverage"; public String getDescription(){ return description; } public abstract double cost(); } // 装饰类 abstract class CondimentDecorator extends Drink{ public abstract String getDescription(); } // 饮料 class DarkCoffee extends Drink{ public DarkCoffee(){ this.description = "DarkCoffee"; } public double cost(){ return 1.99; } } // 包装类:调料 class Sugar extends CondimentDecorator{ Drink drink; public Sugar(Drink drink){ this.drink = drink; } public String getDescription(){ return drink.getDescription() + ", add sugar"; } public double cost(){ return .20 + drink.cost(); } } //测试 class Test{ public static void main(String[] args){ Drink drink = new DarkCoffee(); drink = new Sugar(drink); // 加双份糖 drink = new Sugar(drink); System.out.println(drink.getDescription() + " ,$"+drink.cost()); //输出:DarkCoffee, add sugar, add sugar ,$2.39 } }装饰模式
public interface Drink{ public double cost(); } public class DarkCoffee implements Drink{ public double cost(){ return 2.5; } } public class Decorator implements Drink{ private Drink drink; public Decorator (Drink drink){ this.drink= drink; } public double cost(){ return drink.cost(); } } public class Sugar extends Decorator { public Sugar (Drink drink){ super(drink); } public double cost(){ return super.cost() + 0.1; } }装饰模式2
装饰模式:动态将责任附加到对象上,若要拓展功能,装饰者提供了比继承更有弹性的替代方案。
装饰者模式容易造成设计中大量的小类。数量太多。容易把人看懵。例如:java.io
/* 编写一个装饰者,将输入流内所有大写字符转小写。 我们需要拓展InputStream。 */ public class LowerCaseInputStream extends FilterInputStream { public LowerCaseInputStream(InputStream in) { super(in); } public int read() throws IOException { int c = in.read(); return (c == -1 ? c : Character.toLowerCase((char)c)); } public int read(byte[] b, int offset, int len) throws IOException { int result = in.read(b, offset, len); for (int i = offset; i < offset+result; i++) { b[i] = (byte)Character.toLowerCase((char)b[i]); } return result; } } public class InputTest { public static void main(String[] args) throws IOException { int c; InputStream in = null; try { in = new LowerCaseInputStream( new BufferedInputStream( new FileInputStream("test.txt"))); while((c = in.read()) >= 0) { System.out.print((char)c); } } catch (IOException e) { e.printStackTrace(); } finally { if (in != null) { in.close(); } } System.out.println(); try (InputStream in2 = new LowerCaseInputStream( new BufferedInputStream( new FileInputStream("test.txt")))) { while((c = in2.read()) >= 0) { System.out.print((char)c); } } catch (IOException e) { e.printStackTrace(); } } }自己包装IO
例如:我们对HttpServletRequest进行拦截进行处理,实现过滤请求参数。
1). Servlet API 中提供了一个 HttpServletRequestWrapper 类来包装原始的 request 对象, HttpServletRequestWrapper 类实现了 HttpServletRequest 接口中的所有方法, 这些方法的内部实现都是仅仅调用了一下所包装的的 request 对象的对应方法 //包装类实现 ServletRequest 接口. public class ServletRequestWrapper implements ServletRequest { //被包装的那个 ServletRequest 对象 private ServletRequest request; //构造器传入 ServletRequest 实现类对象 public ServletRequestWrapper(ServletRequest request) { if (request == null) { throw new IllegalArgumentException("Request cannot be null"); } this.request = request; } //具体实现 ServletRequest 的方法: 调用被包装的那个成员变量的方法实现。 public Object getAttribute(String name) { return this.request.getAttribute(name); } public Enumeration getAttributeNames() { return this.request.getAttributeNames(); } //... } 2). 作用: 用于对 HttpServletRequest 或 HttpServletResponse 的某一个方法进行修改或增强. public class MyHttpServletRequest extends HttpServletRequestWrapper{ public MyHttpServletRequest(HttpServletRequest request) { super(request); } @Override public String getParameter(String name) { String val = super.getParameter(name); if(val != null && val.contains(" fuck ")){ val = val.replace("fuck", "****"); } return val; } } 3). 使用: 在 Filter 中, 利用 MyHttpServletRequest 替换传入的 HttpServletRequest HttpServletRequest req = new MyHttpServletRequest(request); filterChain.doFilter(req, response);增强HttpServletRequest
例如:在一个方法前后打印时间
// 接口 public interface Dao { public void insert(); public void delete(); public void update(); } // 基础实现类 public class DaoImpl implements Dao { @Override public void insert() { System.out.println("DaoImpl.insert()"); } @Override public void delete() { System.out.println("DaoImpl.delete()"); } @Override public void update() { System.out.println("DaoImpl.update()"); } } // 包装类 public class LogDao implements Dao { private Dao dao; public LogDao(Dao dao) { this.dao = dao; } @Override public void insert() { System.out.println("insert()方法开始时间:" + System.currentTimeMillis()); dao.insert(); System.out.println("insert()方法结束时间:" + System.currentTimeMillis()); } @Override public void delete() { dao.delete(); } @Override public void update() { System.out.println("update()方法开始时间:" + System.currentTimeMillis()); dao.update(); System.out.println("update()方法结束时间:" + System.currentTimeMillis()); } } // 调用时候 Dao dao = new LogDao(new DaoImpl()); // 对于调用方来说,只知道调用了dao,不知道加上了日志功能 // 问题:1.输出日志的逻辑无法复用;2.输入日志和业务逻辑有耦合。装饰模式方法前后打日志
其实装饰模式和适配器模式很像。都是包装一个对象,然后利用这个对象的功能搞出点对外提供的方法。
装饰模式
适配器模式
外观模式
目的:不改变接口,加入责任
目的:将一个接口转成另一个接口
目的:让接口跟简单装饰者模式可以让新的行为和责任要加入到设计中。而无需修改现有的代码。
即:方法还是原方法,但是在原方法前后加点东西。
可以整合若干类来提供客户需要的接口。
即将一个不兼容的接口对象包装起来,变成兼容的对象。
当一个方法调用被委托给装饰者的时候,
不知道有多少其他装饰者已经处理过这个调用了。
装饰者有点像递归套娃,你不知道当前是第几层。
可以使用新的库和子集合,而无需改变任何代码。适配器会按照原接口给你包好。
拓展包装对象的行为和责任,不是简单的传送
一定进行接口的装换。
示例:多支付系统解决方案
通过加入适配器模式,订单 Service 在进行支付时调用的不再是外部的支付接口,而是“支付方式”接口,与外部系统解耦。
只要保证“支付方式”接口是稳定的,那么订单 Service 就是稳定的。比如:
当支付宝支付接口发生变更时,影响的只限于支付宝 Adapter;
当微信支付接口发生变更时,影响的只限于微信支付 Adapter;
当要增加一个新的支付方式时,只需要再写一个新的 Adapter。
日后不论哪种变更,要修改的代码范围缩小了,维护成本自然降低了,代码质量就提高了。
问题:
在划分微服务过程中,经常纠结,对外的功能有没有必要抽象出一个代理服务。专门负责与某厂商对接。
根据适配器的理念,如果这个代理服务如果能抽象出标准的接口,那他就有独立的必要。核心业务服务只和代理服务交互,代理服务屏蔽外部厂商的不同。
日后厂商有变动,我们修改范围控制在代理服务内,不会涉及到核心业务服务。
例如:我们一般会开发一个统一支付的服务,这个服务对接各个支付系统,对内提供标准支付接口,业务服务只和统一支付交互。
同时统一支付还能帮助我们处理对账、过期自动退款等功能。让业务系统稳定,轻便。