重學 Java 設計模式:實戰適配器模式

image

做者:小傅哥
博客:https://bugstack.cnhtml

沉澱、分享、成長,讓本身和他人都能有所收穫!😄

1、前言

擦屁屁紙80%的面積都是保護手的!java

工做到3年左右很大一部分程序員都想提高本身的技術棧,開始嘗試去閱讀一些源碼,例如SpringMybaitsDubbo等,但讀着讀着發現愈來愈難懂,一會從這過來一會跑到那去。甚至懷疑本身技術太差,慢慢也就不肯意再觸碰這部分知識。程序員

而這主要的緣由是一個框架隨着時間的發展,它的複雜程度是愈來愈高的,從最開始只有一個很是核心的點到最後開枝散葉。這就像你本身開發的業務代碼或者某個組件同樣,最開始的那部分核心代碼也許只能佔到20%,而其餘大部分代碼都是爲了保證核心流程能正常運行的。因此這也是你讀源碼費勁的一部分緣由。數據庫

框架中用到了設計模式嗎?json

框架中不只用到設計模式還用了不少,並且有些時候根本不是一個模式的單獨使用,而是多種設計模式的綜合運用。與大部分小夥伴平時開發的CRUD可就不同了,若是都是if語句從上到下,也就算得不上什麼框架了。就像你到Spring的源碼中搜關鍵字Adapter,就會出現不少實現類,例如;UserCredentialsDataSourceAdapter。而這種設計模式就是咱們本文要介紹的適配器模式。設計模式

適配器在生活裏隨處可見框架

若是提到在平常生活中就不少適配器的存在你會想到什麼?在沒有看後文以前能夠先思考下。ide

2、開發環境

  1. JDK 1.8
  2. Idea + Maven
  3. 涉及工程三個,能夠經過關注公衆號bugstack蟲洞棧,回覆源碼下載獲取(打開獲取的連接,找到序號18)
工程 描述
itstack-demo-design-6-00 場景模擬工程;模擬多個MQ消息體
itstack-demo-design-6-01 使用一坨代碼實現業務需求
itstack-demo-design-6-02 經過設計模式優化改造代碼,產生對比性從而學習

3、適配器模式介紹

適配器模式,圖片來自 refactoringguru.cn

適配器模式的主要做用就是把本來不兼容的接口,經過適配修改作到統一。使得用戶方便使用,就像咱們提到的萬能充、數據線、MAC筆記本的轉換頭、出國旅遊買個插座等等,他們都是爲了適配各類不一樣的,作的兼容。。單元測試

萬能充、數據線

除了咱們生活中出現的各類適配的場景,那麼在業務開發中呢?學習

在業務開發中咱們會常常的須要作不一樣接口的兼容,尤爲是中臺服務,中臺須要把各個業務線的各類類型服務作統一包裝,再對外提供接口進行使用。而這在咱們日常的開發中也是很是常見的。

4、案例場景模擬

場景模擬;接收多類型MQ消息

隨着公司的業務的不斷髮展,當基礎的系統逐步成型之後。業務運營就須要開始作用戶的拉新和促活,從而保障DAU的增速以及最終ROI轉換。

而這時候就會須要作一些營銷系統,大部分常見的都是裂變、拉客,例如;你邀請一個用戶開戶、或者邀請一個用戶下單,那麼平臺就會給你返利,多邀多得。同時隨着拉新的量愈來愈多開始設置每個月下單都會給首單獎勵,等等,各類營銷場景。

那麼這個時候作這樣一個系統就會接收各類各樣的MQ消息或者接口,若是一個個的去開發,就會耗費很大的成本,同時對於後期的拓展也有必定的難度。此時就會但願有一個系統能夠配置一下就把外部的MQ接入進行,這些MQ就像上面提到的多是一些註冊開戶消息、商品下單消息等等。

而適配器的思想方式也偏偏能夠運用到這裏,而且我想強調一下,適配器不僅是能夠適配接口每每還能夠適配一些屬性信息。

1. 場景模擬工程

itstack-demo-design-6-00
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                ├── mq
                │   ├── create_account.java
                │   ├── OrderMq.java
                │   └── POPOrderDelivered.java
                └── service
                    ├── OrderServicejava
                    └── POPOrderService.java
  • 這裏模擬了三個不一樣類型的MQ消息,而在消息體中都有一些必要的字段,好比;用戶ID、時間、業務ID,可是每一個MQ的字段屬性並不同。就像用戶ID在不一樣的MQ裏也有不一樣的字段:uId、userId等。
  • 同時還提供了兩個不一樣類型的接口,一個用於查詢內部訂單訂單下單數量,一個用於查詢第三方是否首單。
  • 後面會把這些不一樣類型的MQ和接口作適配兼容。

