六角形アーキテクチャパターン - AWS 規範ガイダンス

翻訳は機械翻訳により提供されています。提供された翻訳内容と英語版の間で齟齬、不一致または矛盾がある場合、英語版が優先します。

六角形アーキテクチャパターン

Intent

六角形アーキテクチャパターンは、ポートおよびアダプターパターンとも呼ばれ、2005 年に Alistair Cockburn 博士によって提唱された手法であり、データストアやユーザーインターフェイス (UI) に依存せず、アプリケーションコンポーネントを独立してテストできる疎結合アーキテクチャの構築を目的にしています。このパターンを使用すると、データストアおよび UI の技術的ロックインを防ぎやすくなるため、長期的な観点から技術スタックを容易に変更できるうえ、ビジネスロジックへの影響もほとんど、あるいはまったく生じません。この疎結合アーキテクチャのアプリケーションでは、ポートというインターフェイスを介して外部コンポーネントと通信すると共に、アダプターによってこうしたコンポーネント間でやり取りされる技術情報が翻訳されます。

導入する理由

六角形アーキテクチャパターンを使用すると、ビジネスロジック (ドメインロジック) を、関連インフラストラクチャコード (データベースや外部 API にアクセスするコード) から分離できます。また、外部サービスとの統合が必要な AWS Lambda 関数に、疎結合のビジネスロジックとインフラストラクチャコードを作成しやすくなります。従来のアーキテクチャでは、一般的に、ビジネスロジックを、ストアドプロシージャとしてデータベース層に埋め込み、ユーザーインターフェイスにも埋め込んでいました。また、ビジネスロジック内で UI 固有のコンストラクトを使用することで、密結合のアーキテクチャが形成され、これによって、データベース移行やユーザーエクスペリエンス (UX) のモダナイズに取り組む際にボトルネックが生じていました。六角形アーキテクチャパターンにより、技術別ではなく、目的別にシステムやアプリケーションを設計できるうえ、データベース、UX、サービスといったアプリケーションコンポーネントを簡単に交換することも可能です。

適用対象

六角形アーキテクチャパターンは、次の場合に使用します。

  • アプリケーションアーキテクチャを切り離して、完全にテストできるコンポーネントを作成する必要がある。

  • 複数のクライアントタイプに同じドメインロジックを使用できる。

  • UI およびデータベースコンポーネントに、アプリケーションロジックに影響を与えることなく定期的な技術更新を行う必要がある。

  • アプリケーションに複数の入力プロバイダーと出力コンシューマーが必要なため、アプリケーションロジックをカスタマイズするとコードが複雑化し、拡張が難しくなる。

問題点と考慮事項

  • ドメイン駆動型設計: 六角形アーキテクチャは、とりわけ、ドメイン駆動型設計 (DDD) の場合に効果を発揮します。各アプリケーションコンポーネントを DDD のサブドメインとして表し、六角形アーキテクチャを適用すると、アプリケーションコンポーネント間の疎結合を実現できます。

  • テスト可能性: 六角形アーキテクチャでは、設計上、入力と出力に抽象化を使用するため、この手法特有の疎結合によって、単体テストの作成や独立したテストの実行が容易になります。

  • 複雑さ: ビジネスロジックをインフラストラクチャコードから分離するのは複雑なプロセスですが、これに慎重に対処できれば、俊敏性、テストカバレッジ、技術的適応性といった大きな利点を得られる可能性があります。そのように対処できない場合は、問題解決が複雑になりかねません。

  • メンテナンス上のオーバーヘッド: アーキテクチャをプラグイン可能にするアダプターコードの追加が、理にかなっているのは、アプリケーションコンポーネントに複数の入力元と、複数の出力書き込み先が必要な場合か、入力データストアと出力データストアを時間の経過と共に変更する必要がある場合のみです。それ以外の場合は、このアダプターによってメンテナンス対象のレイヤーが増えることになり、メンテナンス上のオーバーヘッドが発生します。

  • レイテンシーの問題: ポートとアダプターを使用すると、レイヤーをさらに追加することになるため、レイテンシーが発生する可能性があります。

実装

六角形アーキテクチャを適用すると、アプリケーションとビジネスロジックを、インフラストラクチャコードから分離すると同時に、アプリケーションを UI、外部 API、データベース、メッセージブローカーと統合するコードからも分離できます。また、ポートとアダプターを介して、ビジネスロジックコンポーネントを、アプリケーションアーキテクチャの他のコンポーネント (データベースなど) に簡単に接続することも可能です。

ポートは、アプリケーションコンポーネントに接続する技術に依存しないエントリポイントとして機能します。こうしたカスタムインターフェイスによって、外部アクターがアプリケーションコンポーネントと通信可能となるインターフェイスが決定します。そのインターフェイスを実装するユーザーやシステムを問わずそのように動作します。こうした動作は、USB ポートで USB アダプターを使用している限り、各種デバイスがコンピュータと通信できるようになるのと似ています。

アダプターは、特定の技術を使用して、ポート経由でアプリケーションとやり取りしており、こうしたポートに接続して、ポートからのデータ受信、ポートへのデータ提供、処理に必要なデータ変換を行っています。例えば、REST アダプターを使用すると、アクターが REST API を介してアプリケーションコンポーネントと通信できるようになります。ポートには、ポートやアプリケーションコンポーネントへのリスクなしに、複数のアダプターを追加できます。前の例を拡張するために、同じポートに GraphQL アダプターを追加すると、GraphQL API を介してアプリケーションとやり取りするという別の手段をアクターに提供できます。その場合でも、REST API、ポート、アプリケーションへの影響はありません。

ポートはアプリケーションに接続する機能を、アダプターは外部に接続する機能を備えています。ポートを使用すると、疎結合のアプリケーションコンポーネントを作成でき、アダプターの変更によって、依存コンポーネントを交換することが可能です。これにより、アプリケーションコンポーネントと外部の入出力要素が、コンテキストの認識なしに、やり取りを行えるようになります。また、どのレベルでもコンポーネントを交換可能なため、自動テストが容易になります。インフラストラクチャコードに依存することなく、コンポーネントを個別にテストでき、テスト実行のために環境全体をプロビジョニングする必要はありません。アプリケーションロジックに外部要因との依存関係がないため、テストの簡素化や、依存関係の模倣も簡単に行えるようになります。

例えば、疎結合アーキテクチャのアプリケーションコンポーネントでは、データストアの詳細を認識せずに、読み取りと書き込みを行えなければなりません。インターフェイス (ポート) にデータを提供することが、アプリケーションコンポーネントの役割だからです。アダプターでは、データストアへの書き込みロジックを定義し、データストアには、アプリケーションのニーズに応じて、データベース、ファイルシステム、または Amazon S3 などのオブジェクトストレージシステムを使用できます。

高レベルのアーキテクチャ

アプリケーションまたはアプリケーションコンポーネントは、コアビジネスロジックを備えており、ポートからコマンドまたはクエリを受信し、ポートを介して外部アクターにリクエストを送信します。外部アクターはアダプターを介して実装します。これを次の図に示します。

六角形アーキテクチャパターン

AWS のサービス を使用した実装

AWS Lambda 関数には、多くの場合、ビジネスロジックとデータベース統合コードの両方からなり、これらは、目的を達成するために密結合されています。六角形アーキテクチャパターンを使用すると、ビジネスロジックをインフラストラクチャコードから分離できます。こうした分離により、データベースコードへの依存なくビジネスロジックのユニットテストが可能になり、開発プロセスの俊敏性が向上します。

次のアーキテクチャでは、Lambda 関数に、六角形アーキテクチャパターンを実装しており、Lambda 関数は、Amazon API Gateway REST API によって開始します。この関数にビジネスロジックを実装することで、DynamoDB テーブルにデータを書き込みます。

AWS での六角形アーキテクチャパターンの実装

サンプルコード

このセクションのサンプルコードでは、Lambda を使用してドメインモデルを実装し、それをインフラストラクチャコード (DynamoDB にアクセスするためのコードなど) から分離した後に、関数のユニットテストを実装しています。

ドメインモデル

ドメインモデルクラスは、外部コンポーネントや依存関係に関する情報は持たず、ビジネスロジックのみを実装します。次の例の Recipient クラスは、ドメインモデルクラスであり、これによって予約日の重複をチェックします。

