複数の類似システムを安全に統合する。Strategyパターンによる「共通化」と「独立性」の両立
はじめに
トレジャー・ファクトリーの真木です。
今回は弊社の宅配買取サービスにStrategyパターンを導入した際の事例を紹介させて頂きます。
弊社の宅配買取サービスには、トレファクスタイル宅配買取・アパレル優待宅配・トレファクアマゾン宅配・ブランドコレクト宅配買取の4つのサービスがあります。これらは今までそれぞれ別々のシステムとして開発・運用されてきました。その結果、改修を行う際に4つのシステムを同時に変更しないといけないことが多く、開発に多くの時間と労力がかかってしまうという課題を抱えていました。
そこで、この課題を根本から解決するため、これら4つのシステムを共通化し、1つのシステムへ統合する方針を決定しました。しかし、これら4つのサービスにはそれぞれ独自のビジネスロジックが数多く存在します。そのため、ただシステムを1つに共通化するだけでなく、サービスごとにビジネスロジックが異なる部分は、互いに影響を与えることなく独立して変更できる柔軟な設計が不可欠でした。そこで、この「共通化」と「独立性」を両立させるために採用したのが、Strategyパターンです。
本記事では、宅配買取で集荷する箱の数を表す「集荷箱数」というユビキタス言語[1]を例として取り上げます。この「集荷箱数」を題材に、Strategyパターンをどのように統合の際に活用しているのかについて解説します。また、まだ統合途中なのですが、Strategyパターンを活用した結果、どのように開発の効率性やメンテナンス性が改善したのかについて、現時点で感じていることをまとめます。
題材にする実装と解説
まず、中核となる集荷箱数(PickupBoxCount)の値オブジェクトを記載します。
※ 公開用に大部分を編集して載せています。分かりづらい部分もあるかと思いますがご了承ください。
<?php
namespace App\Domains\Models\Pickup;
use InvalidArgumentException;
class PickupBoxCount
{
readonly int $value;
private function __construct(int $pickupBoxCount)
{
if ($pickupBoxCount <= 0) {
throw new InvalidArgumentException('集荷箱数は1以上である必要があります。value: ' . $pickupBoxCount);
}
$this->value = $pickupBoxCount;
}
public static function create(
MaxPickupBoxCountCreator $maxPickupBoxCountCreator,
int $pickupBoxCount
): self {
$maxPickupBoxCount = $maxPickupBoxCountCreator->create();
if ($maxPickupBoxCount < $pickupBoxCount) {
throw new InvalidArgumentException('不正な集荷箱数です。value: ' . $pickupBoxCount);
}
return new self($pickupBoxCount);
}
public static function reconstruct(int $pickupBoxCount): self
{
// DBなどから永続化されたデータを復元する際に、
// バリデーションをスキップしてオブジェクトを再構築するためにこのメソッドがあります
return new self($pickupBoxCount);
}
}
変数$maxPickupBoxCountは、集荷可能な箱数の上限である「集荷上限箱数」を表しています。
この値オブジェクトで重要なのが、PickupBoxCountクラスのcreateファクトリメソッドです。
集荷上限箱数を超えた集荷箱数が指定されることはありえません。もし、集荷箱数が集荷上限箱数を超えている場合は不正な状態のため即座に例外を出さないといけません。この部分を、createファクトリメソッドで実装しています。
ここで難しいポイントとして、集荷上限箱数の生成ロジックは、上述の4つのサービスそれぞれで微妙に異なります。そのため、集荷上限箱数の生成ロジックをMaxPickupBoxCountCreatorインターフェイスとして抽出して、このインターフェイスをcreateファクトリメソッドの引数に与えています。
このインターフェイスを下記に記載します。
<?php
namespace App\Domains\Models\Pickup;
interface MaxPickupBoxCountCreator
{
// 本当は引数がありますが省略しています
public function create(): int
}
そして、4つのサービスで集荷上限箱数の生成ロジックが異なる部分を、上記のインターフェイスを実装したそれぞれのクラスの中に定義しています。
<?php
namespace App\Domains\Models\Pickup\PickupBoxCount\Style;
class StyleMaxPickupBoxCountCreator implements MaxPickupBoxCountCreator
{
// コンストラクタなどは省略
public function create(): int
{
// トレファクスタイル宅配買取の
// 集荷上限箱数の生成ロジックを定義しています。
// ロジックの詳細は省略します
}
}
<?php
namespace App\Domains\Models\Pickup\PickupBoxCount\BrandCollect;
class BrandCollectMaxPickupBoxCountCreator implements MaxPickupBoxCountCreator
{
public function create(): int
{
// ブランドコレクト宅配買取の
// 集荷上限箱数の生成ロジックを定義しています。
}
}
<?php
namespace App\Domains\Models\Pickup\PickupBoxCount\Amazon;
class AmazonMaxPickupBoxCountCreator implements MaxPickupBoxCountCreator
{
public function create(): int
{
// トレファクアマゾン宅配買取の
// 集荷上限箱数の生成ロジックを定義しています。
}
}
<?php
namespace App\Domains\Models\Pickup\PickupBoxCount\Apparel;
class ApparelMaxPickupBoxCountCreator implements MaxPickupBoxCountCreator
{
public function create(): int
{
// アパレル優待宅配の
// 集荷上限箱数の生成ロジックを定義しています。
}
}

図1: パッケージ間の依存関係
この記事では詳細を省きますが、上記の実装クラスをAbstract Factoryパターンを使って生成して、PickupBoxCountクラスのcreateファクトリメソッドに与えています。
MaxPickupBoxCountCreatorインターフェイスを実装したクラスであれば、なんでもcreateファクトリメソッドに与えることができます。これによって、4つのサービスそれぞれで異なる集荷上限箱数のロジックをcreateファクトリメソッドに与えることができ、1つのファクトリメソッドで「集荷箱数が集荷上限箱数を超えていないかどうか」の判定を柔軟に行うことができています。このような、オブジェクトの振る舞いを外部から柔軟に差し替える実装方法を、Strategyパターンと呼んでいます。
導入効果
Strategyパターンを使ったことによって、どのように開発の効率性やメンテナンス性が改善したのかについて、現時点で感じているメリットを挙げます。
- ロジックの独立性を確保し「安全な共通化」を実現
- 並行開発のしやすさが向上
- テスト容易性の向上
- 変更の影響範囲を局所化しメンテナンス性を向上
それぞれについて詳しくまとめます。
ロジックの独立性を確保し「安全な共通化」を実現
図1より、StyleMaxPickupBoxCountCreatorなどの集荷上限箱数の生成クラスは、各サービスごとに完全に独立しています。もし、StyleMaxPickupBoxCountCreatorのロジックを変更しても、その変更がApparelMaxPickupBoxCountCreatorなどの他のサービスのロジックに影響を与えてしまうことは確実にありません。これによって、4つのシステムを共通化して1つのシステムにしたとしても、各システムごとに仕様が異なる部分はお互いに影響を与えることなく完全に独立して変更することができます。
このようにStrategyパターンを使うことにより、共通化できる部分は1つにまとめつつ、各サービスで仕様が異なる部分は互いに影響を与えることなく独立して変更することができます。この「安全な共通化」を実現できることこそが、4つのシステムを1つに共通化する方針を決定づけた重要な理由です。
並行開発のしやすさが向上
集荷上限箱数の生成クラスは各サービスごとに完全に独立しています。そのため、4つのサービスを共通化しても、Aさんはトレファクスタイル宅配買取専用の集荷上限箱数の生成ロジックの開発、Bさんはブランドコレクト宅配買取専用の集荷上限箱数の生成ロジックの開発、といったように、各担当者が他のサービスへの影響を一切気にすることなく、自身の担当範囲の開発に集中できます。また、それぞれの担当者間で変更内容がコンフリクトしてしまうこともありません。
テスト容易性の向上
StyleMaxPickupBoxCountCreatorクラスなどは、状態を持たず隠れた入出力もありません。そのため、最も単純な出力値ベーステストでの単体テストが可能です。これにより、複雑な集荷上限箱数の生成ロジックを、より分かりやすく詳細に単体テストすることが可能です。
変更の影響範囲を局所化しメンテナンス性を向上
変更頻度が低い本質部分と変更頻度が高い詳細部分を分離することで、システムのメンテナンス性を高めることができます。今回の例の場合、集荷上限箱数の生成ロジックは頻繁に変更される部分で詳細部分にあたります。一方、「集荷箱数が0以下にならない」や、「集荷箱数が集荷上限箱数を超えることはない」といった仕様は、今後も変更されることがない本質部分です。ここでもし、頻繁に変更される集荷上限箱数の生成ロジックを、システムの「本質」を担うPickupBoxCountクラスに直接実装してしまうと何が起きるでしょうか。
PickupBoxCountクラスは集荷パッケージに属しています。この集荷パッケージは、システム内の様々なパッケージから依存されている安定度が非常に高いパッケージであり、システムの土台部分です。

