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

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

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

Intent

ポートとアダプターのパターンとも呼ばれる六角形アーキテクチャパターンは、2005 年に Alistair Cockburn によって提案されました。データストアやユーザーインターフェイス () に依存しずに、アプリケーションコンポーネントを個別にテストできる疎結合アーキテクチャを作成することを目的としていますUIs。このパターンは、データストアと のテクノロジーロックインを防ぐのに役立ちますUIs。これにより、ビジネスロジックへの影響が限定的またはまったくなく、時間の経過とともにテクノロジースタックを簡単に変更できます。この疎結合アーキテクチャでは、アプリケーションはポート と呼ばれるインターフェイスを介して外部コンポーネントと通信し、アダプターを使用してこれらのコンポーネントとの技術的な交換を変換します。

導入する理由

六角形アーキテクチャパターンは、データベースや外部 にアクセスするためのコードなど、関連するインフラストラクチャコードからビジネスロジック (ドメインロジック) を分離するために使用されますAPIs。このパターンは、外部サービスとの統合を必要とする AWS Lambda 関数用に疎結合のビジネスロジックとインフラストラクチャコードを作成するのに役立ちます。従来のアーキテクチャでは、ストアドプロシージャとしてデータベースレイヤーにビジネスロジックを埋め込み、ユーザーインターフェイスにビジネスロジックを埋め込むことが一般的です。この手法は、ビジネスロジック内で UI 固有のコンストラクトを使用することとともに、データベース移行とユーザーエクスペリエンス (UX) のモダナイゼーションの取り組みにボトルネックを引き起こすアーキテクチャを密接に結び付けます。六角形アーキテクチャパターンを使用すると、テクノロジーではなく目的別にシステムやアプリケーションを設計できます。この戦略により、データベース、UX、サービスコンポーネントなどのアプリケーションコンポーネントを簡単に交換できます。

適用対象

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

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

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

  • UI とデータベースコンポーネントには、アプリケーションロジックに影響を与えない定期的なテクノロジー更新が必要です。

  • アプリケーションには複数の入力プロバイダーと出力コンシューマーが必要であり、アプリケーションロジックをカスタマイズすると、コードが複雑になり、拡張性が損なわれます。

問題点と考慮事項

  • ドメイン駆動型設計: 六角形アーキテクチャは、ドメイン駆動型設計 () で特にうまく機能しますDDD。各アプリケーションコンポーネントは のサブドメインを表しDDD、六角形アーキテクチャを使用してアプリケーションコンポーネント間の疎結合を実現できます。

  • テスト可能性: 設計上、六角形アーキテクチャは入力と出力に抽象化を使用します。したがって、本質的に疎結合であるため、単体テストの作成と単体テストが容易になります。

  • 複雑さ: ビジネスロジックをインフラストラクチャコードから分離する複雑さは、慎重に処理すると、俊敏性、テストカバレッジ、テクノロジーの適応性などの大きな利点をもたらす可能性があります。そうしないと、問題が解決するのが複雑になる可能性があります。

  • メンテナンスオーバーヘッド : アーキテクチャをプラガブルにする追加のアダプターコードは、アプリケーションコンポーネントが複数の入力ソースと出力先への書き込みを必要とする場合、または入力と出力のデータストアが時間の経過とともに変更される場合にのみ正当化されます。そうしないと、アダプターはメンテナンスする別の追加レイヤーになり、メンテナンスオーバーヘッドが発生します。

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

実装

六角形アーキテクチャは、インフラストラクチャコードと、アプリケーションを 、外部 UIs、APIsデータベース、およびメッセージブローカーと統合するコードからのアプリケーションとビジネスロジックの分離をサポートします。ポートやアダプターを使用して、ビジネスロジックコンポーネントをアプリケーションアーキテクチャの他のコンポーネント (データベースなど) に簡単に接続できます。

ポートは、アプリケーションコンポーネントへのテクノロジーに依存しないエントリポイントです。これらのカスタムインターフェイスは、誰が、または何がインターフェイスを実装するかに関係なく、外部アクターがアプリケーションコンポーネントと通信できるようにするインターフェイスを決定します。これは、USBポートがUSBアダプターを使用している限り、さまざまなタイプのデバイスがコンピュータと通信できるようにする方法に似ています。

アダプターは、特定のテクノロジーを使用してポートを介してアプリケーションとやり取りします。アダプターはこれらのポートに接続し、 からデータを受信したり、ポートにデータを提供したりして、データを変換してさらに処理します。例えば、RESTアダプターを使用すると、アクターは REST を介してアプリケーションコンポーネントと通信できますAPI。ポートには、ポートやアプリケーションコンポーネントにリスクを与えることなく、複数のアダプターを含めることができます。前の例を拡張するために、同じポートに GraphQL アダプターを追加すると、アクターが 、ポートAPI、またはアプリケーションに影響を与えAPIずに GraphQL REST を介してアプリケーションとやり取りするための追加の手段が提供されます。

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

例えば、疎結合アーキテクチャでは、アプリケーションコンポーネントはデータストアの詳細を知らなくてもデータの読み書きを行える必要があります。アプリケーションコンポーネントの責任は、インターフェイス (ポート) にデータを提供することです。アダプターは、データストアへの書き込みロジックを定義します。データストアは、アプリケーションのニーズに応じて、データベース、ファイルシステム、または 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 クラスは受信者クラスに接続し、ドメインロジックを実行します。

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-modelsample の GitHub リポジトリを参照してください。

関連情報

動画

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