クリーンコード【境界編】
はじめに
本記事では、Robert C. Martinの名著『Clean Code』の第8章「境界」に関して自分用にまとめました。コードの具体例は差し替えたり、追加したりしています。本書にはさらに詳細なベストプラクティスが含まれていますので、興味がある方はぜひお読みください。
イントロダクション
この記事では、外部のコードと自分のコードを綺麗に接続するための方法について詳しく見ていきます。多くのプロジェクトでは、サードパーティのライブラリやフレームワークを利用することが一般的です。これらの外部依存を適切に扱うことは、システムの安定性や保守性を高めるために重要です。
サードパーティのインターフェース境界
サードパーティのパッケージやフレームワークは多様なアプリケーションで動作することを意識しているため、使用者側のニーズに特化したインターフェースを提供するわけではありません。例えば、java.util.Mapを利用する場合を考えてみましょう。
Map<Member> members = new HashMap<>();
...
Member m = members.get(memberId);
Mapにはget以外にも多くの機能が含まれているため、アプリケーションにとって必要以上の機能を提供してしまいます。さらに、Mapインターフェースをシステム内で無造作に持ち回ると、Mapの仕様が変更された場合に多くの箇所で影響を受けることになります。Mapのような標準ライブラリであれば頻繁に変更されることは少ないかもしれませんが、サードパーティのライブラリではバージョンアップによる変更が頻繁に発生します。加えて、別のライブラリに乗り換える際にも修正が大変になります。
こうした問題を回避するために、以下のようにライブラリをラップすることで外部インターフェースを隠すことができます。
public class Members {
private Map members = new HashMap<>();
public Member getById(String id) {
return members.get(id);
}
}
この方法により、外部ライブラリのアプリケーションに対する影響を最小限に抑えることができ、拡張や変更も容易になります。また、アプリケーションのニーズに合った設計とビジネスルールを強制することで、コードの理解が容易になり、誤用も防ぎやすくなります。
もちろん、必ずしもMapを利用する際にこのようにカプセル化すべきというわけではありません。重要なのは、サードパーティのインターフェースをむやみにシステム全体で持ち回らないようにすることであり、もし持ち回る際はクラス内部や強い関連を持ったクラス内だけに留めるようにするべきです。また、公開APIでサードパーティのインターフェースをそのまま返したり、引数として受け取ることは避けるべきです。
サードパーティの学習テスト
サードパーティの学習テストを書くことで、対象の知識を的確に実験することができますし、新バージョンで互換性のない変更が行われた際にも直ちに発見できます。また、テストによって外部との明確な境界を担保でき、将来のライブラリの移行も簡単に行うことができます。以下は、log4jに対する学習テストの例です。
public class LogTest {
private Logger logger;
@Before
public void initialize() {
logger = Logger.getLogger("logger");
logger.removeAllAppenders();
Logger.getRootLogger().removeAllAppenders();
}
@Test
public void basicLogger() {
BasicConfigurator.configure();
logger.info("basicLogger");
}
@Test
public void addAppenderWithStream() {
logger.addAppender(new ConsoleAppender(
new PatternLayout("%p %t %m%n"),
ConsoleAppender.SYSTEM_OUT));
logger.info("addAppenderWithStream");
}
@Test
public void addAppenderWithoutStream() {
logger.addAppender(new ConsoleAppender(
new PatternLayout("%p %t %m%n")));
logger.info("addAppenderWithoutStream");
}
}
未知のコードに対する境界
システム開発の際、自分達のコードが利用予定のサブシステムやAPIの仕様が未確定の状態で実装を進めなければならないことがあります。この場合、先に自分たちに都合の良いインターフェースを定義し、アダプターパターンを使って後から仕様の違いを吸収するアプローチが有効です。
例えば、Eコマースプラットフォームの開発プロジェクトで、支払いシステムを導入する例を考えます。支払いシステムの詳細なAPI仕様はまだ確定していません。まず、我々のシステムに必要なインターフェースを定義します。
public interface PaymentService {
void processPayment(PaymentDetails paymentDetails);
PaymentStatus getPaymentStatus(String transactionId);
}
このインターフェースを用いて実装を進めることができます。
public class PaymentController {
private PaymentService paymentService;
public PaymentController(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void handlePayment(PaymentDetails paymentDetails) {
paymentService.processPayment(paymentDetails);
}
public PaymentStatus checkPaymentStatus(String transactionId) {
return paymentService.getPaymentStatus(transactionId);
}
}
実際の支払いシステム(例えば、Stripe)のAPI仕様が確定したら、アダプターを作成してインターフェースに適合させます。
public class PaymentServiceAdapter implements PaymentService {
private StripeApi stripeApi;
public PaymentServiceAdapter() {
this.stripeApi = new StripeApi(); // Stripe APIの初期化
}
@Override
public void processPayment(PaymentDetails paymentDetails) {
// Stripe APIを使った支払い処理の実装
stripeApi.charge(paymentDetails.getAmount(), paymentDetails.getCurrency(), paymentDetails.getCardDetails());
}
@Override
public PaymentStatus getPaymentStatus(String transactionId) {
// Stripe APIを使った支払いステータスの取得
return stripeApi.retrievePaymentStatus(transactionId);
}
}
このアプローチにより、次のような利点が得られます。
- 柔軟性: 支払いシステムの詳細が不明な間でも、既存のシステムに影響を与えずに他の部分の開発を進めることができます。
- モジュール化: インターフェースを介して支払いシステムを切り替えることが容易になります。
- テスト容易性: 各支払いシステムの実装を独立してテストすることが可能です。
このように、専用のインターフェースを先に定義し、アダプターパターンを利用することで、システム開発の柔軟性と効率性を高めることができます。
結論
サードパーティのコードに依存する際には、自分たちが制御できる小さな領域でサードパーティをラップするか、アダプターパターンを使用してサードパーティのインターフェースを自分達専用のインターフェースに変換することで、サードパーティのコードの変更による影響を最小限に抑えることができます。自分たちのコードの広範囲にサードパーティのコードの詳細を知らしめることを避け、自分たちが制御できるものに対して依存することが大切です。
Discussion