図2: 集荷パッケージは安定度が非常に高い
この安定すべきPickupBoxCountクラスに、頻繁に変わりうる集荷上限箱数の生成ロジックを含めると、仕様変更のたびにPickupBoxCountクラス自体の修正が必要になります。その変更の影響は、PickupBoxCountクラスだけに留まらず、依存しているパッケージA〜Dの全てに波及してしまいます。少なくても、これらのパッケージに全く変更の影響が波及しないと言い切れなくなってしまいます。これでは、集荷上限箱数のロジックの変更が、心理的にも時間的にもコストが非常に高くなってしまいます。
そこでStrategyパターンの出番です。このパターンを使うことで、変更頻度が高い集荷上限箱数の生成ロジックをMaxPickupBoxCountCreatorインターフェイスとして抽出し、PickupBoxCountクラスから切り離すことができました。そして、このインターフェイスを「本質」側である集荷パッケージに配置し、具体的な実装クラス(StyleMaxPickupBoxCountCreatorなど)を、「スタイル専用パッケージ」などの各サービス専用のパッケージに配置しました。これにより、図3のように各サービスごとのパッケージが集荷パッケージに依存する形となり、依存関係逆転の原則が実現できました。

図3: 集荷上限箱数の生成ロジックを別クラス・別パッケージとして切り出した
この設計がもたらす効果は絶大です。頻繁に変更が発生するStyleMaxPickupBoxCountCreatorクラスは、どのパッケージからも依存されていません。そのため、このクラスをいくら変更しても、システム内の他の部分に変更の影響が波及してしまうことはありません。他への変更の影響を一切気にすることなく、迅速かつ安全に集荷上限箱数の仕様変更に対応できるのです。一方で、「本質」であるPickupBoxCountクラスは、具体的な集荷上限箱数の生成ロジックを知る必要がなくなり、その安定性を保ちます。
このように、Strategyパターンを用いて「本質」と「詳細」を分離し、依存関係を整理することで、変更に強い真にメンテナンス性の高い構造を実現できるのです。
まとめ
本記事では、4つの宅配買取システムが個別に存在することで生じていた高いメンテナンスコストという課題に対し、Strategyパターンを駆使して立ち向かっている事例を紹介しました。統合の際に常に意識しているのは、単にコードを一つにまとめるのではなく、「共通化」と「各サービスの独立性」をいかに両立させるかという点です。これを実現しているのがStrategyパターンです。最後にこのパターンを使ったことによるメリットを簡単にまとめておきます。
安全な共通化の実現
4つのサービス固有のロジックを完全に独立させられたことで、4つのサービスを1つのシステムに統合したとしても、あるサービスの仕様変更が他のサービスへ意図せず影響を及ぼしてしまうリスクを排除できました。
並行開発のしやすさが向上
4つのサービスを1つに統合しても、4つのサービス固有のロジックは完全に独立しているため、各担当者がそれぞれのサービス固有のロジックを同時並行で開発することができます。また、変更内容がコンフリクトしてしまう心配もありません。
テスト容易性の向上
最も単純な出力値ベーステストで、集荷上限箱数のロジックを単体テストできるようになりました。
メンテナンス性の向上
システムの基盤となる安定した部分(本質)と、頻繁に変更される末端のロジック(詳細)を切り離したことで、変更の影響範囲を極めて狭い範囲に限定できました。その結果、仕様変更に対して迅速かつ安全に対応できる、柔軟性の高い構造を実現できました。
-
宅配買取事業に関わる全ての人が使う共通言語。この共通言語を、会議での会話からプログラムのコードに至るまで様々な場面で用いています。 ↩︎
Discussion