iT邦幫忙

2021 iThome 鐵人賽

DAY 11
0
Software Development

也該是時候學學 Design Pattern 了系列 第 11

Day 11: Structural patterns - Adapter

目的

如何在不修改物件的情況上,使用中間層(Adapter)後,能轉換跟其他物件聯繫,同時原有功能不受影響。

說明

這模式在現實最典型的應用是充電器:使用 110v 的電子設備,在國外 220v 的環境下,需要使用「變壓器」讓「充電」這個目的,仍能實踐。

應用在軟體開發上,可能有這些應用:

  • 系統要連線到不同種類的資料庫,建立一個中間層,負責與新資料庫的溝通。
  • 老舊系統只接受 XML,新系統只提供 JSON,需要中間層負責轉換。

簡而言之,就是建立一個「轉換器」,讓系統維持運作。

在實作前,要注意的是判斷環境中,目標物件本身是單一物件還是繼承虛擬層的物件?而作法則是:

  1. 注意目標物件本身是不是有繼承虛擬層?還是單一物件?
    1. 如果有繼承虛擬層,則建立一個「繼承虛擬層」的轉接器(Adapter),能提供與目標物件一樣的方法。
    2. 如果是單一物件,則建立一個「繼承目標物件」的轉接器(Adapter),能提供與目標物件一樣的方法。
  2. 轉接器(Adapter)在個方法的實作上,會呼叫其他物件(Adaptee)的方法,達到使用其他物件的初衷。
  3. 當環境需要使用 Adaptee 時,使用 Adapter 即可。

以下 UML 圖與範例程式碼,採用「目標物件本身繼承虛擬層」。

UML 圖

Adapter Pattern UML Diagram

使用 Java 實作

親代、虛擬層:Car

public abstract class Car {
    protected String name;

    protected Car(String name) {
        this.name = name;
    }

    public void turnOn() {
        System.out.println("發動車子,名稱是:" + name);
    }

    public void turnOff() {
        System.out.println("車子熄火");
    }

    public abstract void turnRight();

    public abstract void turnLeft();

    public void speedUp() {
        System.out.println("腳踩油門");
    }

    public void brake() {
        System.out.println("腳踩煞車");
    }
}

子代:LeftHandCarRightHandCar(Adaptee)

public class LeftHandCar extends Car {
    public LeftHandCar(String name) {
        super(name);
    }

    @Override
    public void turnRight() {
        System.out.println("打右轉燈");
        System.out.println("車輛靠右");
        System.out.println("確認沒有行人");
        System.out.println("確認沒有機車");
        System.out.println("車輛右轉");
    }

    @Override
    public void turnLeft() {
        System.out.println("打左轉燈");
        System.out.println("車輛向左靠近分隔島");
        System.out.println("車輛向前靠近左轉區塊");
        System.out.println("等待左轉燈亮起");
        System.out.println("左轉燈亮起");
        System.out.println("車輛左轉");
    }
}

public class RightHandCar extends Car {
    public RightHandCar(String name) {
        super(name);
    }

    @Override
    public void turnRight() {
        System.out.println("打右轉燈");
        System.out.println("車輛向右靠近分隔島");
        System.out.println("車輛向前靠近右轉區塊");
        System.out.println("等待右轉燈亮起");
        System.out.println("右轉燈亮起");
        System.out.println("車輛右轉");
    }

    @Override
    public void turnLeft() {
        System.out.println("打左轉燈");
        System.out.println("車輛靠左");
        System.out.println("確認沒有行人");
        System.out.println("確認沒有機車");
        System.out.println("車輛左轉");
    }
}

右駕車適應在左駕環境:RightHandInLeftHandTraffic(Adapter)

public class RightHandInLeftHandTraffic extends RightHandCar {
    public RightHandInLeftHandTraffic(String name) {
        super(name);
    }

    @Override
    public void turnRight() {
        System.out.println("打右轉燈");
        System.out.println("車輛靠右");
        System.out.println("確認沒有行人");
        System.out.println("確認沒有機車");
        System.out.println("車輛右轉");
    }

    @Override
    public void turnLeft() {
        System.out.println("打左轉燈");
        System.out.println("車輛向左靠近分隔島");
        System.out.println("車輛向前靠近左轉區塊");
        System.out.println("等待左轉燈亮起");
        System.out.println("左轉燈亮起");
        System.out.println("車輛左轉");
    }
}

在左駕的環境上路:LeftHandTraffic

