

本文為英文版的機器翻譯版本，如內容有任何歧義或不一致之處，概以英文版為準。

# 六角形架構模式
<a name="hexagonal-architecture"></a>

## 意圖
<a name="hexagonal-architecture-intent"></a>

Alistair Cockburn 醫生於 2005 年提出六邊形架構模式，也稱為連接埠和轉接器模式。它旨在建立鬆耦合的架構，其中應用程式元件可以獨立測試，而不需要依賴資料存放區或使用者介面 UIs)。此模式有助於防止資料存放區和 UIs 的技術鎖定。這可讓您更輕鬆地隨著時間變更技術堆疊，而對商業邏輯的影響有限或沒有影響。在此鬆耦合架構中，應用程式會透過稱為*連接埠*的界面與外部元件通訊，並使用*轉接器*來翻譯與這些元件的技術交換。

## 動機
<a name="hexagonal-architecture-motivation"></a>

六邊形架構模式用於隔離業務邏輯 （網域邏輯） 與相關基礎設施程式碼，例如存取資料庫或外部 APIs程式碼。此模式適用於為需要與外部服務整合的 AWS Lambda 函數建立鬆散耦合的商業邏輯和基礎設施程式碼。在傳統架構中，常見的做法是將商業邏輯嵌入資料庫層，做為預存程序和使用者介面。此實務以及在商業邏輯中使用 UI 特定的建構，會導致密切結合的架構，在資料庫遷移和使用者體驗 (UX) 現代化工作中造成瓶頸。六邊形架構模式可讓您根據用途而非技術來設計系統和應用程式。此策略可讓您輕鬆交換應用程式元件，例如資料庫、UX 和服務元件。

## 適用性
<a name="hexagonal-architecture-applicability"></a>

在下列情況下使用六邊形架構模式：
+ 您想要解耦應用程式架構，以建立可完整測試的元件。
+ 多種類型的用戶端可以使用相同的網域邏輯。
+ 您的 UI 和資料庫元件需要不會影響應用程式邏輯的定期技術重新整理。
+ 您的應用程式需要多個輸入提供者和輸出消費者，而自訂應用程式邏輯會導致程式碼複雜性和缺乏可擴展性。

## 問題和考量
<a name="hexagonal-architecture-issues"></a>
+ **網域驅動型設計**： 六角形架構特別適用於網域驅動型設計 (DDD)。每個應用程式元件都代表 DDD 中的子網域，而六邊形架構可用於實現應用程式元件之間的鬆散耦合。
+ 可**測試性**：根據設計，六邊形架構使用抽象處理輸入和輸出。因此，由於固有的鬆耦合，撰寫單元測試和獨立測試變得更容易。
+ **複雜性**：謹慎處理時，將商業邏輯與基礎設施程式碼分開的複雜性，可以帶來許多好處，例如敏捷性、測試涵蓋範圍和技術適應性。否則，問題可能會變得複雜，無法解決。
+ **維護開銷**：只有在應用程式元件需要多個輸入來源和輸出目的地才能寫入，或輸入和輸出資料存放區必須隨時間變更時，才能合理化讓架構可插入** **的額外轉接器程式碼。否則，轉接器會成為要維護的另一層，這會導致維護開銷。
+ **延遲問題**：使用連接埠和轉接器會新增另一層，這可能會導致延遲。

## 實作
<a name="hexagonal-architecture-implementation"></a>

六角架構支援隔離應用程式和商業邏輯與基礎設施程式碼，以及整合應用程式與 UIs、外部 APIs、資料庫和訊息中介裝置的程式碼。您可以透過連接埠和轉接器，輕鬆將商業邏輯元件連接到應用程式架構中的其他元件 （例如資料庫）。

連接埠是與技術無關的應用程式元件進入點。這些自訂界面會決定允許外部執行者與應用程式元件通訊的界面，無論介面的實作者是誰或什麼。這類似於 USB 連接埠允許許多不同類型的裝置與電腦通訊的方式，只要它們使用 USB 轉接器。

