デザインパターンと言えば TECHSCORE さんの記事が有名ですね。時々「このコード、何かのパターンで表現できそう...」と思うのですが、日常業務でデザインパターンをサッとかっこよく使う事ができていません。という訳で TECHSCORE さんの記事を読みつつ、自分の中で噛み砕いた知識を TypeScript のコードで書き起こしてみようと思います。
- 対象読者:Java のコードを読むのに苦戦する人(自分!!)
- 環境:typescript 4.0
23 パターン全部まとめたら記事同士のリンクを貼ろうと思います。
Adapterパターン
サードパーティの API を使用した時、そのバージョンによるインターフェースの差分を吸収する例です。Adaptor パターンには 委譲 と 継承 の 2 つの構文があります。
委譲
/*
Target
*/
interface ThirdPartyApi {
refreshToken(token: string): string;
}
/*
Adapter(委譲)
*/
class ThirdPartyApiImpl implements ThirdPartyApi {
private api = new ApiForV1();
refreshToken(token: string): string {
// 変数として持たせた別クラスに処理を委譲
return this.api.refreshToken(token);
}
}
/*
Adaptee
*/
class ApiForV1 implements ThirdPartyApi {
sdk = new SdkForV1();
refreshToken(token: string): string {
return this.sdk.getTokenInfo(token).refresh().token;
}
}
class ApiForV2 implements ThirdPartyApi {
sdk = new SdkForV2();
refreshToken(token: string): string {
return this.sdk.login(token).generateNewToken();
}
}
継承
/*
Target
*/
interface ThirdPartyApi {
refreshToken(token: string): string;
}
/*
Adapter(継承)
*/
class ThirdPartyApiImpl extends ApiForV1 implements ThirdPartyApi {
refreshToken(token: string): string {
// 継承元のクラスのメソッドを呼び出す
return super.refreshToken(token);
}
}
/*
Adaptee
*/
class ApiForV1 implements ThirdPartyApi {
sdk = new SdkForV1();
refreshToken(token: string): string {
return this.sdk.getTokenInfo(token).refresh().token;
}
}
class ApiForV2 implements ThirdPartyApi {
sdk = new SdkForV2();
refreshToken(token: string): string {
return this.sdk.login(token).generateNewToken();
}
}
WebAPI は認証トークンをリクエストに含めるものとして、SDK は認証トークンを更新するメソッドを備えています。
説明
Target
アプリから利用する SDK の機能をインターフェースとして切り出します。
/*
Target
*/
interface ThirdPartyApi {
refreshToken(token: string): string;
}
Adaptee
インターフェースの振る舞いを実装したクラスです。SDK のバージョンアップによりオブジェクトやメソッドのインターフェースがガラリと変わるという事があるため、バージョンごとにクラスを分けています。
/*
Adaptee
*/
class ApiForV1 implements ThirdPartyApi {
sdk = new SdkForV1();
refreshToken(token: string): string {
return this.sdk.getTokenInfo(token).refresh().token;
}
}
class ApiForV2 implements ThirdPartyApi {
sdk = new SdkForV2();
refreshToken(token: string): string {
return this.sdk.login(token).generateNewToken();
}
}
Adatpter
Adaptee と同じくインターフェースの振る舞いを実装したクラスですが、こちらはアプリから利用するためのものです。このクラスが V1
V2
どちらの API を使うのかを知っていて、委譲または継承により機能を呼び出します。
/*
Adapter(委譲)
*/
class ThirdPartyApiImpl implements ThirdPartyApi {
private api = new ApiForV1();
refreshToken(token: string): string {
return this.api.refreshToken(token);
}
}
/*
Adapter(継承)
*/
class ThirdPartyApiImpl extends ApiForV1 implements ThirdPartyApi {
refreshToken(token: string): string {
return super.refreshToken(token);
}
}
利用例
アプリは Adapter を利用しているだけで V1
V2
どちらの SDK を使っているのか知る必要がなくなります。
const thirdPartyApi = new ThirdPartyApiImpl();
const deadToken = "...";
const reviveToken = thirdPartyApi.refreshToken(legacyToken);
SDK のバージョンを切り替える時は Adapter を変更します。
class ThirdPartyApiImpl implements ThirdPartyApi {
- private api = new ApiForV1();
+ private api = new ApiForV2();
- class ThirdPartyApiImpl extends ApiForV1 implements ThirdPartyApi {
+ class ThirdPartyApiImpl extends ApiForV2 implements ThirdPartyApi {
このパターンが解決する問題
もしアプリから SDK のメソッドを直接呼び出した場合、API とそれに伴う SDK のバージョンアップによりコードの書き換えが大量に発生します。メソッドの引数の順番が login(token:string, option:string)
から login(option:string, token:string)
に変わったとしたら、コンパイルエラーやユニットテストを手がかりにコードを修正する事はできません。
Adapter はこのような互換性のないインターフェースの相違を安全に「適合」させる事ができるパターンです。