Javaにおける寛容なリーダーパターン:APIの回復力と互換性の向上
別名
- 寛大なコンシューマー
寛容なリーダーデザインパターンの意図
寛容なリーダーパターンは、認識されない要素を戦略的に無視することで、データ構造の変更に対するシステムの回復力を高め、堅牢なAPI設計を促進します。
寛容なリーダーパターンの詳細な説明と実際の例
実際の例
受取人に手紙や小包を配達する郵便システムを想像してみてください。このシステムでは、郵便局員は封筒や小包に付いている可能性のある追加情報やステッカーに関係なく、郵便物を配達します。小包に郵便システムが認識しない追加のラベルや指示がある場合、郵便局員はこれらを無視し、住所のような必須の情報のみに焦点を当てます。このアプローチは、送信者が異なる形式を使用したり、不必要な詳細を含めたりした場合でも、配信プロセスが機能し続けることを保証します。これは、寛容なリーダーパターンがソフトウェアで認識されないデータ要素を無視して機能と互換性を維持する方法と似ています。
平易な言葉で
寛容なリーダーパターンを活用して、サービス間の堅牢で回復力のある通信を確立し、データの互換性と統合を確保します。
堅牢性の原則は次のように述べています
自分の行うことは控えめに、他人から受け入れることは寛大に。
Javaにおける寛容なリーダーパターンのプログラム例
RainbowFish
オブジェクトをファイルに永続化しています。後でそれらを復元する必要があります。問題となるのは、RainbowFish
のデータ構造がバージョン管理されており、時間とともに進化することです。新しいバージョンのRainbowFish
は、古いバージョンも復元できる必要があります。
バージョン管理されたRainbowFish
を以下に示します。2番目のバージョンで追加のプロパティが導入されていることに注目してください。
@Getter
@RequiredArgsConstructor
public class RainbowFish implements Serializable {
private static final long serialVersionUID = 1L;
private final String name;
private final int age;
private final int lengthMeters;
private final int weightTons;
}
@Getter
public class RainbowFishV2 extends RainbowFish {
@Serial
private static final long serialVersionUID = 1L;
private boolean sleeping;
private boolean hungry;
private boolean angry;
public RainbowFishV2(String name, int age, int lengthMeters, int weightTons) {
super(name, age, lengthMeters, weightTons);
}
public RainbowFishV2(String name, int age, int lengthMeters, int weightTons, boolean sleeping,
boolean hungry, boolean angry) {
this(name, age, lengthMeters, weightTons);
this.sleeping = sleeping;
this.hungry = hungry;
this.angry = angry;
}
}
次に、RainbowFishSerializer
を紹介します。これは、寛容なリーダーパターンを実装するクラスです。
@NoArgsConstructor
public final class RainbowFishSerializer {
public static void writeV1(RainbowFish rainbowFish, String filename) throws IOException {
var map = Map.of(
"name", rainbowFish.getName(),
"age", String.format("%d", rainbowFish.getAge()),
"lengthMeters", String.format("%d", rainbowFish.getLengthMeters()),
"weightTons", String.format("%d", rainbowFish.getWeightTons())
);
try (var fileOut = new FileOutputStream(filename);
var objOut = new ObjectOutputStream(fileOut)) {
objOut.writeObject(map);
}
}
public static void writeV2(RainbowFishV2 rainbowFish, String filename) throws IOException {
var map = Map.of(
"name", rainbowFish.getName(),
"age", String.format("%d", rainbowFish.getAge()),
"lengthMeters", String.format("%d", rainbowFish.getLengthMeters()),
"weightTons", String.format("%d", rainbowFish.getWeightTons()),
"angry", Boolean.toString(rainbowFish.getAngry()),
"hungry", Boolean.toString(rainbowFish.getHungry()),
"sleeping", Boolean.toString(rainbowFish.getSleeping())
);
try (var fileOut = new FileOutputStream(filename);
var objOut = new ObjectOutputStream(fileOut)) {
objOut.writeObject(map);
}
}
public static RainbowFish readV1(String filename) throws IOException, ClassNotFoundException {
Map<String, String> map;
try (var fileIn = new FileInputStream(filename);
var objIn = new ObjectInputStream(fileIn)) {
map = (Map<String, String>) objIn.readObject();
}
return new RainbowFish(
map.get("name"),
Integer.parseInt(map.get("age")),
Integer.parseInt(map.get("lengthMeters")),
Integer.parseInt(map.get("weightTons"))
);
}
}
そして最後に、動作中の完全な例を以下に示します。
public static void main(String[] args) throws IOException, ClassNotFoundException {
// Write V1
var fishV1 = new RainbowFish("Zed", 10, 11, 12);
LOGGER.info("fishV1 name={} age={} length={} weight={}", fishV1.getName(),
fishV1.getAge(), fishV1.getLengthMeters(), fishV1.getWeightTons());
RainbowFishSerializer.writeV1(fishV1, "fish1.out");
// Read V1
var deserializedRainbowFishV1 = RainbowFishSerializer.readV1("fish1.out");
LOGGER.info("deserializedFishV1 name={} age={} length={} weight={}",
deserializedRainbowFishV1.getName(), deserializedRainbowFishV1.getAge(),
deserializedRainbowFishV1.getLengthMeters(), deserializedRainbowFishV1.getWeightTons());
// Write V2
var fishV2 = new RainbowFishV2("Scar", 5, 12, 15, true, true, true);
LOGGER.info(
"fishV2 name={} age={} length={} weight={} sleeping={} hungry={} angry={}",
fishV2.getName(), fishV2.getAge(), fishV2.getLengthMeters(), fishV2.getWeightTons(),
fishV2.isHungry(), fishV2.isAngry(), fishV2.isSleeping());
RainbowFishSerializer.writeV2(fishV2, "fish2.out");
// Read V2 with V1 method
var deserializedFishV2 = RainbowFishSerializer.readV1("fish2.out");
LOGGER.info("deserializedFishV2 name={} age={} length={} weight={}",
deserializedFishV2.getName(), deserializedFishV2.getAge(),
deserializedFishV2.getLengthMeters(), deserializedFishV2.getWeightTons());
}
プログラム出力
15:38:00.602 [main] INFO com.iluwatar.tolerantreader.App -- fishV1 name=Zed age=10 length=11 weight=12
15:38:00.618 [main] INFO com.iluwatar.tolerantreader.App -- deserializedFishV1 name=Zed age=10 length=11 weight=12
15:38:00.618 [main] INFO com.iluwatar.tolerantreader.App -- fishV2 name=Scar age=5 length=12 weight=15 sleeping=true hungry=true angry=true
15:38:00.619 [main] INFO com.iluwatar.tolerantreader.App -- deserializedFishV2 name=Scar age=5 length=12 weight=15
Javaで寛容なリーダーパターンを使用する場合
- システムが進化する外部ソースからデータを消費する場合、寛容なリーダーパターンを適用して、効率とデータ整合性を維持します。
- API設計で下位互換性が必要な場合に適用できます。
- 異なるシステムがデータを交換し、独立して進化する統合シナリオに適しています。
Javaにおける寛容なリーダーパターンの実際の応用例
- 不明な要素をスキップするJSONまたはXMLパーサー。
- サービスの複数のバージョンと対話するマイクロサービスアーキテクチャのAPIクライアント。
寛容なリーダーパターンの利点とトレードオフ
利点
- システムの堅牢性と柔軟性が向上します。
- 分散システムでプロデューサーとコンシューマーが独立して進化できます。
- 下位互換性を有効にすることで、バージョン管理が簡素化されます。
トレードオフ
- 重要なデータが無視されると、サイレントな障害が発生する可能性があります。
- 欠落または認識されないデータにより、問題のデバッグと追跡が複雑になる可能性があります。
関連するJavaデザインパターン
- アダプター:どちらのパターンもデータ変換と統合を扱いますが、アダプターパターンはインターフェースの変換に焦点を当て、寛容なリーダーは認識されないデータの無視に焦点を当てています。
- ファサード:寛容なリーダーが関連のないデータを無視することでデータの消費を簡素化するのと同様に、複雑なシステムとの対話を簡素化します。
- ストラテジー:寛容なリーダーと組み合わせて使用して、さまざまなデータ処理戦略を動的に切り替えることができます。