ギャップを埋める:Rustにおけるアダプターパターンとコンポジットパターンの理解

カバー画像(Bridging the Gap: Understanding Adapter and Composite Patterns in Rust)

Max Zhuk

Max Zhuk

2023年10月4日投稿・2023年10月26日更新

こんにちは、開発者のみなさん!

ソフトウェアエンジニアリングの世界では、デザインパターンがよく使われる一般的な問題解決策として役立ちます。デザインパターンは、コードをどのように構造化するかを視覚化する手助けをし、同僚とそのアーキテクチャについて議論する際の共通言語を提供します。その中でも、構造的デザインパターンとして重要な位置を占めるものがあります。それはオブジェクトやクラスを組み合わせ複雑な構造を形成し、堅牢かつスケーラブルなソフトウェアのためのビルディングブロックとして機能するものです。

この記事では、そんな不可欠な構造的デザインパターンのうち、アダプターパターンとコンポジットパターンに焦点を当てます。これらのパターンは、特定のプログラミング言語に固有のものではありませんが、パフォーマンスと安全性に焦点を当てた言語として知られるRustでの適用に注目します。Rustのリアルなプロジェクト、オープンソースライブラリHyperswitchで、これらのパターンがどのように利用されているかを探ります。

Rustが好きな人でも、Rustがどのように構造的な課題に取り組んでいるのかに興味があるソフトウェアアーキテクトでも、この記事には何か新しい発見があります。理論と実践の間をつなぐこのプロセスに一緒に参加し、Rustでアダプターパターンとコンポジットパターンがいかに実現されているかを見てみましょう!

アダプターパターン

アダプターパターンは、二つの互換性のないインターフェイスを橋渡しする役割を果たし、本来は利用できないメソッドやプロパティを利用できるようにするためのものです。これを現実世界のアダプターに例えるなら、外国の電源プラグに自分のノートパソコンのプラグを接続するために電源アダプターを使うように、コード内の馴染みのないインターフェイスを接続するためにアダプターパターンを利用します。

ソフトウェアエンジニアリングにおいては、アダプターパターンは以下のようなシナリオでよく利用されます:

  • レガシーコードの統合:新しい機能を異なるインターフェイスを持つ既存システムと統合する必要があるとき。
  • サードパーティのライブラリの使用:役立つ機能を提供するが、アプリケーションが期待するインターフェイスと異なる外部ライブラリを使用するとき。
  • コードの再利用:異なる目的に設計された、異なるインターフェイスのコードを再利用したいとき。

Hyperswitchでの実装方法(Transformersモジュールに焦点を当てて)

Hyperswitchでは、fiservのような各支払いコネクターには、それぞれのtransformerモジュールがあります。このモジュールはHyperswitchの内部リクエストおよびレスポンスオブジェクトをfiservサービスが期待するものに適応させたりその逆を行う関数を含んでいるかもしれません。

以下に簡略化した例を示します:

// transformers/fiserv.rs内

// Hyperswitchの汎用的な支払いリクエストおよびレスポンスオブジェクトをインポート
use crate: :core: :PaymentRequest;
use crate: :core: :PaymentResponse;

// Fiservの特定のタイプをインポート
use fiserv_sdk: :FiservPaymentRequest;
use fiserv_sdk: :FiservPaymentResponse;

pub struct FiservTransformer;

impl FiservTransformer {
  // Hyperswitchの汎用的なPaymentRequestをFiservの特定のリクエストタイプに適応させる
  pub fn adapt_request(hs_request: PaymentRequest) -> FiservPaymentRequest {
    // 変換ロジックはここに記述
    // ...
  }

  // Fiservの特定のレスポンスタイプをHyperswitchの汎用的なPaymentResponseに適応させる
  pub fn adapt_response(fiserv_response: FiservPaymentResponse) -> PaymentResponse {
    // 変換ロジックはここに記述
    // ...
  }
}

この例において、FiservTransformerは一般的なPaymentRequestFiservPaymentRequestに、そしてFiservPaymentResponseを一般的なPaymentResponseに適応させるメソッドを含んでいます。

コードの説明:

  • adapt_requestはHyperswitchのコアモジュールからPaymentRequestオブジェクトを受け取り、fiservサービスが理解できるFiservPaymentRequestに変換します。
  • adapt_responseは逆の変換を行い、fiservサービスからのレスポンスをHyperswitchが取り扱える標準的なPaymentResponseオブジェクトに変換します。

利点:

  1. 柔軟性:アダプターパターンを使用すると、既存のコードを変更することなく新しい機能を導入できるので、オープン/クローズド原則に従えます。
  2. 複雑さの低減:中間層を作ることで、異なるインターフェイス間の相互作用を単純化できます。
  3. 再使用性の向上:もともと異なるインターフェイス用に設計されたコードを再利用できるので、時間と労力が節約できます。