2. 場景簡述

1.1 註冊開戶MQ

public class create_account {

    private String number;      // 開戶編號
    private String address;     // 開戶地
    private Date accountDate;   // 開戶時間
    private String desc;        // 開戶描述

    // ... get/set     
}

1.2 內部訂單MQ

public class OrderMq {

    private String uid;           // 用戶ID
    private String sku;           // 商品
    private String orderId;       // 訂單ID
    private Date createOrderTime; // 下單時間     

    // ... get/set      
}

1.3 第三方訂單MQ

public class POPOrderDelivered {

    private String uId;     // 用戶ID
    private String orderId; // 訂單號
    private Date orderTime; // 下單時間
    private Date sku;       // 商品
    private Date skuName;   // 商品名稱
    private BigDecimal decimal; // 金額

    // ... get/set      
}

1.4 查詢用戶內部下單數量接口

public class OrderService {

    private Logger logger = LoggerFactory.getLogger(POPOrderService.class);

    public long queryUserOrderCount(String userId){
        logger.info("自營商家,查詢用戶的訂單是否爲首單:{}", userId);
        return 10L;
    }

}

1.5 查詢用戶第三方下單首單接口

public class POPOrderService {

    private Logger logger = LoggerFactory.getLogger(POPOrderService.class);

    public boolean isFirstOrder(String uId) {
        logger.info("POP商家,查詢用戶的訂單是否爲首單:{}", uId);
        return true;
    }

}
  • 以上這幾項就是不一樣的MQ以及不一樣的接口的一個體現,後面咱們將使用這樣的MQ消息和接口,給它們作相應的適配。

5、用一坨坨代碼實現

其實大部分時候接MQ消息都是建立一個類用於消費,經過轉換他的MQ消息屬性給本身的方法。

咱們接下來也是先體現一下這種方式的實現模擬,可是這樣的實現有一個很大的問題就是,當MQ消息愈來愈多後,甚至幾十幾百之後,你做爲中颱要怎麼優化呢?

1. 工程結構

itstack-demo-design-6-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── create_accountMqService.java
                └── OrderMqService.java
                └── POPOrderDeliveredService.java
  • 目前須要接收三個MQ消息,全部就有了三個對應的類,和咱們平時的代碼幾乎同樣。若是你的MQ量很少,這樣的寫法也沒什麼問題,可是隨着數量的增長,就須要考慮用一些設計模式來解決。

2. Mq接收消息實現

public class create_accountMqService {

    public void onMessage(String message) {

        create_account mq = JSON.parseObject(message, create_account.class);

        mq.getNumber();
        mq.getAccountDate();

        // ... 處理本身的業務
    }

}
  • 三組MQ的消息都是同樣模擬使用,就不一一展現了。能夠獲取源碼後學習。

6、適配器模式重構代碼

接下來使用適配器模式來進行代碼優化,也算是一次很小的重構。

適配器模式要解決的主要問題就是多種差別化類型的接口作統一輸出,這在咱們學習工廠方法模式中也有所提到不一樣種類的獎品處理,其實那也是適配器的應用。

在本文中咱們還會再另外體現出一個多種MQ接收,使用MQ的場景。來把不一樣類型的消息作統一的處理,便於減小後續對MQ接收。

在這裏若是你以前沒要開發過接收MQ消息,可能聽上去會有些不理解這樣的場景。對此,我我的建議先了解下MQ。另外就算不了解也不要緊,不會影響對思路的體會。

再者,本文所展現的MQ兼容的核心部分,也就是處理適配不一樣的類型字段。而若是咱們接收MQ後,在配置不一樣的消費類時,若是不但願一個個開發類,那麼可使用代理類的方式進行處理。

1. 工程結構

itstack-demo-design-6-02
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                ├── impl
                │   ├── InsideOrderService.java
                │   └── POPOrderAdapterServiceImpl.java
                ├── MQAdapter,java
                ├── OrderAdapterService,java
                └── RebateInfo,java

適配器模型結構

