はじめに
本記事では、Robert C. Martinの名著『Clean Code』の第8章「境界」に関して、自分が大切だと感じた箇所を、具体的なコードを追加しながらまとめました。本書にはさらに詳細なベストプラクティスが含まれていますので、興味がある方はぜひお読みください。
本記事では、Robert C. Martinの名著『Clean Code』の第8章「境界」に関して、自分が大切だと感じた箇所を、具体的なコードを追加しながらまとめました。本書にはさらに詳細なベストプラクティスが含まれていますので、興味がある方はぜひお読みください。
この記事では、外部のコードと自分のコードを綺麗に接続するための方法を見ていきます。
サードパーティのパッケージやフレームワークは、多様なアプリケーションで動作することが求められますが、使用者側のニーズに特化したインターフェースを提供するわけではありません。例えば、java.util.Map
を利用する場合を考えてみましょう。
Map<Member> members = new HashMap<>();
...
Member m = members.get(memberId);
Map
にはget
以外にも多くの機能が含まれているため、アプリケーションにとって本来必要以上の機能を提供してしまいます。さらに、Map
インターフェースをシステム内で無造作に使用すると、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");
}
}
未知の部分があるシステム開発において、我々に都合のいいインターフェースを先に定義し、アダプターパターンを用いて後からその部分の違いを吸収するというアプローチは、非常に有効な手法です。
例えば、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);
}
}
このアプローチにより、次のような利点が得られます。
このように、インターフェースを先に定義し、アダプターパターンを利用することで、システム開発の柔軟性と効率性を高めることができます。
サードパーティのコードに依存する際には、直接使用するのではなく、自分たちが制御できる小さな領域にラップするか、アダプターパターンを使用して接続することが重要です。これにより、サードパーティのコードが変更されても、影響を最小限に抑えることができます。
例えば、java.util.Map
を直接使用するのではなく、独自のクラスでラップし、そのクラスを通じてMap
にアクセスするようにすることで、変更の影響を最小限に抑えることができます。また、未知の外部システムとのインターフェースを先に定義し、後からアダプターを実装することで、柔軟性とテスト容易性が向上します。自分たちのコードの広範囲にサードパーティのコードの詳細を知らしめることを避け、自分たちが制御できるものに依存することが最善です。
カテゴリー: