JavaにおけるSpecificationパターン:分離されたロジックによるビジネスルールの強化
別名
- フィルタ
- 基準
Specificationデザインパターンの目的
オブジェクトが満たすべきビジネスルールと基準をカプセル化し、アプリケーションの様々な場所でこれらのルールをチェックできるようにします。
Specificationパターンの詳細な説明と現実世界の例
現実世界の例
会議を組織していて、登録状況、支払完了、セッションへの関心など、特定の基準に基づいて参加者をフィルタリングする必要があると想像してください。
Specificationデザインパターンを使用すると、各基準(例:「IsRegistered」、「HasPaid」、「IsInterestedInSessionX」)ごとに個別の仕様を作成します。これらの仕様を動的に組み合わせて、登録済みで、支払いを完了し、特定のセッションに関心のある参加者など、すべての必要な基準を満たす参加者をフィルタリングできます。このアプローチにより、柔軟で再利用可能なビジネスルールが実現し、基になる参加者オブジェクトを変更せずにフィルタリングロジックを簡単に調整できます。
簡単に言うと
JavaのSpecificationデザインパターンは、ビジネスルールの効率的なカプセル化と再利用を可能にし、堅牢なソフトウェア開発のための基準を柔軟かつ動的に組み合わせる方法を提供します。
Wikipediaによると
コンピュータプログラミングにおいて、Specificationパターンは特定のソフトウェアデザインパターンであり、ビジネスルールをブール論理を使用して連結することにより、ビジネスルールを再結合できます。
JavaにおけるSpecificationパターンのプログラミング例
生き物のプールを例に考えてみましょう。特定のプロパティを持つ生き物のコレクションがあります。これらのプロパティは、事前に定義された限定的なセット(`Size`、`Movement`、`Color`などの列挙型で表現)に属している場合もあれば、連続的な値(例:`Creature`の質量)である場合もあります。連続値の場合、「パラメータ化された仕様」を使用する方がよく、ここでプロパティ値は`Creature`のインスタンス化時に引数として提供され、より大きな柔軟性が得られます。さらに、事前に定義されたプロパティとパラメータ化されたプロパティはブール論理を使用して組み合わせることができ、ほぼ無限の選択の可能性を提供します(これは「複合仕様」として知られ、以下でさらに説明します)。各アプローチの利点と欠点は、このドキュメントの最後に記載されている表に詳しく説明されています。
まず、`Creature`インターフェースを次に示します。
public interface Creature {
String getName();
Size getSize();
Movement getMovement();
Color getColor();
Mass getMass();
}
そして、`Dragon`の実装は次のようになります。
public class Dragon extends AbstractCreature {
public Dragon() {
super("Dragon", Size.LARGE, Movement.FLYING, Color.RED, new Mass(39300.0));
}
}
これらのいくつかを選択したい場合は、セレクタを使用します。飛ぶ生き物を選択するには、`MovementSelector`を使用する必要があります。このスニペットは、基底クラス`AbstractSelector`も示しています。
public abstract class AbstractSelector<T> implements Predicate<T> {
public AbstractSelector<T> and(AbstractSelector<T> other) {
return new ConjunctionSelector<>(this, other);
}
public AbstractSelector<T> or(AbstractSelector<T> other) {
return new DisjunctionSelector<>(this, other);
}
public AbstractSelector<T> not() {
return new NegationSelector<>(this);
}
}
public class MovementSelector extends AbstractSelector<Creature> {
private final Movement movement;
public MovementSelector(Movement m) {
this.movement = m;
}
@Override
public boolean test(Creature t) {
return t.getMovement().equals(movement);
}
}
一方、選択した量よりも重い生き物を選択する場合は、`MassGreaterThanSelector`を使用します。
public class MassGreaterThanSelector extends AbstractSelector<Creature> {
private final Mass mass;
public MassGreaterThanSelector(double mass) {
this.mass = new Mass(mass);
}
@Override
public boolean test(Creature t) {
return t.getMass().greaterThan(mass);
}
}
これらの構成要素を準備することで、いくつかの検索を実行できます。
@Slf4j
public class App {
public static void main(String[] args) {
// initialize creatures list
var creatures = List.of(
new Goblin(),
new Octopus(),
new Dragon(),
new Shark(),
new Troll(),
new KillerBee()
);
// so-called "hard-coded" specification
LOGGER.info("Demonstrating hard-coded specification :");
// find all walking creatures
LOGGER.info("Find all walking creatures");
print(creatures, new MovementSelector(Movement.WALKING));
// find all dark creatures
LOGGER.info("Find all dark creatures");
print(creatures, new ColorSelector(Color.DARK));
LOGGER.info("\n");
// so-called "parameterized" specification
LOGGER.info("Demonstrating parameterized specification :");
// find all creatures heavier than 500kg
LOGGER.info("Find all creatures heavier than 600kg");
print(creatures, new MassGreaterThanSelector(600.0));
// find all creatures heavier than 500kg
LOGGER.info("Find all creatures lighter than or weighing exactly 500kg");
print(creatures, new MassSmallerThanOrEqSelector(500.0));
LOGGER.info("\n");
// so-called "composite" specification
LOGGER.info("Demonstrating composite specification :");
// find all red and flying creatures
LOGGER.info("Find all red and flying creatures");
var redAndFlying = new ColorSelector(Color.RED).and(new MovementSelector(Movement.FLYING));
print(creatures, redAndFlying);
// find all creatures dark or red, non-swimming, and heavier than or equal to 400kg
LOGGER.info("Find all scary creatures");
var scaryCreaturesSelector = new ColorSelector(Color.DARK)
.or(new ColorSelector(Color.RED)).and(new MovementSelector(Movement.SWIMMING).not())
.and(new MassGreaterThanSelector(400.0).or(new MassEqualSelector(400.0)));
print(creatures, scaryCreaturesSelector);
}
private static void print(List<? extends Creature> creatures, Predicate<Creature> selector) {
creatures.stream().filter(selector).map(Objects::toString).forEach(LOGGER::info);
}
}
コンソール出力
12:49:24.808 [main] INFO com.iluwatar.specification.app.App -- Demonstrating hard-coded specification :
12:49:24.810 [main] INFO com.iluwatar.specification.app.App -- Find all walking creatures
12:49:24.812 [main] INFO com.iluwatar.specification.app.App -- Goblin [size=small, movement=walking, color=green, mass=30.0kg]
12:49:24.812 [main] INFO com.iluwatar.specification.app.App -- Troll [size=large, movement=walking, color=dark, mass=4000.0kg]
12:49:24.812 [main] INFO com.iluwatar.specification.app.App -- Find all dark creatures
12:49:24.815 [main] INFO com.iluwatar.specification.app.App -- Octopus [size=normal, movement=swimming, color=dark, mass=12.0kg]
12:49:24.816 [main] INFO com.iluwatar.specification.app.App -- Troll [size=large, movement=walking, color=dark, mass=4000.0kg]
12:49:24.816 [main] INFO com.iluwatar.specification.app.App --
12:49:24.816 [main] INFO com.iluwatar.specification.app.App -- Demonstrating parameterized specification :
12:49:24.816 [main] INFO com.iluwatar.specification.app.App -- Find all creatures heavier than 600kg
12:49:24.816 [main] INFO com.iluwatar.specification.app.App -- Dragon [size=large, movement=flying, color=red, mass=39300.0kg]
12:49:24.816 [main] INFO com.iluwatar.specification.app.App -- Troll [size=large, movement=walking, color=dark, mass=4000.0kg]
12:49:24.816 [main] INFO com.iluwatar.specification.app.App -- Find all creatures lighter than or weighing exactly 500kg
12:49:24.816 [main] INFO com.iluwatar.specification.app.App -- Goblin [size=small, movement=walking, color=green, mass=30.0kg]
12:49:24.816 [main] INFO com.iluwatar.specification.app.App -- Octopus [size=normal, movement=swimming, color=dark, mass=12.0kg]
12:49:24.816 [main] INFO com.iluwatar.specification.app.App -- Shark [size=normal, movement=swimming, color=light, mass=500.0kg]
12:49:24.816 [main] INFO com.iluwatar.specification.app.App -- KillerBee [size=small, movement=flying, color=light, mass=6.7kg]
12:49:24.816 [main] INFO com.iluwatar.specification.app.App --
12:49:24.816 [main] INFO com.iluwatar.specification.app.App -- Demonstrating composite specification :
12:49:24.816 [main] INFO com.iluwatar.specification.app.App -- Find all red and flying creatures
12:49:24.817 [main] INFO com.iluwatar.specification.app.App -- Dragon [size=large, movement=flying, color=red, mass=39300.0kg]
12:49:24.817 [main] INFO com.iluwatar.specification.app.App -- Find all scary creatures
12:49:24.818 [main] INFO com.iluwatar.specification.app.App -- Dragon [size=large, movement=flying, color=red, mass=39300.0kg]
12:49:24.818 [main] INFO com.iluwatar.specification.app.App -- Troll [size=large, movement=walking, color=dark, mass=4000.0kg]
Specificationパターンを採用することで、Javaアプリケーション内のビジネスルールの柔軟性と再利用性が大幅に向上し、より保守しやすいコードに貢献します。
JavaでSpecificationパターンを使用する場合
Java Specificationパターンを適用する状況
- 異なる基準に基づいてオブジェクトをフィルタリングする必要がある場合。
- フィルタリング基準が動的に変化する場合。
- アプリケーションのさまざまな部分で再利用する必要がある複雑なビジネスルールを含むユースケースに最適です。
JavaにおけるSpecificationパターンの現実世界の応用例
- エンタープライズアプリケーションでのユーザー入力の検証。
- eコマースアプリケーションでの検索結果のフィルタリング。
- ドメイン駆動設計(DDD)におけるビジネスルールの検証。
Specificationパターンの利点とトレードオフ
利点
- ビジネスルールの柔軟性と再利用性を向上させます。
- ビジネスルールとエンティティを分離することにより、単一責任の原則を促進します。
- ビジネスルールの単体テストを容易にします。
トレードオフ
- 小さなクラスが大量に発生し、複雑さが増す可能性があります。
- 仕様の動的チェックにより、パフォーマンスのオーバーヘッドが発生する可能性があります。
関連するJavaデザインパターン
- 複合パターン:複数の仕様を組み合わせるために、Specificationと併せて使用されることがよくあります。
- デコレータパターン:仕様に動的に追加の基準を追加するために使用できます。
- ストラテジーパターン:両方のパターンは、アルゴリズムのファミリーをカプセル化しています。ストラテジーは異なる戦略またはアルゴリズムをカプセル化しますが、Specificationはビジネスルールをカプセル化します。