聊一聊介面卡模式

知了一笑發表於2023-05-17

介面不能用?行,我幫你適配

一、概述

介面卡模式(Adapter),是23種設計模式中的結構型模式之一;它就像我們電腦上介面不夠時,需要用到的擴充塢,起到轉接的作用。它可以將新的功能和原先的功能連線起來,使由於需求變動導致不能用的功能,重新利用起來。

上圖的Mac上,只有兩個typec介面,當我們需要用到USB、網線、HDMI等介面時,這就不夠用了,所以我們需要一個擴充塢來增加電腦的介面

言歸正傳,下面來了解下介面卡模式中的角色:請求者(client)、目標角色(Target)、源角色(Adaptee)、介面卡角色(Adapter),這四個角色是保證這個設計模式執行的關鍵。

  • client:需要使用介面卡的物件,不需要關心介面卡內部的實現,只對接目標角色。
  • Target:目標角色,和client直接對接,定義了client需要用到的功能。
  • Adaptee:需要被進行適配的物件。
  • Adapter:介面卡,負責將源物件轉化,給client做適配。

二、入門案例

介面卡模式也分兩種:物件介面卡、類介面卡。其實兩種方式的區別在於,介面卡類中的實現,類介面卡是透過繼承源物件的類,物件介面卡是引用源物件的類。

當然兩種方式各有優缺點,我們分別來說下;

類介面卡:由於採用繼承模式,在介面卡中可以重寫Adaptee原有的方法,使得介面卡可以更加靈活;但是有侷限性,Java是單繼承模式,所以介面卡類只能繼承Adaptee,不能在額外繼承其他類,也導致Target類只能是介面。

物件介面卡:這個模式規避了單繼承的劣勢,將Adaptee類用引用的方式傳遞給Adapter,這樣可以傳遞的是Adaptee物件本身及其子類物件,相比類介面卡更加的開放;但是也正是因為這種開放性,導致需要自己重新定義Adaptee,增加額外的操作。

類介面卡UML圖

物件介面卡UML圖

下面,是結合上面電腦的場景,寫的一個入門案例,分別是四個類:ClientAdapteeAdapterTarget,代表了介面卡模式中的四種角色。

/**
 * @author 往事如風
 * @version 1.0
 * @date 2023/5/9 15:54
 * @description:源角色
 */
public class Adaptee {
    /**
     * 需要被適配的適配的功能
     * 以Mac筆記本的typec介面舉例
     */
    public void typeC() {
        System.out.println("我只是一個typeC介面");
    }
}
/**
 * @author 往事如風
 * @version 1.0
 * @date 2023/5/9 15:57
 * @description:目標介面
 */
public interface Target {

    /**
     * 定義一個轉接功能的入口
     */
    void socket();
}
/**
 * @author 往事如風
 * @version 1.0
 * @date 2023/5/9 16:00
 * @description:介面卡
 */
public class Adapter extends Adaptee implements Target {

    /**
     * 實現適配功能
     * 以Mac的擴充塢為例,擴充更多的介面:usb、typc、網線插口...
     */
    @Override
    public void socket() {
        typeC();
        System.out.println("新增usb插口。。。");
        System.out.println("新增網線插口。。。");
        System.out.println("新增typec插口。。。");
    }
}
/**
 * @author 往事如風
 * @version 1.0
 * @date 2023/5/9 15:52
 * @description:請求者
 */
public class Client {

    public static void main(String[] args) {
        Target target = new Adapter();
        target.socket();
    }
}

這個案例比較簡單,僅僅是一個入門的demo,也是類介面卡模式的案例,採用繼承模式。在物件介面卡模式中,區別就是Adapter這個介面卡類,採用的是組合模式,下面是物件介面卡模式中Adapter的程式碼;

/**
 * @author 往事如風
 * @version 1.0
 * @date 2023/5/9 16:00
 * @description:介面卡
 */
public class Adapter implements Target {

    private Adaptee adaptee;

    public Adapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }

    /**
     * 實現適配功能
     * 以Mac的擴充塢為例,擴充更多的介面:usb、typc、網線插口...
     */
    @Override
    public void socket() {
        adaptee.typeC();
        System.out.println("新增usb插口。。。");
        System.out.println("新增網線插口。。。");
        System.out.println("新增typec插口。。。");
    }
}

三、運用場景

