Files
knowledge-kit/Chapter6 - Design Pattern/6.16.md
2024-07-15 20:03:01 +08:00

292 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 适配器模式
主要用来解决:一个现有类需要适应变化的问题。
## 适配器模式的原理
适配器模式的英文翻译是 Adapter Design Pattern。顾名思义这个模式就是用来做适配的它将不兼容的接口转换为可兼容的接口让原本由于接口不兼容而不能一起工作的类可以一起工作。举个现实的例子USB 转接头充当适配器,把两种不兼容的接口,通过转接变得可以一起工作
## 适配器模式的实现
适配器模式有两种实现方式:
- 类适配器,使用继承关系来实现
- 对象适配器,对象适配器使用组合关系来实现
### 对象适配器
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ObjectAdapter.png" style="zoom:30%" />
假设一个类存在年代久远,如果需要适配,则需要创建一个适配对象。然后被适配的对象以成员变量的形式集成到适配对象中。
```
- (void)handleLogic {
// 适配逻辑
[被适配对象 对象方法];
// 适配逻辑
}
```
举个例子:
- ITarget 表示要转化成的接口定义
- Adaptee 是一组不兼容 ITarget 接口定义的接口
- Adaptor 将 Adaptee 转化成一组符合 ITarget 接口定义的接口
类适配器: 基于继承
```
public interface ITarget {
void f1();
void f2();
void fc();
}
public class Adaptee {
public void fa() { //... }
public void fb() { //... }
public void fc() { //... }
}
public class Adaptor extends Adaptee implements ITarget {
public void f1() {
super.fa();
}
public void f2() {
//...重新实现f2()...
}
// 这里fc()不需要实现直接继承自Adaptee这是跟对象适配器最大的不同点
}
```
对象适配器:基于组合
```
public interface ITarget {
void f1();
void f2();
void fc();
}
public class Adaptee {
public void fa() { //... }
public void fb() { //... }
public void fc() { //... }
}
public class Adaptor implements ITarget {
private Adaptee adaptee;
public Adaptor(Adaptee adaptee) {
this.adaptee = adaptee;
}
public void f1() {
adaptee.fa(); //委托给Adaptee
}
public void f2() {
//...重新实现f2()...
}
public void fc() {
adaptee.fc();
}
}
```
针对这两种实现方式,在实际的开发中,到底该如何选择使用哪一种呢?判断的标准主要有两个,一个是 Adaptee 接口的个数,另一个是 Adaptee 和 ITarget 的契合程度:
- 如果 Adaptee 接口并不多,那两种实现方式都可以。
- 如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都相同,推荐使用类适配器,因为 Adaptor 复用父类 Adaptee 的接口比起对象适配器的实现方式Adaptor 的代码量要少一些。
- 如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都不相同,推荐使用对象适配器,因为组合结构相对于继承更加灵活
## 适配器模式应用场景总结
适配器模式可以看作一种“补偿模式”,用来补救设计上的缺陷。应用这种模式算是“无奈之举”。如果在设计初期,我们就能协调规避接口不兼容的问题,那这种模式就没有应用的机会了
适配器模式的应用场景是“接口不兼容”。那在实际的开发中,什么情况下才会出现接口不兼容呢?
### 封装有缺陷的接口设计
假设我们依赖的外部系统在接口设计方面有缺陷(比如包含大量静态方法),引入之后会影响到我们自身代码的可测试性。为了隔离设计上的缺陷,我们希望对外部系统提供的接口进行二次封装,抽象出更好的接口设计,这个时候就可以使用适配器模式了。
```java
public class CD { //这个类来自外部sdk我们无权修改它的代码
//...
public static void staticFunction1() { //... }
public void uglyNamingFunction2() { //... }
public void tooManyParamsFunction3(int paramA, int paramB, ...) { //... }
public void lowPerformanceFunction4() { //... }
}
// 使用适配器模式进行重构
public class ITarget {
void function1();
void function2();
void fucntion3(ParamsWrapperDefinition paramsWrapper);
void function4();
//...
}
// 注意适配器类的命名不一定非得末尾带Adaptor
public class CDAdaptor extends CD implements ITarget {
//...
public void function1() {
super.staticFunction1();
}
public void function2() {
super.uglyNamingFucntion2();
}
public void function3(ParamsWrapperDefinition paramsWrapper) {
super.tooManyParamsFunction3(paramsWrapper.getParamA(), ...);
}
public void function4() {
//...reimplement it...
}
}
```
### 统一多个类的接口设计
某个功能的实现依赖多个外部系统(或者说类)。通过适配器模式,将它们的接口适配为统一的接口定义,然后我们就可以使用多态的特性来复用代码逻辑。
假设我们的系统要对用户输入的文本内容做敏感词过滤,为了提高过滤的召回率,我们引入了多款第三方敏感词过滤系统,依次对用户输入的内容进行过滤,过滤掉尽可能多的敏感词。但是,每个系统提供的过滤接口都是不同的。这就意味着我们没法复用一套逻辑来调用各个系统。这个时候,我们就可以使用适配器模式,将所有系统的接口适配为统一的接口定义,这样我们可以复用调用敏感词过滤的代码。
```java
public class ASensitiveWordsFilter { // A敏感词过滤系统提供的接口
//text是原始文本函数输出用***替换敏感词之后的文本
public String filterSexyWords(String text) {
// ...
}
public String filterPoliticalWords(String text) {
// ...
}
}
public class BSensitiveWordsFilter { // B敏感词过滤系统提供的接口
public String filter(String text) {
//...
}
}
public class CSensitiveWordsFilter { // C敏感词过滤系统提供的接口
public String filter(String text, String mask) {
//...
}
}
// 未使用适配器模式之前的代码:代码的可测试性、扩展性不好
public class RiskManagement {
private ASensitiveWordsFilter aFilter = new ASensitiveWordsFilter();
private BSensitiveWordsFilter bFilter = new BSensitiveWordsFilter();
private CSensitiveWordsFilter cFilter = new CSensitiveWordsFilter();
public String filterSensitiveWords(String text) {
String maskedText = aFilter.filterSexyWords(text);
maskedText = aFilter.filterPoliticalWords(maskedText);
maskedText = bFilter.filter(maskedText);
maskedText = cFilter.filter(maskedText, "***");
return maskedText;
}
}
// 使用适配器模式进行改造
public interface ISensitiveWordsFilter { // 统一接口定义
String filter(String text);
}
public class ASensitiveWordsFilterAdaptor implements ISensitiveWordsFilter {
private ASensitiveWordsFilter aFilter;
public String filter(String text) {
String maskedText = aFilter.filterSexyWords(text);
maskedText = aFilter.filterPoliticalWords(maskedText);
return maskedText;
}
}
//...省略BSensitiveWordsFilterAdaptor、CSensitiveWordsFilterAdaptor...
// 扩展性更好,更加符合开闭原则,如果添加一个新的敏感词过滤系统,
// 这个类完全不需要改动;而且基于接口而非实现编程,代码的可测试性更好。
public class RiskManagement {
private List<ISensitiveWordsFilter> filters = new ArrayList<>();
public void addSensitiveWordsFilter(ISensitiveWordsFilter filter) {
filters.add(filter);
}
public String filterSensitiveWords(String text) {
String maskedText = text;
for (ISensitiveWordsFilter filter : filters) {
maskedText = filter.filter(maskedText);
}
return maskedText;
}
}
```
### 替换依赖的外部系统
当我们把项目中依赖的一个外部系统替换为另一个外部系统的时候,利用适配器模式,可以减少对代码的改动。
```java
// 外部系统A
public interface IA {
//...
void fa();
}
public class A implements IA {
//...
public void fa() { //... }
}
// 在我们的项目中外部系统A的使用示例
public class Demo {
private IA a;
public Demo(IA a) {
this.a = a;
}
//...
}
Demo d = new Demo(new A());
// 将外部系统A替换成外部系统B
public class BAdaptor implemnts IA {
private B b;
public BAdaptor(B b) {
this.b= b;
}
public void fa() {
//...
b.fb();
}
}
// 借助 BAdaptorDemo 的代码中,调用 IA 接口的地方都无需改动,
// 只需要将BAdaptor如下注入到Demo即可。
Demo d = new Demo(new BAdaptor(new B()));
```
### 兼容老版本
在做版本升级的时候,对于一些要废弃的接口,我们不直接将其删除,而是暂时保留,并且标注为 deprecated并将内部实现逻辑委托为新的接口实现。这样做的好处是让使用它的项目有个过渡期而不是强制进行代码修改。这也可以粗略地看作适配器模式的一个应用场景
### 适配不同格式的数据
前面我们讲到适配器模式主要用于接口的适配实际上它还可以用在不同格式的数据之间的适配。比如把从不同征信系统拉取的不同格式的征信数据统一为相同的格式以方便存储和使用。再比如Java 中的 Arrays.asList() 也可以看作一种数据适配器,将数组类型的数据转化为集合容器类型。
```
List<String> stooges = Arrays.asList("Larry", "Moe", "Curly");
```
## 代理、桥接、装饰器、适配器 4 种设计模式的区别
代理、桥接、装饰器、适配器,这 4 种模式是比较常用的结构型设计模式。它们的代码结构非常相似。笼统来说,它们都可以称为 Wrapper 模式,也就是通过 Wrapper 类二次封装原始类
尽管代码结构相似,但这 4 种设计模式的用意完全不同,也就是说要解决的问题、应用场景不同,这也是它们的主要区别
- 代理模式:不改变原始类接口,为原始类定义一个代理类,主要的目的是为了访问控制,隔离原始代码。而非增加功能
- 桥接模式:为了接口和实现分离,做到更好的解耦,可以让类更好、更容易的独立改变
- 装饰器模式:在不改变类原始接口的情况下,对类的功能进行加强,并且支持多个装饰器的嵌套使用
- 适配器模式:类似一个事后补救策略,提供跟原始类不同的接口,主要为了抹平不同接口的差异性,做到一致性。