今回は何かと耳にするAdapterパターンについてです。
Adapterパターンは、インターフェース(API)が異なっているクラス同士を結びつけるパターンです。
目的
現実世界との比喩
Adapterパターンは、変換アダプターをイメージすると良いでしょう。
USB type-B
の機器を接続したいけど、PCはUSB type-C
対応で接続することができない。
そこで登場するのが変換アダプター
ですよね。Type-Bの穴もあれば、Type-Cの穴もある。
変換することで、本来使うことができなかった機器同士を接続することができる。
今回紹介するAdapterパターンは、まさにメソッドを変換するアダプターです。
保守の段階で活用する
誤解してはいけないのは、Adapterを使わないで済むなら使わないに越したことはないです。
基本的にType-C対応機器を使うのなら、Type-C対応のケーブルを使うべきです。
メソッドも一致して利用できるのであれば、元からAdapterを使わないように設計するのがベストでしょう。
ただ途中から、テスト済みのクラス同士を組み合わせて実装しないといけないとき、このAdapterパターンが必要になるのです。
現実だと、昔はType-Bが主流だったので問題なかったけど、今はType-Cが主流になり、以前購入した機器と接続するには変換アダプターが必要になった。それで仕方なく変換アダプターを利用している。これがリアルでしょう。
メソッドも同じで昔の要件だと問題にならなかったけど、要件が複雑になったり、新しいAPIと接続する必要がでたりして、変換アダプターが必要になった。こういったケースに利用します。
メリット
Adapterパターンを利用するメリットは、テスト済みのクラスを変更しないで済むことです。
例えば返り値を変更した場合、このクラスを変更前から利用している処理すべてに影響がでます。そのテストが大変ですし、バグも生まれるでしょう。既存で利用しているサービスに影響がなく、一方で新しい要件を満たしたい。
この際、既存の処理をAdapterで加工して情報を渡せば、
テスト完了済みのクラスを変更せずに、新しい要件を満たすことができます。
これがAdapterパターンのメリットです。
既存の機能に影響を及ばないのは、非常にメリットです。
規模が大きいサービスほど、Adapterパターンの恩恵を得られるでしょう。
よく使うであろう処理
異なるAPIで同じ機能を実装する。
例:メッセージを送る(Email, Slack, Twitter)
例:在庫管理(Amazon API, 楽天API, zozo API)
登場する役割
役割一覧
名 | 役割クラスの役割 | 現実世界だと |
Client | 【Controller・Service】 メソッドの呼び出し元。Targetのメソッドを利用する。 | PC |
Target | 【interfaceまたは抽象クラス】 Adapterで利用する共通メソッドを定義。 | PCのUSBポート |
Adapter | 【実装クラスまたは具象クラス】 Targetで定義した共通メソッドを持つ。Targetで定義した共通メソッド内で、Adapteeを利用して処理を実現する。 | 変換アダプター |
Adaptee | 【クラス】 APIを扱うクラスなどテスト済みのクラス。Adaptee同士は全く異なる処理を持つ。 | 接続したい機器 |
図解でわかる関係性
例:メッセージ送信処理
リファクタリングの余地はありますが、簡単にコードの例を紹介します。
Client
/**
* Client:ControllerまたはService
*/
function sendEmail(Notification $notification)
{
$notification = new EmailNotification("developers@example.com"); // Adapter
$notification->send();
}
/**
* Client:ControllerまたはService
*/
function sendSlack(Notification $notification)
{
$slackApi = new SlackApi("example.com", "XXXXXXXX"); // Adaptee:Adapter内でnewで生成した方がよい
$notification = new SlackNotification($slackApi, "Example.com Developers"); // Adapter
$notification->send();
}
/**
* Client:ControllerまたはService
*/
function sendTwitter(Notification $notification)
{
$twitterApi = new TwitterApi("@example", "XXXXXXXX"); // Adaptee:Adapter内でnewで生成した方がよい
$notification = new TwitterNotification($twitterApi, "Example.com Developers"); // Adapter
$notification->send();
}
Target
/**
* Target
*/
interface Notification
{
public function send(string $title, string $message);
}
Adapter & Adaptee
/**
* Adapter
*/
class EmailNotification implements Notification
{
private $adminEmail;
public function __construct(string $adminEmail)
{
$this->adminEmail = $adminEmail;
}
/**
* TargetMethod
*/
public function send(string $title, string $message): void
{
mail($this->adminEmail, $title, $message);
echo "Sent email with title '$title' to '{$this->adminEmail}' that says '$message'.";
}
}
Slack
/**
* Adaptee
*/
class SlackApi
{
private $login;
private $apiKey;
public function __construct(string $login, string $apiKey)
{
$this->login = $login;
$this->apiKey = $apiKey;
}
public function login(): void
{
echo "Logged in to a slack account '{$this->login}'.\n";
}
public function sendMessage(string $chatId, string $message): void
{ echo "Posted following message into the '$chatId' chat: '$message'.\n";
}
}
/**
* Adapter
*/
class SlackNotification implements Notification
{
private $slack;
private $chatId;
public function __construct(SlackApi $slack, string $chatId)
{
$this->slack = $slack;
$this->chatId = $chatId;
}
/**
* TargetMethod
*/
public function send(string $title, string $message): void
{
$slackMessage = "#" . $title . "# " . strip_tags($message);
$this->slack->login();
$this->slack->sendMessage($this->chatId, $slackMessage);
}
}
似ているデザインパターン
Bridgeパターン
Bridgeパターンは、機能の階層と実装の階層を結びつけるパターンです。
Decoratorパターン
Decoratorパターンは、インターフェース(API)を変えずに機能を追加するパターンです。
Stateパターン
Adapteeはないが、Target
とAdapter
とClient
の3点の関係性を使って、状態管理している。
もちろん状態管理のStateパターンなので、役割の名称は異なる。しかし、構造は非常に似ている。
終わりに
いかがだったでしょうか?
複数の異なるAPIで同じ機能を実装するときに非常に便利ですよね
今後もデザインパターンなどアウトプットしていきますので、ぜひチェックしてください。
コメント