適配器模型結構

  • 這裏包括了兩個類型的適配;接口適配、MQ適配。之因此不僅是模擬接口適配,由於不少時候你們都很常見了,因此把適配的思想換一下到MQ消息體上,增長你們多設計模式的認知。
  • 先是作MQ適配,接收各類各樣的MQ消息。當業務發展的很快,須要對下單用戶首單纔給獎勵,在這樣的場景下再增長對接口的適配操做。

2. 代碼實現(MQ消息適配)

2.1 統一的MQ消息體

public class RebateInfo {

    private String userId;  // 用戶ID
    private String bizId;   // 業務ID
    private Date bizTime;   // 業務時間
    private String desc;    // 業務描述
    
    // ... get/set
}
  • MQ消息中會有多種多樣的類型屬性,雖然他們都有一樣的值提供給使用方,可是若是都這樣接入那麼當MQ消息特別多時候就會很麻煩。
  • 因此在這個案例中咱們定義了通用的MQ消息體,後續把全部接入進來的消息進行統一的處理。

2.2 MQ消息體適配類

public class MQAdapter {

    public static RebateInfo filter(String strJson, Map<String, String> link) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        return filter(JSON.parseObject(strJson, Map.class), link);
    }

    public static RebateInfo filter(Map obj, Map<String, String> link) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        RebateInfo rebateInfo = new RebateInfo();
        for (String key : link.keySet()) {
            Object val = obj.get(link.get(key));
            RebateInfo.class.getMethod("set" + key.substring(0, 1).toUpperCase() + key.substring(1), String.class).invoke(rebateInfo, val.toString());
        }
        return rebateInfo;
    }

}
  • 這個類裏的方法很是重要,主要用於把不一樣類型MQ種的各類屬性,映射成咱們須要的屬性並返回。就像一個屬性中有用戶ID;uId,映射到咱們須要的;userId,作統一處理。
  • 而在這個處理過程當中須要把映射管理傳遞給Map<String, String> link,也就是準確的描述了,當前MQ中某個屬性名稱,映射爲咱們的某個屬性名稱。
  • 最終由於咱們接收到的mq消息基本都是json格式,能夠轉換爲MAP結構。最後使用反射調用的方式給咱們的類型賦值。

2.3 測試適配類

2.3.1 編寫單元測試類
@Test
public void test_MQAdapter() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
    create_account create_account = new create_account();
    create_account.setNumber("100001");
    create_account.setAddress("河北省.廊坊市.廣陽區.大學裏職業技術學院");
    create_account.setAccountDate(new Date());
    create_account.setDesc("在校開戶");          

    HashMap<String, String> link01 = new HashMap<String, String>();
    link01.put("userId", "number");
    link01.put("bizId", "number");
    link01.put("bizTime", "accountDate");
    link01.put("desc", "desc");
    RebateInfo rebateInfo01 = MQAdapter.filter(create_account.toString(), link01);
    System.out.println("mq.create_account(適配前)" + create_account.toString());
    System.out.println("mq.create_account(適配後)" + JSON.toJSONString(rebateInfo01));

    System.out.println("");

    OrderMq orderMq = new OrderMq();
    orderMq.setUid("100001");
    orderMq.setSku("10928092093111123");
    orderMq.setOrderId("100000890193847111");
    orderMq.setCreateOrderTime(new Date()); 

    HashMap<String, String> link02 = new HashMap<String, String>();
    link02.put("userId", "uid");
    link02.put("bizId", "orderId");
    link02.put("bizTime", "createOrderTime");
    RebateInfo rebateInfo02 = MQAdapter.filter(orderMq.toString(), link02);

    System.out.println("mq.orderMq(適配前)" + orderMq.toString());
    System.out.println("mq.orderMq(適配後)" + JSON.toJSONString(rebateInfo02));
}
  • 在這裏咱們分別模擬傳入了兩個不一樣的MQ消息,並設置字段的映射關係。
  • 等真的業務場景開發中,就能夠配這種映射配置關係交給配置文件或者數據庫後臺配置,減小編碼。
2.3.2 測試結果
mq.create_account(適配前){"accountDate":1591024816000,"address":"河北省.廊坊市.廣陽區.大學裏職業技術學院","desc":"在校開戶","number":"100001"}
mq.create_account(適配後){"bizId":"100001","bizTime":1591077840669,"desc":"在校開戶","userId":"100001"}

mq.orderMq(適配前){"createOrderTime":1591024816000,"orderId":"100000890193847111","sku":"10928092093111123","uid":"100001"}
mq.orderMq(適配後){"bizId":"100000890193847111","bizTime":1591077840669,"userId":"100001"}

