多系統對接的適配與包裝模式應用

2022-06-13 12:01:53

 

日常開發系統中通常需要對接多個系統,需要用到介面卡模式。

例如:支付方式就涉及多個系統對接。 

國際慣例,先引入概念。

 

介面卡模式:

 

提到介面卡自然就能想到手機用的電源介面卡。

他的作用就是將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

 

 其實裝飾模式和介面卡模式很像。都是包裝一個物件,然後利用這個物件的功能搞出點對外提供的方法。

裝飾模式

介面卡模式

外觀模式

目的:不改變介面,加入責任

目的:將一個介面轉成另一個介面

目的:讓介面跟簡單

裝飾者模式可以讓新的行為和責任要加入到設計中。而無需修改現有的程式碼。
即:方法還是原方法,但是在原方法前後加點東西。

 可以整合若干類來提供客戶需要的介面。
即將一個不相容的介面物件包裝起來,變成相容的物件。

 

當一個方法呼叫被委託給裝飾者的時候,
不知道有多少其他裝飾者已經處理過這個呼叫了。

裝飾者有點像遞迴套娃,你不知道當前是第幾層。

可以使用新的庫和子集合,而無需改變任何程式碼。介面卡會按照原介面給你包好。

 

拓展包裝物件的行為和責任,不是簡單的傳送

一定進行介面的裝換。

 

 

範例:多支付系統解決方案

通過加入介面卡模式,訂單 Service 在進行支付時呼叫的不再是外部的支付介面,而是「支付方式」介面,與外部系統解耦。

只要保證「支付方式」介面是穩定的,那麼訂單 Service 就是穩定的。比如:

當支付寶支付介面發生變更時,影響的只限於支付寶 Adapter;

當微信支付介面發生變更時,影響的只限於微信支付 Adapter;

當要增加一個新的支付方式時,只需要再寫一個新的 Adapter。

日後不論哪種變更,要修改的程式碼範圍縮小了,維護成本自然降低了,程式碼質量就提高了。

 

問題:

在劃分微服務過程中,經常糾結,對外的功能有沒有必要抽象出一個代理服務。專門負責與某廠商對接。

根據介面卡的理念,如果這個代理服務如果能抽象出標準的介面,那他就有獨立的必要。核心業務服務只和代理服務互動,代理服務遮蔽外部廠商的不同。

日後廠商有變動,我們修改範圍控制在代理服務內,不會涉及到核心業務服務。

 

例如:我們一般會開發一個統一支付的服務,這個服務對接各個支付系統,對內提供標準支付介面,業務服務只和統一支付互動。

同時統一支付還能幫助我們處理對賬、過期自動退款等功能。讓業務系統穩定,輕便。

 

本文來自部落格園,作者:wanglifeng,轉載請註明原文連結: https://www.cnblogs.com/wanglifeng717/p/16348529.html