1. 程式人生 > 實用技巧 >洛谷 CF865D Buy Low Sell High(反悔貪心)

洛谷 CF865D Buy Low Sell High(反悔貪心)

介面卡模式

案例

一天,張三把自己的iPhone手機玩的沒電了想找充電器,但是完了帶資料線。而李四有資料線,只不過是TypeC介面的,張三用不了,就不能繼續愉快的玩耍了。下面我們來模擬出描述的內容:

1.首先定義了三個實體類:

TyecC資料線介面類:

/**
 * TypeC介面
 */
public class TypeC {
    private String username;

    public TypeC(String username) {
        this.username = username;
        System.out.println("我是" + this.username + "的Type-C資料線介面");
    }

    public void typeC() {
        System.out.println(this.username + "的資料線提供Type-C介面進行充電~~");
    }
}

Lightning資料線介面類:

/**
 * Lightning介面
 */
public class Lightning {
    private String username;

    public Lightning() {
    }

    public Lightning(String username) {
        this.username = username;
        System.out.println("我是" + this.username + "的Lighting資料線介面");
    }

    public void lightning() {
        System.out.println(this.username + "的資料線提供Lightning介面進行充電~~");
    }
}

手機類:

/**
 * 手機類
 */
public class IPhone {
    private String username;

    public IPhone(String username) {
        this.username = username;
        System.out.println("我是" + this.username + "的iPhone");
    }

    public void charge(Lightning lightning) {
        System.out.println(this.username + "的iPhone需要Lightning介面進行充電~~");
        if (lightning == null) {
            System.out.println("沒帶資料線,不能充電");
            return;
        }
        lightning.lightning();
    }
}

2.程式碼模擬出整個過程:

/**
 * 模擬手機充電過程
 */
public class Main {
    public static void main(String[] args) {
        String zs = "張三";
        String ls = "李四";
        IPhone iPhone = new IPhone(zs);
        System.out.println("我沒電了");
        System.out.println();

        System.out.println(zs + "說:‘手機沒電了,找資料線充電’");
        System.out.println();

        Lightning lightning = new Lightning(zs);
        lightning.lightning();
        System.out.println("今天" + zs + "忘了帶上我>_<");
        lightning = null;
        iPhone.charge(lightning);
        System.out.println();

        System.out.println(ls + "說:‘我有資料線’");
        TypeC typeC = new TypeC(ls);
        typeC.typeC();
        System.out.println();

        System.out.println(zs + "的iPhone不能使用" + ls + "的資料線,不能繼續玩手機了╯﹏╰");
    }
}

3.程式碼執行結果:

我是張三的iPhone
我沒電了

張三說:‘手機沒電了,找資料線充電’

我是張三的Lighting資料線介面
張三的資料線提供Lightning介面進行充電~~
今天張三忘了帶上我>_<
張三的iPhone需要Lightning介面進行充電~~
沒帶資料線,不能充電

李四說:‘我有資料線’
我是李四的Type-C資料線介面
李四的資料線提供Type-C介面進行充電~~

張三的iPhone不能使用李四的資料線,不能繼續玩手機了╯﹏╰

上述過程在生活中可能並不少見,一些人出門玩,手機沒電了卻沒帶充電器。向別人尋求幫助時,可能因為自己的手機與別人提供的資料線不匹配而充不了電,最後導致手機不能用了。這種時候我們如果實在找不到與手機匹配的資料線介面,還有種解決方法,那就是我們再找一個轉接頭就可以用別人的資料線介面進行充電了。這與我們設計模式中的介面卡模式非常相似,下面就介紹一下介面卡模式。

模式介紹

在計算機程式設計中,介面卡模式-結構型模式(有時候也稱包裝樣式或者包裝)將一個類的介面適配成使用者所期待的。一個適配允許通常因為介面不相容而不能在一起工作的類工作在一起,做法是將類自己的介面包裹在一個已存在的類中。

注意:在介面卡模式定義中所提及的介面是指廣義的介面,它可以表示一個方法或者方法的集合。

角色構成:

  • Target(目標抽象類):目標抽象類定義客戶所需介面,可以是一個抽象類或介面,也可以是具體類。
  • Adapter(介面卡類):介面卡可以呼叫另一個介面,作為一個轉換器,對Adaptee和Target進行適配,介面卡類是介面卡模式的核心,在物件介面卡中,它通過繼承Target並關聯一個Adaptee物件使二者產生聯絡。
  • Adaptee(適配者類):適配者即被適配的角色,它定義了一個已經存在的介面,這個介面需要適配,適配者類一般是一個具體類,包含了客戶希望使用的業務方法,在某些情況下可能沒有適配者類的原始碼。

