翻訳は機械翻訳により提供されています。提供された翻訳内容と英語版の間で齟齬、不一致または矛盾がある場合、英語版が優先します。
六角アーキテクチャパターン
Intent
ポートとアダプターのパターンとも呼ばれる六角形のアーキテクチャパターンは、2005 年に Alistair Cockburn によって提案されました。データストアやユーザーインターフェイス (UIs) に依存しずに、アプリケーションコンポーネントを個別にテストできる疎結合アーキテクチャを作成することを目的としています。このパターンは、データストアと UIs のテクノロジーロックインを防ぐのに役立ちます。これにより、ビジネスロジックへの影響が限定的またはまったくなく、時間の経過とともにテクノロジースタックを簡単に変更できます。この疎結合アーキテクチャでは、アプリケーションはポートと呼ばれるインターフェイスを介して外部コンポーネントと通信し、アダプターを使用してこれらのコンポーネントとの技術的な交換を変換します。
導入する理由
六角形アーキテクチャパターンは、データベースや外部 APIs にアクセスするためのコードなど、関連するインフラストラクチャコードからビジネスロジック (ドメインロジック) を分離するために使用されます。このパターンは、外部サービスとの統合を必要とする AWS Lambda 関数用に疎結合のビジネスロジックとインフラストラクチャコードを作成するのに役立ちます。従来のアーキテクチャでは、ストアドプロシージャおよびユーザーインターフェイスとしてデータベースレイヤーにビジネスロジックを埋め込むことが一般的です。この手法は、ビジネスロジック内で UI 固有のコンストラクトを使用することに加えて、データベース移行とユーザーエクスペリエンス (UX) のモダナイゼーションの取り組みにボトルネックを引き起こす密接に連携したアーキテクチャにつながります。六角形アーキテクチャパターンを使用すると、テクノロジーではなく目的別にシステムやアプリケーションを設計できます。この戦略により、データベース、UX、サービスコンポーネントなどのアプリケーションコンポーネントを簡単に交換できます。
適用対象
次の場合は、六角形アーキテクチャパターンを使用します。
-
アプリケーションアーキテクチャを切り離して、完全にテストできるコンポーネントを作成する必要があります。
-
複数のタイプのクライアントが同じドメインロジックを使用できます。
-
UI とデータベースコンポーネントには、アプリケーションロジックに影響を与えない定期的なテクノロジー更新が必要です。
-
アプリケーションには複数の入力プロバイダーと出力コンシューマーが必要であり、アプリケーションロジックをカスタマイズすると、コードが複雑になり、拡張性が不足します。
問題点と考慮事項
-
ドメイン駆動型設計: 六角形アーキテクチャは、ドメイン駆動型設計 (DDD) で特にうまく機能します。各アプリケーションコンポーネントは DDD のサブドメインを表し、六角形アーキテクチャを使用してアプリケーションコンポーネント間の疎結合を実現できます。
-
テスト可能性: 設計上、六角形アーキテクチャは入力と出力に抽象化を使用します。したがって、本質的に疎結合であるため、単体テストの作成と単独テストが容易になります。
-
複雑さ: ビジネスロジックをインフラストラクチャコードから分離する複雑さは、慎重に処理すると、俊敏性、テストカバレッジ、テクノロジーの適応性などの大きな利点をもたらす可能性があります。そうしないと、解決が複雑になる可能性があります。
-
メンテナンスオーバーヘッド: アーキテクチャをプラグイン可能にする追加のアダプターコードは、アプリケーションコンポーネントが複数の入力ソースと出力先への書き込みを必要とする場合、または入力と出力データストアが時間の経過とともに変更される必要がある場合にのみ正当です。それ以外の場合、アダプターはメンテナンスする別の追加レイヤーになり、メンテナンスオーバーヘッドが発生します。
-
レイテンシーの問題: ポートとアダプターを使用すると別のレイヤーが追加され、レイテンシーが発生する可能性があります。
実装
六角形アーキテクチャは、インフラストラクチャコードと、アプリケーションを UIs、外部 APIs、メッセージブローカーと統合するコードから、アプリケーションとビジネスロジックを分離することをサポートします。ポートやアダプターを使用して、ビジネスロジックコンポーネントをアプリケーションアーキテクチャの他のコンポーネント (データベースなど) に簡単に接続できます。
ポートは、アプリケーションコンポーネントへのテクノロジーに依存しないエントリポイントです。これらのカスタムインターフェイスは、インターフェイスを実装するユーザーや内容に関係なく、外部アクターがアプリケーションコンポーネントと通信できるようにするインターフェイスを決定します。これは、USB ポートが USB アダプターを使用している限り、さまざまなタイプのデバイスがコンピュータと通信できるようにする方法に似ています。
アダプターは、特定のテクノロジーを使用してポートを介してアプリケーションとやり取りします。アダプターは、これらのポートに接続し、ポートからデータを受信またはポートにデータを提供し、さらに処理するためにデータを変換します。例えば、REST アダプターを使用すると、アクターは REST API を介してアプリケーションコンポーネントと通信できます。ポートには、ポートやアプリケーションコンポーネントのリスクなしに、複数のアダプターを含めることができます。前の例を拡張するために、同じポートに GraphQL アダプターを追加すると、アクターは REST API、ポート、またはアプリケーションに影響を与えることなく、GraphQL API を介してアプリケーションとやり取りするための追加の手段が提供されます。
ポートはアプリケーションに接続し、アダプターは外部への接続として機能します。ポートを使用して疎結合のアプリケーションコンポーネントを作成し、アダプターを変更することで依存コンポーネントを交換できます。これにより、アプリケーションコンポーネントはコンテキストを認識することなく、外部入出力とやり取りできます。コンポーネントは任意のレベルで交換できるため、自動テストが容易になります。テストを実行するために環境全体をプロビジョニングするのではなく、インフラストラクチャコードに依存することなく、コンポーネントを個別にテストできます。アプリケーションロジックは外部要因に依存しないため、テストが簡素化され、依存関係を模倣することが容易になります。
例えば、疎結合アーキテクチャでは、アプリケーションコンポーネントはデータストアの詳細を知らなくてもデータを読み書きできる必要があります。アプリケーションコンポーネントの責任は、インターフェイス (ポート) にデータを提供することです。アダプターは、データストアへの書き込みロジックを定義します。データストアは、アプリケーションのニーズに応じて、データベース、ファイルシステム、または Amazon S3 などのオブジェクトストレージシステムです。
高レベルのアーキテクチャ
アプリケーションまたはアプリケーションコンポーネントには、コアビジネスロジックが含まれています。次の図に示すように、ポートからコマンドまたはクエリを受け取り、ポートを介して外部アクターにリクエストを送信します。外部アクターはアダプターを介して実装されます。

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

「サンプルコード」
このセクションのサンプルコードは、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-model-sample
関連情報
-
を使用した進化アーキテクチャの開発 AWS Lambda
(日本語AWS ブログ記事)
動画
次の動画 (日本語) では、Lambda 関数を使用したドメインモデルの実装における六角形アーキテクチャの使用について説明します。