コンポジットパターン

コンポジットパターンでは、オブジェクトを木構造に組み合わせて部分-全体の階層関係を表現することができます。このパターンでは、個別のオブジェクトとその組み合わせを同等に扱うため、その複雑さに関係なく同じ方法で対話できます。

コンポジットパターンの一般的な使用例には以下が含まれます:

  • グラフィクスシステム:2Dまたは3Dグラフィクスシステム内での図形や図形のグループを表現します。
  • ファイルシステム:ファイルやディレクトリを表現します。
  • UIフレームワーク:小さなコンポーネントから構成される複雑なUI要素を扱います。

Hyperswitchでの実装方法(EnumとStructに焦点を当てて)

Rustでは、特にHyperswitchでは、EnumとStructを使ってコンポジットパターンを実装することがよくあります。Enumは複数のタイプ間の選択肢を表し、Structは単純なものを組み合わせてより複雑なタイプを構築するために使用されます。

以下に簡略化したコード例を示します:

// core/models.rsまたは類似のファイル内

// 支払い方法の様々なタイプを表す単純なEnum
enum PaymentMethod {
    CreditCard,
    DebitCard,
    BankTransfer,
}

// 顧客を表すStruct
struct Customer {
    name: String,
    email: String,
    payment_method: PaymentMethod,  // EnumをStructに組み込む
}

// 支払いリクエストを表すStruct
struct PaymentRequest {
    customer: Customer,  // 他のStructにStructを組み込む
    amount: f64,
    currency: String,
}

コードの説明:

  • PaymentMethodは支払い方法の異なるタイプを表すEnumです。

  • Customerは様々なフィールドを含むStructであり、PaymentMethodを含むことで、EnumをStructに組み込みます。

  • PaymentRequestCustomerオブジェクトを含むもう一つのStructであり、部分-全体の階層を形成します。

利点:

  1. 統一性:コンポジットパターンによって、個々のオブジェクトとオブジェクトの組み合わせを同等に扱うことができます。
  2. 単純さ:クライアントコードを単純化することができ、複雑な組み合わせと個々のオブジェクトを同じように扱うことができます。
  3. 拡張性の向上:すべてが共通のインターフェースに従うため、新しいコンポーネントや組み合わせの追加が容易になります。

アダプターとコンポジットの使い分け

デザインパターンを理解することは第一歩ですが、それらをいつ適用するかを知ることが同様に重要です。アダプターとコンポジットはどちらも構造的なものですが、異なる目的を持ち、異なるシナリオに最適です。いつどちらを使うかを決めるために、いくつかの重要な違いを見てみましょう。

アダプターパターン

  • 目的:二つの互換性のないインターフェイスが連携できるようにする。
  • 最適なシナリオ
    • レガシーコードの統合。
    • サードパーティライブラリの使用。
    • 異なるインターフェイスを持つコードの再利用。
  • 特徴
    • 抽象化の追加レイヤーがある。
    • 通常は一つのインターフェイスから別のインターフェイスにデータや関数を変換することに関連しています。

コンポジットパターン

  • 目的:オブジェクトを木構造に組み合わせて部分-全体の階層を表現する。
  • 最適なシナリオ
    • グラフィックシステム、ファイルシステム、複雑なUIコンポーネントなどの階層的オブジェクト構造。
    • 個別のオブジェクトとオブジェクトの組み合わせを同等に扱いたいとき。
  • 特徴
    • 個々のオブジェクトと複合オブジェクトを同じ方法で扱うことで、クライアントコードを単純化します。
    • 配慮が行き届いていなければ、設計があまりにも一般的になってしまうことがあります。

主な違い

  1. 構成の性質:アダプターは機能レベルでの構成(つまり、関数やメソッドを適応する)を関係していますが、コンポジットはオブジェクトレベルでの構成(つまり、簡単なものから複雑なオブジェクトを構築する)に関連しています。

  2. クライアントとの対話:アダプターでは、クライアントは通常、適応されたインターフェイスのみと対話します。コンポジットでは、クライアントは個別のオブジェクトと複合オブジェクトの両方と対話する必要があります。

  3. 柔軟性と構造:アダプターは異なるインターフェイスの統合においてより柔軟性を提供しますが、複雑さが増す可能性があります。コンポジットでは、オブジェクト階層を構築するための構造化された方法を提供しますが、うまく管理されていない場合は複雑になりすぎる可能性があります。

結論

ソフトウェアエンジニアリングの継続的に進化する景色の中で、デザインパターンは

こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/zhukmax/bridging-the-gap-understanding-adapter-and-composite-patterns-in-rust-50ab