轉接器會使用特定技術，透過連接埠與應用程式互動。轉接器會插入這些連接埠、接收資料或將資料提供給連接埠，以及轉換資料以進行進一步處理。例如，REST 轉接器可讓演員透過 REST API 與應用程式元件通訊。連接埠可以有多個轉接器，而不會對連接埠或應用程式元件造成任何風險。若要延伸先前的範例，將 GraphQL 轉接器新增至相同的連接埠，可讓演員透過 GraphQL API 與應用程式互動，而不會影響 REST API、連接埠或應用程式。

連接埠會連線至應用程式，而轉接器可做為外部世界的連線。您可以使用連接埠來建立鬆耦合的應用程式元件，並透過變更轉接器來交換相依元件。這可讓應用程式元件與外部輸入和輸出互動，而不需要具備任何內容感知。元件可在任何層級交換，這有助於自動化測試。您可以獨立測試元件，而不需要依賴基礎設施程式碼，而不需要佈建整個環境來執行測試。應用程式邏輯不依賴外部因素，因此測試會簡化，而且更容易模擬相依性。

例如，在鬆耦合的架構中，應用程式元件應該能夠在不知道資料存放區詳細資訊的情況下讀取和寫入資料。應用程式元件的責任是將資料提供給 界面 （連接埠）。轉接器定義寫入資料存放區的邏輯，可以是資料庫、檔案系統或物件儲存系統，例如 Amazon S3，視應用程式的需求而定。

### 高層級架構
<a name="hexagonal-architecture-high-level-arch"></a>

應用程式或應用程式元件包含核心商業邏輯。它會從連接埠接收命令或查詢，並透過連接埠將請求傳送至外部演員，這些透過轉接器實作，如下圖所示。

![\[六角形架構模式\]](http://docs.aws.amazon.com/zh_tw/prescriptive-guidance/latest/cloud-design-patterns/images/hexagonal-1.png)


### 使用 實作 AWS 服務
<a name="hexagonal-architecture-aws-services"></a>

AWS Lambda 函數通常同時包含商業邏輯和資料庫整合程式碼，這些程式碼會緊密耦合以符合目標。您可以使用六邊形架構模式，將商業邏輯與基礎設施程式碼分開。此區隔可讓單元測試商業邏輯，而不需依賴資料庫程式碼，並改善開發程序的敏捷性。

在下列架構中，Lambda 函數會實作六邊形架構模式。Lambda 函數是由 Amazon API Gateway REST API 啟動。函數實作商業邏輯，並將資料寫入 DynamoDB 資料表。

![\[在 上實作六邊形架構模式 AWS\]](http://docs.aws.amazon.com/zh_tw/prescriptive-guidance/latest/cloud-design-patterns/images/hexagonal-2.png)


### 範本程式碼
<a name="hexagonal-architecture-sample-code"></a>

本節中的範例程式碼說明如何使用 Lambda 實作網域模型、將其與基礎設施程式碼 （例如存取 DynamoDB 的程式碼） 分開，以及實作函數的單元測試。

#### 網域模型
<a name="hexagonal-architecture-domain-model"></a>

網域模型類別不知道外部元件或相依性，它只會實作商業邏輯。在下列範例中， 類別`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:
    #.....
```

#### 輸入連接埠
<a name="hexagonal-architecture-input-port"></a>

`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 轉接器類別
<a name="hexagonal-architecture-adapter-class"></a>

`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
        }),
    }
```

#### 單元測試
<a name="hexagonal-architecture-unit-testing"></a>

您可以插入模擬類別來測試網域模型類別的商業邏輯。下列範例提供網域模型`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 儲存庫
<a name="hexagonal-architecture-repo"></a>

如需此模式範例架構的完整實作，請參閱 GitHub 儲存庫，網址為 https：//[https://github.com/aws-samples/aws-lambda-domain-model-sample](https://github.com/aws-samples/aws-lambda-domain-model-sample)。

## 相關內容
<a name="hexagonal-architecture-resources"></a>
+ [六角形架構](https://alistair.cockburn.us/hexagonal-architecture/)，作者：Alistair Cockburn
+ [使用 開發進化架構 AWS Lambda](https://aws.amazon.com/jp/blogs/news/developing-evolutionary-architecture-with-aws-lambda/)（日文AWS 部落格文章）

## 影片
<a name="hexagonal-architecture-videos"></a>

以下影片 （日文） 討論使用 Lambda 函數在實作網域模型時使用六邊形架構。


