介面恐怕會傷到皇城之內的和氣

介面恐怕會傷到皇城之內的和氣

Adapter Pattern

介面不相容時,Adapter Pattern 能轉接介面,使不相容的類別能一起工作。

Adapter Pattern 用來「彌補」設計上的缺陷,以避免改動原有設計,實屬無奈之舉。

Adapter Pattern 實作

有二種實作方式,一是利用繼承的「類別轉接器」,二是利用組合的「物件轉接器」

類別轉接器

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 ITraget {
    public void f1() { 
        super.fa();
    }

    public void f2() {
        super.fb();
    }

    // fc 繼承 Adaptee,不需要實作
}

類別轉接器的優點是,可以繼承被轉接者,無需重新實作介面每個方法

物件轉接器

public interface ITarget {
    /* same */
}

public class Adaptee {
    /* same */
}

public class Adaptor implements ITraget {
    private Adaptee adaptee;
    public Adaptor(Adaptee adaptee) {
        this.adaptee = adaptee;
    }

    public void f1() { 
        adaptee.fa();
    }

    public void f2() {
        adaptee.fb();
    }

    public void fc() {
        adaptee.fc();
    }
}

物件轉接器的優點是,組合更有彈性,轉接器也能轉接 Adaptee 的子類別

該選哪一種?

  • Adaptee 與 ITarget 介面類似,選類別轉接器能提高複用性,減少程式碼量
  • Adaptee 與 ITarget 介面大部分不同,選擇較有彈性的物件轉接器

Adapter Pattern 應用

1. 封裝有缺陷的介面

系統必須引入有許多問題的外部 SDK,例如靜態方法造成可測試性差、命名糟糕、過多參數、效能不足等,但又無法修改其程式碼

public class SDK {
    public static void staticFunction1() { /*...*/ }

    public static void uglyNamingFunction2() { /*...*/ }

    public static void tooManyParamsFunction3(int a, int b, ...) { /*...*/ }

    public static void lowPerformanceFunction4() { /*...*/ }
}

使用 Adapter Pattern 重構

public ITarget {
    void function1();
    void function2();
    void function3(ParamsWrapper paramWrapper);
    void function4();
}

public SDKAdaptor extends SDK implements Itarget {
    public void function1() {
        super.staticFunction1();
    }

    public void function2() {
        super.uglyNamingFunction2();
    }

    public void function3(ParamsWrapper paramWrapper) {
        super.tooManyParamsFunction3(paramWrapper.getParamA(), ...)
    }

    public void function4() {
        // reimplement
    }
}

2. 統一多個類別的介面

某個功能依賴多個外部系統的類別,將他們的介面統一,可以利用多型提高程式碼重複使用性

// 有多個過濾內容中敏感文字的類別
public class ASesitiveWordsFilter {
    public String filterSexyWords(String text) { /*...*/ }

    public String filterCurseWords(String text) { /*...*/ }
}

public class BSesitiveWordsFilter {
    public String filter(String text) { /*...*/ }
}

public class CSesitiveWordsFilter {
    public String filter(String text, String mask) { /*...*/ }
}

client 擴展性差、可測試性低

public class RiskManagement {
    private ASesitiveWordsFilter aFilter = new ASesitiveWordsFilter();
    private BSesitiveWordsFilter bFilter = new BSesitiveWordsFilter();
    private CSesitiveWordsFilter cFilter = new CSesitiveWordsFilter();

    public String filterSensitiveWord(String text) {
        String maskedText = aFilter.filterSexyWords(text);
        maskedText = aFilter.filterCurseWords(maskedText);
        maskedText = bFilter.filter(maskedText);
        maskedText = cFilter.filter(maskedText, "***");
        return maskedText
    }
}

使用 Adapter Pattern 重構

public interface ISensitiveWordsFilter {
    String filter(String text);
}

public ASensitiveWordsFilterAdaptor implements ISensitiveWordsFilter {
    private ASesitiveWordsFilter filter;

    public ASensitiveWordsFilterAdaptor(ASesitiveWordsFilter filter) {
        this.filter = filter;
    }

    public String filter(String text) {
        String maskedText = aFilter.filterSexyWords(text);
        maskedText = aFilter.filterCurseWords(maskedText);
        return maskedText;
    }
}

// 省略 BSesitiveWordsFilterAdaptor, CSesitiveWordsFilterAdaptor

public class RiskManagement {
    private List<ISensitiveWordsFilter> filters = new ArrayList<>();

    public void addSensitiveWordsFilter(ISensitiveWordsFilter filter) {
        filters.add(filter);
    }

    public String filterSensitiveWord(String text) {
        String maskedText = text;
        for (ISensitiveWordsFilter filter : filters) {
            maskedText = filter.filter(maskedText);
        }
        return maskedText;
    }
}

3. 替換依賴

原本依賴外部系統 A,要改成依賴外部系統 B,利用 Adapter Pattern 能減少改動

// 外部系統 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 demo = new Demo(new A());

利用 BAdaptor,Demo 類別依賴 IA 介面的地方無須修改

// 將系統 A 替換成系統 B
public class BAdaptor implemnts IA {
    private B b;

    public BAdaptor(B b) {
        this.b= b;
    }

    public void fa() {
        b.fb();
    }
}

Demo demo = new Demo(new BAdaptor(new B()));

4. 相容舊版介面

在升級版本時,有些要棄用的介面,可以有個過渡期,暫時保留舊版本介面

JDK 1.0 有個集合容器類別 Enumeration,在 JDK 2.0 進行了重構,改名為 Iteraotr;升級後為了相容舊版 JDK 開發的程式,暫時保留了 Enumeration 類別

public class Collections {
    public static Enmueration enumeration(final Collection c) {
        return new Enumeration() {
            Iterator i = c.iterator();

            public boolean hasMoreElements() {
                return i.hasNext();
            }

            public Object nextElement() {
                return i.next();
            }
        }
    }
}

實際應用

Slf4j,是一個 Java 的日誌框架,針對不同日誌框架進行封裝,提供統一介面;系統內只需要針對 Slf4j 介面來開發使用日誌的程式碼,具體使用哪種或多種日誌框架(log4j、logback...) 只要將對應的 SDK 導入專案,而無須修改原本程式