在介面卡模式中,我們通過增加一個新的介面卡類來解決介面不相容的問題,使得原本沒有任何關係的類可以協同工作。根據介面卡類與適配者類的關係不同,介面卡模式可分為物件介面卡類介面卡兩種,在物件介面卡模式中,介面卡與適配者之間是關聯關係;在類介面卡模式中,介面卡與適配者之間是繼承(或實現)關係。兩種形式最大的區別在於介面卡和適配者之間的關係不同,因此它們的UML類圖略有不同。

UML類圖:

1.物件介面卡:

2.類介面卡:

與轉接頭相似,在介面卡模式中引入了一個被稱為介面卡(Adapter)的包裝類,而它所包裝的物件稱為適配者(Adaptee),即被適配的類。介面卡的實現就是把客戶類的請求轉化為對適配者的相應介面的呼叫。也就是說:當客戶類呼叫介面卡的方法時,在介面卡類的內部將呼叫適配者類的方法,而這個過程對客戶類是透明的,客戶類並不直接訪問適配者類。因此,介面卡讓那些由於介面不相容而不能互動的類可以一起工作。

程式碼改造

接下來我們就按照物件介面卡形式的對手機充電這一過程進行改造。

1.首先依舊是上面的三個實體類:

程式碼與上面的一樣,這裡就不重複貼上了。其中Lightning類是Target目標類角色,TypeC類是Adaptee適配者類。

2.引入Adapter介面卡類:

/**
 * 介面轉接頭:介面卡角色
 */
public class Adapter extends Lightning {
    private TypeC typeC;

    public Adapter(String username, TypeC typeC) {
        this.typeC = typeC;
        System.out.println("我是" + username + "的轉接頭");
    }

    @Override
    public void lightning() {
        System.out.println("我的一頭適配了" + typeC.getUsername() + "的資料線");
        typeC.typeC();
        System.out.println("我的另一頭提供Lightning介面連線手機進行充電~~");
    }
}

3.模擬整個過程:

public class Main {
    public static void main(String[] args) {
        String zs = "張三";
        String ls = "李四";
        IPhone iPhone = new IPhone(zs);
        System.out.println("我沒電了");
        System.out.println();

        System.out.println(zs + "說:‘手機沒電了,找資料線充電’");
        System.out.println();

        Lightning lightning = new Lightning(zs);
        lightning.lightning();
        System.out.println("今天" + zs + "忘了帶上我>_<");
        lightning = null;
        iPhone.charge(lightning);
        System.out.println();

        System.out.println(ls + "說:‘我有資料線’");
        TypeC typeC = new TypeC(ls);
        typeC.typeC();
        System.out.println(zs + "的iPhone不能使用" + ls + "的資料線進行充電");
        System.out.println();

        String ww = "王五";
        System.out.println(ww + "說:‘我有轉接頭’");
        Adapter adapter = new Adapter(ww, typeC);

        iPhone.charge(adapter);
        System.out.println(zs + "的手機充完電又可以繼續玩了Y(^_^)Y");
    }
}

4.執行結果:

我是張三的iPhone
我沒電了

張三說:‘手機沒電了,找資料線充電’

我是張三的Lighting資料線介面
張三的資料線提供Lightning介面進行充電~~
今天張三忘了帶上我>_<
張三的iPhone需要Lightning介面進行充電~~
沒帶資料線,不能充電

李四說:‘我有資料線’
我是李四的Type-C資料線介面
李四的資料線提供Type-C介面進行充電~~
張三的iPhone不能使用李四的資料線進行充電

王五說:‘我有轉接頭’
我是王五的轉接頭
張三的iPhone需要Lightning介面進行充電~~
我的一頭適配了李四的資料線
李四的資料線提供Type-C介面進行充電~~
我的另一頭提供Lightning介面連線手機進行充電~~
張三的手機充完電又可以繼續玩了Y(^_^)Y

可以看到我們引入了Adapter介面卡類後,張三就可以用王五的轉接頭,再配合李四的TypeC資料線介面進行充電了。

模式應用