Process finished with exit code 0
  • 從上面能夠看到,一樣的字段值在作了適配先後分別有統一的字段屬性,進行處理。這樣業務開發中也就很是簡單了。
  • 另外有一個很是重要的地方,在實際業務開發中,除了反射的使用外,還能夠加入代理類把映射的配置交給它。這樣就能夠不須要每個mq都手動建立類了。

3. 代碼實現(接口使用適配)

就像咱們前面提到隨着業務的發展,營銷活動自己要修改,不能只是接了MQ就發獎勵。由於此時已經拉新的愈來愈多了,須要作一些限制。

由於增長了只有首單用戶纔給獎勵,也就是你一年或者新人或者一個月的第一單纔給你獎勵,而不是你以前每一次下單都給獎勵。

那麼就須要對此種方式進行限制,而此時MQ中並無判斷首單的屬性。只能經過接口進行查詢,而拿到的接口以下;

接口 描述
org.itstack.demo.design.service.OrderService.queryUserOrderCount(String userId) 出參long,查詢訂單數量
org.itstack.demo.design.service.OrderService.POPOrderService.isFirstOrder(String uId) 出參boolean,判斷是否首單
  • 兩個接口的判斷邏輯和使用方式都不一樣,不一樣的接口提供方,也有不一樣的出參。一個是直接判斷是否首單,另一個須要根據訂單數量判斷。
  • 所以這裏須要使用到適配器的模式來實現,固然若是你去編寫if語句也是能夠實現的,可是咱們常常會提到這樣的代碼很難維護。

3.1 定義統一適配接口

public interface OrderAdapterService {

    boolean isFirst(String uId);

}
  • 後面的實現類都須要完成此接口,並把具體的邏輯包裝到指定的類中,知足單一職責。

3.2 分別實現兩個不一樣的接口

內部商品接口

public class InsideOrderService implements OrderAdapterService {

    private OrderService orderService = new OrderService();

    public boolean isFirst(String uId) {
        return orderService.queryUserOrderCount(uId) <= 1;
    }

}

第三方商品接口

public class POPOrderAdapterServiceImpl implements OrderAdapterService {

    private POPOrderService popOrderService = new POPOrderService();

    public boolean isFirst(String uId) {
        return popOrderService.isFirstOrder(uId);
    }

}
  • 在這兩個接口中都實現了各自的判斷方式,尤爲像是提供訂單數量的接口,須要本身判斷當前接到mq時訂單數量是否<= 1,以此判斷是否爲首單。

3.3 測試適配類

3.3.1 編寫單元測試類
@Test
public void test_itfAdapter() {
    OrderAdapterService popOrderAdapterService = new POPOrderAdapterServiceImpl();
    System.out.println("判斷首單,接口適配(POP):" + popOrderAdapterService.isFirst("100001"));   

    OrderAdapterService insideOrderService = new InsideOrderService();
    System.out.println("判斷首單,接口適配(自營):" + insideOrderService.isFirst("100001"));
}
3.3.2 測試結果
23:25:47.076 [main] INFO  o.i.d.design.service.POPOrderService - POP商家,查詢用戶的訂單是否爲首單:100001
判斷首單,接口適配(POP):true
23:25:47.079 [main] INFO  o.i.d.design.service.POPOrderService - 自營商家,查詢用戶的訂單是否爲首單:100001
判斷首單,接口適配(自營):false

Process finished with exit code 0
  • 從測試結果上來看,此時已經的接口已經作了統一的包裝,外部使用時候就不須要關心內部的具體邏輯了。並且在調用的時候只須要傳入統一的參數便可,這樣就知足了適配的做用。

7、總結

  • 從上文能夠看到不使用適配器模式這些功能一樣能夠實現,可是使用了適配器模式就可讓代碼:乾淨整潔易於維護、減小大量重複的判斷和使用、讓代碼更加易於維護和拓展。
  • 尤爲是咱們對MQ這樣的多種消息體中不一樣屬性同類的值,進行適配再加上代理類,就可使用簡單的配置方式接入對方提供的MQ消息,而不須要大量重複的開發。很是利於拓展。
  • 設計模式的學習學習過程可能會在一些章節中涉及到其餘設計模式的體現,只不過不會重點講解,避免喧賓奪主。但在實際的使用中,每每不少設計模式是綜合使用的,並不會單一出現。

8、推薦閱讀

相關文章
相關標籤/搜索