ポリモーフィズムについて
Published on December 13, 2024
Category: オブジェクト指向
はじめに
今回は、通知システムの実装を通じて、ポリモーフィズムがどのように保守性と拡張性の向上について見ていきたいと思います。
※実装要件については、携わっているサービス要件は説明できないため、本記事上では上実装のコード・要件を置き換えて説明しております。
※サンプルコードはあくまでも大袈裟にわかりやすくしたものです。
背景
最近担当した実装要件に基づいた機能開発で、通知システムの実装を行いました。
送信機能を要件を満たした機能を実装するため、要件に基づいて、通知プロバイダーごとにクラスを各自の送信機能を実装していました。
また、レビュー時にポリモーフィズムを用いて実装することでより保守性・拡張性の高い実装を行えると、助言をいただいたので改めてコードに落とし込んで実装を考えてみました。
実装要件
- メール通知の実装
- LINE通知の連携
- モバイルアプリへのプッシュ通知対応
- 将来的な通知手段の追加への対応
当初の実装は以下のように通知プロバイダーごとにコールして呼び出していました。
class NotificationService
{
private $emailService;
private $lineService;
private $pushService;
public function __construct(
EmailService $emailService,
LineService $lineService,
PushNotificationService $pushService
) {
$this->emailService = $emailService;
$this->lineService = $lineService;
$this->pushService = $pushService;
}
public function sendNotification(string $type, string $userId, string $message): bool
{
switch ($type) {
case 'email':
$userEmail = $this->getUserEmail($userId);
return $this->emailService->send($userEmail, $message);
case 'line':
$lineId = $this->getLineId($userId);
return $this->lineService->sendMessage($lineId, $message);
case 'push':
$deviceToken = $this->getDeviceToken($userId);
return $this->pushService->sendPush($deviceToken, $message);
default:
throw new InvalidArgumentException("Unknown notification type: {$type}");
}
}
// 実際の実装ではDBからの取得します。
private function getUserEmail(string $userId): string
{
return "user@example.com";
}
private function getLineId(string $userId): string
{
return "U1234567890";
}
private function getDeviceToken(string $userId): string
{
return "device-token-123";
}
}
実装における課題点
コードレビューを通じて、以下の課題が明確になりました:
- 通知手段の追加ごとにクラスの修正が必要
- 条件分岐による複雑性の増加
- 単一責任の原則への違反
これらの課題に対し、ポリモーフィズムを活用したリファクタリングを行うことにしました。
リファクタリングのアプローチ
まず、通知機能の共通インターフェースを定義しました:
interface NotifierInterface
{
/**
* 通知を送信する
* @param string $userId ユーザーID
* @param string $message 通知メッセージ
* @return bool 送信成功の場合はtrue
*/
public function send(string $userId, string $message): bool;
}
次に、各通知手段の具体的な実装を個別のクラスとして分離しました:
class EmailNotifier implements NotifierInterface
{
private $emailService;
public function __construct(EmailService $emailService)
{
$this->emailService = $emailService;
}
public function send(string $userId, string $message): bool
{
$userEmail = $this->getUserEmail($userId);
return $this->emailService->send($userEmail, $message);
}
private function getUserEmail(string $userId): string
{
return "user@example.com"; // 実際の実装ではDBからの取得
}
}
// 他の通知実装も同様のパターンで実装
そして、メインの通知サービスを以下のように改修しました:
class NotificationService
{
/** @var array<string, NotifierInterface> */
private $notifiers;
public function __construct(array $notifiers)
{
$this->notifiers = $notifiers;
}
public function sendNotification(string $type, string $userId, string $message): bool
{
if (!isset($this->notifiers[$type])) {
throw new InvalidArgumentException("Unknown notification type: {$type}");
}
return $this->notifiers[$type]->send($userId, $message);
}
}
// 利用例
$notificationService = new NotificationService([
'email' => new EmailNotifier($emailService),
'line' => new LineNotifier($lineService),
'push' => new PushNotifier($pushService)
]);
改善された点
このリファクタリングにより、以下の改善が実現できました:
- 拡張性の向上
- 新しい通知手段の追加が容易になりました
- 既存コードの修正なしで機能追加が可能になりました
- 保守性の向上
- 各通知手段の実装が独立し、責任が明確になりました
- テストが書きやすくなりました
- コードの可読性向上
- 条件分岐が削減され、シンプルになりました
- 各クラスの役割が明確になりました
実装時の知見
- インターフェース設計のポイント
- 必要最小限のメソッドのみを定義する
- 将来的な拡張性を考慮する
- 命名規則の重要性
- 役割を明確に表現する名前を選択する
- チーム内での一貫性を保つ
- テスト容易性の向上
- モックオブジェクトの作成が容易になった
- 各実装の単体テストが書きやすくなった
まとめ
ポリモーフィズムを適切に活用することで、保守性と拡張性の高いコードを実現することができました。
下記のような観点で実装して、コードがより簡潔で保守性と拡張性の高いものを実装していけるように意識していきます。
- 同じインターフェースで複数の実装が必要な場合
- 将来的な機能追加が予想される場合
- コードの重複を避けたい場合
用語解説
- 通知プロバイダー (Notification Provider)
- 各通知サービスの提供者や実装を指す用語
参考
- インターフェース
- 下記の記事は本記事内で使用している「インターフェース」について解説してくれています。
- https://qiita.com/dsudo/items/3a882e5bc5f00e4bcd40