介面卡模式將現有介面轉化為客戶類所期望的介面,實現對現有類的複用,它是一種使用頻率非常高的設計模式,在軟體開發中得以廣泛應用。其中 Mybatis 框架的日誌模組中就是使用了介面卡模式。下面引入了 Mybatis 框架及其日誌模組的程式碼來分析介面卡模式的應用。

1.pom 依賴:

<dependencies>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.4.6</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.30</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.3</version>
    </dependency>
</dependencies>

2.目標介面Log--Mybatis 內部使用日誌時的統一介面:

/**
 * @author Clinton Begin
 */
public interface Log {

  boolean isDebugEnabled();

  boolean isTraceEnabled();

  void error(String s, Throwable e);

  void error(String s);

  void debug(String s);

  void trace(String s);

  void warn(String s);

}

3.介面卡類Slf4jLoggerImpl

/**
 * @author Eduardo Macarron
 */
class Slf4jLoggerImpl implements Log {

  private final Logger log;

  public Slf4jLoggerImpl(Logger logger) {
    log = logger;
  }

  @Override
  public boolean isDebugEnabled() {
    return log.isDebugEnabled();
  }
  // 省略其他重寫的介面

}

4.適配者角色是 slf4j 中的介面org.slf4j.Logger類,這是一個介面,它有很多的實現,這裡引入的是 logback 中的實現ch.qos.logback.classic.Logger類。

下面是以上幾個類之間的關係圖:

分析:

Java開發中經常用到的日誌框架有很多,Log4j、Log4j2、slf4j等等,Mybatis定義了一套統一的日誌介面供上層使用,併為上述常用的日誌框架提供了相應的介面卡 在Mybatis的日誌模組中就是使用了介面卡模式。

Mybatis內部在使用日誌模組時,使用了其內部介面 org.apache.ibatis.logging.Log,但是常用的日誌框架的對外介面各不相同,Mybatis為了複用和整合這些第三方日誌元件,在其日誌模組中,提供了多種Adapter,將這些第三方日誌元件對外介面適配成org.apache.ibatis.logging.Log,這樣Myabtis 就可以通過Log介面呼叫第三方日誌了。

總結

1.主要優點

​ 無論是物件介面卡模式還是類介面卡模式都具有如下優點:

  • 將目標類和適配者類解耦,通過引入一個介面卡類來重用現有的適配者類,無須修改原有結構。
  • 增加了類的透明性和複用性,將具體的業務實現過程封裝在適配者類中,對於客戶端類而言是透明的,而且提高了適配者的複用性,同一個適配者類可以在多個不同的系統中複用。
  • 靈活性和擴充套件性都非常好,通過使用配置檔案,可以很方便地更換介面卡,也可以在不修改原有程式碼的基礎上增加新的介面卡類,完全符合“開閉原則”。

​ 具體來說,類介面卡模式還有如下優點:

  • 由於介面卡類是適配者類的子類,因此可以在介面卡類中置換一些適配者的方法,使得介面卡的靈活性更強。

​ 物件介面卡模式還有如下優點:

  • 一個物件介面卡可以把多個不同的適配者適配到同一個目標;

  • 可以適配一個適配者的子類,由於介面卡和適配者之間是關聯關係,根據“里氏代換原則”,適配者的子類也可通過該介面卡進行適配。

1.主要缺點

​ 類介面卡模式的缺點如下:

  • 對於Java、C#等不支援多重類繼承的語言,一次最多隻能適配一個適配者類,不能同時適配多個適配者;
  • 適配者類不能為最終類,如在Java中不能為final類,C#中不能為sealed類;
  • 在Java、C#等語言中,類介面卡模式中的目標抽象類只能為介面,不能為類,其使用有一定的侷限性。

​ 物件介面卡模式的缺點如下:

  • 與類介面卡模式相比,要在介面卡中置換適配者類的某些方法比較麻煩。如果一定要置換掉適配者類的一個或多個方法,可以先做一個適配者類的子類,將適配者類的方法置換掉,然後再把適配者類的子類當做真正的適配者進行適配,實現過程較為複雜。

1.適用場景

  • 系統需要使用一些現有的類,而這些類的介面(如方法名)不符合系統的需要,甚至沒有這些類的原始碼。
  • 想建立一個可以重複使用的類,用於與一些彼此之間沒有太大關聯的一些類,包括一些可能在將來引進的類一起工作。

參考資料

本篇文章github程式碼地址:https://github.com/Phoegel/design-pattern/tree/main/adapter
轉載請說明出處,本篇部落格地址:https://www.cnblogs.com/phoegel/p/13933499.html