其實介面卡模式為何會存在,全靠“爛程式碼”的襯托。在初期的設計上,一代目沒有考慮到後期的相容性問題,只顧自己一時爽,那後期接手的人就會感覺到頭疼,就會有“還不如重寫這段程式碼的想法”。但是這部分程式碼往往都是經過N代人的充分測試,穩定性比較高,一時半會還不能對它下手。這時候我們的介面卡模式就孕育而生,可以在不動用老程式碼的前提下,實現新邏輯,並且能做二次封裝。這種場景,我在之前的系統重構中深有體會,不說了,都是淚。

當然還存在一種情況,可以對不同的外部資料進行統一輸出。例如,寫一個獲取一些資訊的介面,你對前端暴露的都是統一的返回欄位,但是需要呼叫不同的外部api獲取不同的資訊,不同的api返回給你的欄位都是不同的,比如企業工商資訊、使用者賬戶資訊、使用者津貼資訊等等。下面我對這種場景具體分析下;

首先,我定義一個介面,接收使用者id和資料型別兩個引數,定義統一的輸出欄位。

/**
 * @author 往事如風
 * @version 1.0
 * @date 2023/5/10 11:03
 * @description
 */
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserInfoController {

    private final UserInfoTargetService userInfoTargetService;

    @PostMapping("/info")
    public Result<DataInfoVo> queryInfo(@RequestParam Integer userId, @RequestParam String type) {
        return Result.success(userInfoTargetService.queryData(userId, type));
    }
}

定義統一的輸出的類DataInfoVo,這裡定義的欄位需要暴露給前端,具體業務意義跟前端商定。

/**
 * @author 往事如風
 * @version 1.0
 * @date 2023/5/10 14:40
 * @description
 */
@Data
public class DataInfoVo {
    /**
     * 名稱
     */
    private String name;
    /**
     * 型別
     */
    private String type;
    /**
     * 預留欄位:具體業務意義自行定義
     */
    private Object extInfo;
}

然後,定義Target介面(篇幅原因,這裡不做展示),Adapter介面卡類,這裡採用的是物件介面卡,由於單繼承的限制,物件介面卡也是最常用的介面卡模式。

/**
 * @author 往事如風
 * @version 1.0
 * @date 2023/5/10 15:09
 * @description
 */
@Service
@RequiredArgsConstructor
public class UserInfoAdapter implements UserInfoTargetService {
    /**
     * 源資料類管理器
     */
    private final AdapteeManager adapteeManager;

    @Override
    public DataInfoVo queryData(Integer userId, String type) {
        // 根據型別,得到唯一的源資料類
        UserBaseAdaptee adaptee = adapteeManager.getAdaptee(type);
        if (Objects.nonNull(adaptee)) {
            Object data = adaptee.getData(userId, type);
            return adaptee.convert(data);
        }
        return null;
    }
}

這裡定義了一個AdapteeManager類,表示管理Adaptee類,內部維護一個map,用於儲存真實Adaptee類。

/**
 * @author 往事如風
 * @version 1.0
 * @date 2023/5/10 15:37
 * @description
 */
public class AdapteeManager {

    private Map<String, UserBaseAdaptee> baseAdapteeMap;

    public void setBaseAdapteeMap(List<UserBaseAdaptee> adaptees) {
        baseAdapteeMap = adaptees.stream()
                .collect(Collectors.toMap(handler -> AnnotationUtils.findAnnotation(handler.getClass(), Adapter.class).type(), v -> v, (v1, v2) -> v1));
    }

    public UserBaseAdaptee getAdaptee(String type) {
        return baseAdapteeMap.get(type);
    }
}

最後,按照資料型別,定義了三個Adaptee類:AllowanceServiceAdaptee(津貼)、BusinessServiceAdaptee(企業工商)、UserAccountServiceAdaptee(使用者賬戶)。

/**
 * @author 往事如風
 * @version 1.0
 * @date 2023/5/10 15:00
 * @description
 */
@Adapter(type = "JT")
public class AllowanceServiceAdaptee implements UserBaseAdaptee {

    @Override
    public Object getData(Integer userId, String type) {
        // 模擬呼叫外部api,查詢津貼資訊
        AllowanceVo allowanceVo = new AllowanceVo();
        allowanceVo.setAllowanceType("管理津貼");
        allowanceVo.setAllowanceAccount("xwqeretry2345676");
        allowanceVo.setAmount(new BigDecimal(20000));
        return allowanceVo;
    }

    @Override
    public DataInfoVo convert(Object data) {
        AllowanceVo preConvert = (AllowanceVo) data;
        DataInfoVo dataInfoVo = new DataInfoVo();
        dataInfoVo.setName(preConvert.getAllowanceAccount());
        dataInfoVo.setType(preConvert.getAllowanceType());
        dataInfoVo.setExtInfo(preConvert.getAmount());
        return dataInfoVo;
    }
}
/**
 * @author 往事如風
 * @version 1.0
 * @date 2023/5/10 15:00
 * @description
 */
@Adapter(type = "QY")
public class BusinessServiceAdaptee implements UserBaseAdaptee {

    @Override
    public Object getData(Integer userId, String type) {
        // 模擬呼叫外部api,查詢企業工商資訊
        BusinessVo businessVo = new BusinessVo();
        businessVo.setBusName("xxx科技有限公司");
        businessVo.setBusCode("q24243Je54sdfd99");
        businessVo.setBusType("中大型企業");
        return businessVo;
    }

    @Override
    public DataInfoVo convert(Object data) {
        BusinessVo preConvert = (BusinessVo) data;
        DataInfoVo dataInfoVo = new DataInfoVo();
        dataInfoVo.setName(preConvert.getBusName());
        dataInfoVo.setType(preConvert.getBusType());
        dataInfoVo.setExtInfo(preConvert.getBusCode());
        return dataInfoVo;
    }
}
/**
 * @author 往事如風
 * @version 1.0
 * @date 2023/5/10 15:00
 * @description
 */
@Adapter(type = "YH")
public class UserAccountServiceAdaptee implements UserBaseAdaptee {

    @Override
    public Object getData(Integer userId, String type) {
        // 模擬呼叫外部api,查詢企業工商資訊
        UserAccountVo userAccountVo = new UserAccountVo();
        userAccountVo.setAccountNo("afsdfd1243567");
        userAccountVo.setAccountType("銀行卡");
        userAccountVo.setName("中國農業銀行");
        return userAccountVo;
    }

    @Override
    public DataInfoVo convert(Object data) {
        UserAccountVo preConvert = (UserAccountVo) data;
        DataInfoVo dataInfoVo = new DataInfoVo();
        dataInfoVo.setName(preConvert.getName());
        dataInfoVo.setType(preConvert.getAccountType());
        dataInfoVo.setExtInfo(preConvert.getAccountNo());
        return dataInfoVo;
    }
}

這三個類都實現一個介面UserBaseAdaptee,該介面定義了統一的規範

/**
 * @author 往事如風
 * @version 1.0
 * @date 2023/5/10 15:03
 * @description
 */

public interface UserBaseAdaptee {
    /**
     * 獲取資料
     * @param userId
     * @param type
     * @return
     */
    Object getData(Integer userId, String type);

    /**
     * 資料轉化為統一的實體
     * @param data
     * @return
     */
    DataInfoVo convert(Object data);
}

這些類中,其實重點看下UserInfoAdapter介面卡類,這裡做的操作是透過源資料類,拿到外部返回的資料,最後將不同的資料轉化為統一的欄位,返回出去。

這裡我沒有按照固定的模式,稍加了改變。將介面卡類中引用源資料類的方式,改成將源資料類加入map中暫存,最後透過前端傳輸的type欄位來獲取源資料類,這也是物件介面卡比較靈活的一種體現。

四、原始碼中的運用

在JDK的原始碼中,JUC下有個類FutureTask,其中它的一段構造方法如下:

public class FutureTask<V> implements RunnableFuture<V> {
    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }
    
	public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }
}

其中一個建構函式中,callable是透過Executors類的方法進行適配的,透過一個RunnableAdapter的介面卡類,進行包裝並返回

public static <T> Callable<T> callable(Runnable task, T result) {
        if (task == null)
            throw new NullPointerException();
        return new RunnableAdapter<T>(task, result);
    }
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;
        }
        public T call() {
            task.run();
            return result;
        }
    }

這樣的話,無論傳入Runnable還是Callable都可以適配任務,雖然看著是呼叫了Callable的call方法,實際內部是呼叫了Runnable的run方法,並且將傳入的返回資料返回給外部使用。

五、總結

介面卡模式其實是一個比較好理解的設計模式,但是對於大多數初學者而言,就會很容易看一遍之後立馬忘,這是缺少實際運用造成的。其實程式設計主要考察的還是我們的一種思維模式,就像這個介面卡模式,理解它的運用場景最重要。如果給你一個業務場景,你能在腦海中有大致的設計思路或者解決方案,那你就已經掌握精髓了。至於具體的落地,有些細節忘記也是在所難免,翻翻資料就會立馬回到腦海中。

最後,每次遇到問題,用心總結,你會離成功更近一步。

相關文章