class Recipient: def __init__(self, recipient_id:str, email:str, first_name:str, last_name:str, age:int): self.__recipient_id = recipient_id self.__email = email self.__first_name = first_name self.__last_name = last_name self.__age = age self.__slots = [] @property def recipient_id(self): return self.__recipient_id #..... def are_slots_same_date(self, slot:Slot) -> bool: for selfslot in self.__slots: if selfslot.reservation_date == slot.reservation_date: return True return False def is_slot_counts_equal_or_over_two(self) -> bool: #.....

入力ポート

RecipientInputPort クラスは recipient クラスに接続し、ドメインロジックを実行します。

class RecipientInputPort(IRecipientInputPort): def __init__(self, recipient_output_port: IRecipientOutputPort, slot_output_port: ISlotOutputPort): self.__recipient_output_port = recipient_output_port self.__slot_output_port = slot_output_port ''' make reservation: adapting domain model business logic ''' def make_reservation(self, recipient_id:str, slot_id:str) -> Status: status = None # --------------------------------------------------- # get an instance from output port # --------------------------------------------------- recipient = self.__recipient_output_port.get_recipient_by_id(recipient_id) slot = self.__slot_output_port.get_slot_by_id(slot_id) if recipient == None or slot == None: return Status(400, "Request instance is not found. Something wrong!") print(f"recipient: {recipient.first_name}, slot date: {slot.reservation_date}") # --------------------------------------------------- # execute domain logic # --------------------------------------------------- ret = recipient.add_reserve_slot(slot) # --------------------------------------------------- # persistent an instance throgh output port # --------------------------------------------------- if ret == True: ret = self.__recipient_output_port.add_reservation(recipient) if ret == True: status = Status(200, "The recipient's reservation is added.") else: status = Status(200, "The recipient's reservation is NOT added!") return status

DynamoDB アダプタークラス

DDBRecipientAdapter クラスは、DynamoDB テーブルへのアクセスを実装します。

class DDBRecipientAdapter(IRecipientAdapter): def __init__(self): ddb = boto3.resource('dynamodb') self.__table = ddb.Table(table_name) def load(self, recipient_id:str) -> Recipient: try: response = self.__table.get_item( Key={'pk': pk_prefix + recipient_id})  ... def save(self, recipient:Recipient) -> bool: try: item = { "pk": pk_prefix + recipient.recipient_id, "email": recipient.email, "first_name": recipient.first_name, "last_name": recipient.last_name, "age": recipient.age, "slots": [] } # ...

Lambda 関数 get_recipient_input_port は、RecipientInputPort クラスインスタンスのファクトリであり、関連するアダプターインスタンスを使用して、出力ポートクラスのインスタンスを構築します。

def get_recipient_input_port(): return RecipientInputPort( RecipientOutputPort(DDBRecipientAdapter()), SlotOutputPort(DDBSlotAdapter())) def lambda_handler(event, context): body = json.loads(event['body']) recipient_id = body['recipient_id'] slot_id = body['slot_id'] # get an input port instance recipient_input_port = get_recipient_input_port() status = recipient_input_port.make_reservation(recipient_id, slot_id) return { "statusCode": status.status_code, "body": json.dumps({ "message": status.message }), }

ユニットテスト

ドメインモデルクラスのビジネスロジックをテストするには、モッククラスを挿入します。次の例は、ドメインモデル Recipent クラスのユニットテストを示しています。

def test_add_slot_one(fixture_recipient, fixture_slot): slot = fixture_slot target = fixture_recipient target.add_reserve_slot(slot) assert slot != None assert target != None assert 1 == len(target.slots) assert slot.slot_id == target.slots[0].slot_id assert slot.reservation_date == target.slots[0].reservation_date assert slot.location == target.slots[0].location assert False == target.slots[0].is_vacant def test_add_slot_two(fixture_recipient, fixture_slot, fixture_slot_2): #..... def test_cannot_append_slot_more_than_two(fixture_recipient, fixture_slot, fixture_slot_2, fixture_slot_3): #..... def test_cannot_append_same_date_slot(fixture_recipient, fixture_slot): #.....

GitHub リポジトリ

このパターンのサンプルアーキテクチャの完全な実装については、https://github.com/aws-samples/aws-lambda-domain-model-sample にある GitHub リポジトリを参照してください。

関連情報

動画

次の動画 (日本語) では、Lambda 関数を使用したドメインモデル実装における六角形アーキテクチャの使用について解説しています。