Javaにおける特殊ケースパターン:事前定義されたケースによる例外処理の簡素化
別名
- 例外的なケース
特殊ケースデザインパターンの意図
Javaの特殊ケースデザインパターンは、メインのコードベースを複雑にすることなく、ソフトウェア開発における固有の、あるいは例外的な状況に対処するための堅牢なフレームワークを提供します。
実例を用いた特殊ケースパターンの詳細説明
実例
高速道路の料金所システムを考えてみましょう。通常、車両は料金所を通過し、システムは車両の種類に基づいて料金を請求します。しかし、救急車や消防車などの緊急車両は料金を請求すべきではないという特殊ケースがあります。
例えば、料金管理システムでは、特殊ケースパターンにより、緊急車両を個別に処理し、追加のチェックなしで効率的な料金処理を保証します。緊急車両クラスは料金計算メソッドをオーバーライドして料金が適用されないようにし、条件チェックでメインの料金計算ロジックを混乱させることなく、この特殊な動作をカプセル化します。これにより、コードベースはクリーンに保たれ、特殊ケースが常に一貫して処理されます。
簡単に言うと
特殊ケースデザインパターンは、例外的な状況や特定のシナリオをカプセル化し、分離することで、メインのコードロジックを簡素化し、保守性を向上させます。
Martin Fowlerの「Patterns of Enterprise Application Architecture」では
言い訳がましい表現になりますが、私はNull Objectを特殊ケースの特殊ケースとみなしています。
Javaにおける特殊ケースパターンのプログラミング例
特殊ケースパターンは、コード内の一般的なケースとは別に、特定の、しばしばまれなケースを処理するために使用されるソフトウェアデザインパターンです。このパターンは、クラスの状態に基づいて条件付きロジックを必要とする動作がある場合に役立ちます。クラスに条件付きロジックを詰め込む代わりに、特殊な動作をサブクラスにカプセル化することができます。
eコマースシステムでは、プレゼンテーション層はアプリケーション層が特定のビューモデルを生成することに依存しています。レシートビューモデルに実際の購入データが含まれる成功シナリオと、いくつかの失敗シナリオがあります。
Db
クラスは、ユーザー、アカウント、製品のデータを持つシングルトンです。データベースにデータをシードし、データベースからデータを見つけるメソッドを提供します。
@RequiredArgsConstructor
@Getter
public class Db {
// Singleton instance of Db
private static Db instance;
// Maps to hold data
private Map<String, User> userName2User;
private Map<User, Account> user2Account;
private Map<String, Product> itemName2Product;
// Singleton method to get instance of Db
public static synchronized Db getInstance() {
if (instance == null) {
Db newInstance = new Db();
newInstance.userName2User = new HashMap<>();
newInstance.user2Account = new HashMap<>();
newInstance.itemName2Product = new HashMap<>();
instance = newInstance;
}
return instance;
}
// Methods to seed data into Db
public void seedUser(String userName, Double amount) { /*...*/ }
public void seedItem(String itemName, Double price) { /*...*/ }
// Methods to find data in Db
public User findUserByUserName(String userName) { /*...*/ }
public Account findAccountByUser(User user) { /*...*/ }
public Product findProductByItemName(String itemName) { /*...*/ }
}
次に、プレゼンテーション層、レシートビューモデルインターフェース、および成功シナリオの実装を示します。
public interface ReceiptViewModel {
void show();
}
@RequiredArgsConstructor
@Getter
public class ReceiptDto implements ReceiptViewModel {
private static final Logger LOGGER = LoggerFactory.getLogger(ReceiptDto.class);
private final Double price;
@Override
public void show() {
LOGGER.info(String.format("Receipt: %s paid", price));
}
}
そして、特殊ケースである失敗シナリオの実装を示します。
public class DownForMaintenance implements ReceiptViewModel {
private static final Logger LOGGER = LoggerFactory.getLogger(DownForMaintenance.class);
@Override
public void show() {
LOGGER.info("Down for maintenance");
}
}
public class InvalidUser implements ReceiptViewModel {
private static final Logger LOGGER = LoggerFactory.getLogger(InvalidUser.class);
private final String userName;
public InvalidUser(String userName) {
this.userName = userName;
}
@Override
public void show() {
LOGGER.info("Invalid user: " + userName);
}
}
public class OutOfStock implements ReceiptViewModel {
private static final Logger LOGGER = LoggerFactory.getLogger(OutOfStock.class);
private String userName;
private String itemName;
public OutOfStock(String userName, String itemName) {
this.userName = userName;
this.itemName = itemName;
}
@Override
public void show() {
LOGGER.info("Out of stock: " + itemName + " for user = " + userName + " to buy");
}
}
public class InsufficientFunds implements ReceiptViewModel {
private static final Logger LOGGER = LoggerFactory.getLogger(InsufficientFunds.class);
private String userName;
private Double amount;
private String itemName;
public InsufficientFunds(String userName, Double amount, String itemName) {
this.userName = userName;
this.amount = amount;
this.itemName = itemName;
}
@Override
public void show() {
LOGGER.info("Insufficient funds: " + amount + " of user: " + userName
+ " for buying item: " + itemName);
}
}
App
とその様々なシナリオを実行するmain
関数を示します。
public class App {
private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
private static final String LOGGER_STRING = "[REQUEST] User: {} buy product: {}";
private static final String TEST_USER_1 = "ignite1771";
private static final String TEST_USER_2 = "abc123";
private static final String ITEM_TV = "tv";
private static final String ITEM_CAR = "car";
private static final String ITEM_COMPUTER = "computer";
public static void main(String[] args) {
// DB seeding
LOGGER.info("Db seeding: " + "1 user: {\"ignite1771\", amount = 1000.0}, "
+ "2 products: {\"computer\": price = 800.0, \"car\": price = 20000.0}");
Db.getInstance().seedUser(TEST_USER_1, 1000.0);
Db.getInstance().seedItem(ITEM_COMPUTER, 800.0);
Db.getInstance().seedItem(ITEM_CAR, 20000.0);
final var applicationServices = new ApplicationServicesImpl();
ReceiptViewModel receipt;
LOGGER.info(LOGGER_STRING, TEST_USER_2, ITEM_TV);
receipt = applicationServices.loggedInUserPurchase(TEST_USER_2, ITEM_TV);
receipt.show();
MaintenanceLock.getInstance().setLock(false);
LOGGER.info(LOGGER_STRING, TEST_USER_2, ITEM_TV);
receipt = applicationServices.loggedInUserPurchase(TEST_USER_2, ITEM_TV);
receipt.show();
LOGGER.info(LOGGER_STRING, TEST_USER_1, ITEM_TV);
receipt = applicationServices.loggedInUserPurchase(TEST_USER_1, ITEM_TV);
receipt.show();
LOGGER.info(LOGGER_STRING, TEST_USER_1, ITEM_CAR);
receipt = applicationServices.loggedInUserPurchase(TEST_USER_1, ITEM_CAR);
receipt.show();
LOGGER.info(LOGGER_STRING, TEST_USER_1, ITEM_COMPUTER);
receipt = applicationServices.loggedInUserPurchase(TEST_USER_1, ITEM_COMPUTER);
receipt.show();
}
}
例を実行した出力結果です。
11:23:48.669 [main] INFO com.iluwatar.specialcase.App -- Db seeding: 1 user: {"ignite1771", amount = 1000.0}, 2 products: {"computer": price = 800.0, "car": price = 20000.0}
11:23:48.672 [main] INFO com.iluwatar.specialcase.App -- [REQUEST] User: abc123 buy product: tv
11:23:48.672 [main] INFO com.iluwatar.specialcase.DownForMaintenance -- Down for maintenance
11:23:48.672 [main] INFO com.iluwatar.specialcase.MaintenanceLock -- Maintenance lock is set to: false
11:23:48.672 [main] INFO com.iluwatar.specialcase.App -- [REQUEST] User: abc123 buy product: tv
11:23:48.673 [main] INFO com.iluwatar.specialcase.InvalidUser -- Invalid user: abc123
11:23:48.674 [main] INFO com.iluwatar.specialcase.App -- [REQUEST] User: ignite1771 buy product: tv
11:23:48.674 [main] INFO com.iluwatar.specialcase.OutOfStock -- Out of stock: tv for user = ignite1771 to buy
11:23:48.674 [main] INFO com.iluwatar.specialcase.App -- [REQUEST] User: ignite1771 buy product: car
11:23:48.676 [main] INFO com.iluwatar.specialcase.InsufficientFunds -- Insufficient funds: 1000.0 of user: ignite1771 for buying item: car
11:23:48.676 [main] INFO com.iluwatar.specialcase.App -- [REQUEST] User: ignite1771 buy product: computer
11:23:48.676 [main] INFO com.iluwatar.specialcase.ReceiptDto -- Receipt: 800.0 paid
結論として、特殊ケースパターンは、特殊ケースを一般的なケースから分離することにより、コードをクリーンで分かりやすく保つのに役立ちます。また、コードの再利用を促進し、コードの保守を容易にします。
Javaで特殊ケースパターンを使用する場合
- メインのコードベース全体に散らばっている条件付きロジックを避ける方法で、特殊ケースやエラー状態をカプセル化して処理したい場合に使用します。
- 特定の操作に、異なる処理を必要とする既知の例外的なケースがあるシナリオで役立ちます。
Javaにおける特殊ケースパターンの実用例
- Nullチェックを回避するためにNullオブジェクトパターンを実装する。
- eコマースアプリケーションにおける特定のビジネスルールまたは検証ロジックを処理する。
- データ処理アプリケーションで異なるファイル形式またはプロトコルを管理する。
特殊ケースパターンのメリットとトレードオフ
メリット
特殊ケースデザインパターンの採用
- コアアルゴリズムから特殊ケースの処理を削除することで、メインロジックを簡素化します。
- 特殊ケースを分離することで、コードの可読性と保守性を向上させます。
トレードオフ
- システムのコンポーネント数を増やす、追加のクラスやインターフェースを導入する可能性があります。
- 特殊ケースが正しくカプセル化され、予期しない動作が発生しないように、慎重な設計が必要です。
関連するJavaデザインパターン
- デコレータ:コードを変更せずにオブジェクトに特殊ケースの動作を動的に追加するために使用できます。
- Null Object:特殊ケースの特定の種類であるnull参照に対するデフォルトの動作を提供するために使用されます。
- ストラテジー:異なるストラテジックラスにカプセル化することで、特殊ケースの動作を動的に切り替えることができます。