public class LeftHandTraffic {
    public static void main(String[] args) {
        System.out.println("---國產車上路囉---");
        Car domesticCar = new LeftHandCar("Altis");
        domesticCar.turnOn();
        domesticCar.speedUp();
        domesticCar.brake();
        domesticCar.turnRight();
        domesticCar.speedUp();
        domesticCar.brake();
        domesticCar.turnLeft();
        domesticCar.turnOff();

        System.out.println("\n---完美,下車換日本進口車---");
        Car madeInJapanCar = new RightHandInLeftHandTraffic("CT 200h");
        madeInJapanCar.turnOn();
        madeInJapanCar.speedUp();
        madeInJapanCar.brake();
        madeInJapanCar.turnRight();
        madeInJapanCar.speedUp();
        madeInJapanCar.brake();
        madeInJapanCar.turnLeft();
        madeInJapanCar.turnOff();
        System.out.println("\n---測試完成,左駕右駕都很棒---");
    }
}

使用 JavaScript 實作

親代、虛擬層:Car

/** @abstract */
class Car {
  constructor(name) {
    this.name = name;
  }

  turnOn() {
    console.log("發動車子,名稱是:" + this.name);
  }

  turnOff() {
    console.log("車子熄火");
  }

  /** @abstract */
  turnRight() { return; }

  /** @abstract */
  turnLeft() { return; }

  speedUp() {
    console.log("腳踩油門");
  }

  brake() {
    console.log("腳踩煞車");
  }
}

子代:LeftHandCarRightHandCar(Adaptee)

class LeftHandCar extends Car {
  constructor(name) {
    super(name);
  }

  /** @override */
  turnRight() {
    console.log("打右轉燈");
    console.log("車輛靠右");
    console.log("確認沒有行人");
    console.log("確認沒有機車");
    console.log("車輛右轉");
  }

  /** @override */
  turnLeft() {
    console.log("打左轉燈");
    console.log("車輛向左靠近分隔島");
    console.log("車輛向前靠近左轉區塊");
    console.log("等待左轉燈亮起");
    console.log("左轉燈亮起");
    console.log("車輛左轉");
  }
}

class RightHandCar extends Car {
  constructor(name) {
    super(name);
  }

  /** @override */
  turnRight() {
    console.log("打右轉燈");
    console.log("車輛向右靠近分隔島");
    console.log("車輛向前靠近右轉區塊");
    console.log("等待右轉燈亮起");
    console.log("右轉燈亮起");
    console.log("車輛右轉");
  }

  /** @override */
  turnLeft() {
    console.log("打左轉燈");
    console.log("車輛靠左");
    console.log("確認沒有行人");
    console.log("確認沒有機車");
    console.log("車輛左轉");
  }
}

右駕車適應在左駕環境:RightHandInLeftHandTraffic(Adapter)

class RightHandInLeftHandTraffic extends RightHandCar {
  constructor(name) {
    super(name);
  }

  /** @override */
  turnRight() {
    console.log("打右轉燈");
    console.log("車輛靠右");
    console.log("確認沒有行人");
    console.log("確認沒有機車");
    console.log("車輛右轉");
  }

  /** @override */
  turnLeft() {
    console.log("打左轉燈");
    console.log("車輛向左靠近分隔島");
    console.log("車輛向前靠近左轉區塊");
    console.log("等待左轉燈亮起");
    console.log("左轉燈亮起");
    console.log("車輛左轉");
  }
}

在左駕的環境上路:LeftHandTraffic

const leftHandTraffic = () => {
  console.log("---國產車上路囉---");
  const domesticCar = new LeftHandCar("Altis");
  domesticCar.turnOn();
  domesticCar.speedUp();
  domesticCar.brake();
  domesticCar.turnRight();
  domesticCar.speedUp();
  domesticCar.brake();
  domesticCar.turnLeft();
  domesticCar.turnOff();

  console.log("\n---完美,下車換日本進口車---");
  const madeInJapanCar = new RightHandInLeftHandTraffic("CT 200h");
  madeInJapanCar.turnOn();
  madeInJapanCar.speedUp();
  madeInJapanCar.brake();
  madeInJapanCar.turnRight();
  madeInJapanCar.speedUp();
  madeInJapanCar.brake();
  madeInJapanCar.turnLeft();
  madeInJapanCar.turnOff();
  console.log("\n---測試完成,左駕右駕都很棒---");
}

leftHandTraffic();

總結

Adapter 是非常容易理解的模式,理解是「專接器」後幾乎懂含義。

倒是有幾點要思考:

  • 如果一開始架構設計好使用多種不同類型的服務,那使用 Adapter 模式的機率就不高。換句話說,Adapter 模式較常運用在突發狀況,在沒辦法重構的情況下,暫時處理機制。
  • 如果要作為第三方,專責支援其他環境的話,那在設計上採用 Adaptee 並建立許多 Adapter 是合理的做法。

除了今天介紹的應用,C++ 允許多重繼承,所以 Adapter 有另一種寫法,但想想這不是 JavaJavaScript 能實作的,便不深究了。

明天將介紹 Structural patterns 的第二個模式:Bridge 模式。


上一篇
Day 10: Creational patterns - Singleton
下一篇
Day 12: Structural patterns - Bridge
系列文
也該是時候學學 Design Pattern 了31

尚未有邦友留言

立即登入留言