发布于 2026-01-05 3 阅读
0

洋葱建筑🧅 DEV全球展示挑战赛,由Mux呈现:展示你的项目!

洋葱建筑🧅

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

洋葱是一种美味的蔬菜,也是世界各地美食的核心食材。那么你可能会问,为什么我们要在软件工程的语境下讨论洋葱呢?洋葱架构最初由Jeffrey Palermo在一系列博客文章中提出,它指导软件工程师将业务逻辑建模为一个核心集合,而无需与外部关注点(例如数据库选择或用户界面操作方式)耦合。

洋葱建筑长什么样?或许会让你感到惊讶。

https://img.barrymcauley.co.uk/onion_architecture.jpg

它看起来很像洋葱,层层包裹着中心核心。每一层都代表着服务整体功能中的一项特定职责。就像洋葱一样,只有通过最外层的层才能到达核心,正是这种结构揭示了架构的目的——引导耦合流从外向内流向中心。

所以,就像剥洋葱一样,让我们​​一层层深入,希望一路顺利。最外层的三层与我们的业务逻辑没有直接关系,但它们自身的功能依赖于业务逻辑。这些层级经常变化,因此与我们的核心应用程序逻辑是分离的。

这些层包括:基础设施层,即我们数据库、文件系统或任何依赖的外部 Web 服务所在的层;测试层,包括单元测试、集成测试和端到端测试,用于验证我们的业务案例;以及用户界面层,即用户与我们编写的代码进行交互的层。这些层与我们的“应用程序核心”的第一层——应用程序服务层(有时也称为传输层)进行交互。在这一层中,我们通过一系列契约来定义服务的功能。

向内探索,我们来到了领域服务层。这一层承载着我们大部分的业务逻辑,它负责执行将 A 转化为 B、将输入转化为输出、将鸡蛋转化为鸡等操作。它通过与最后一层——领域模型层——交互来实现这些功能,领域模型层是我们所使用的高级数据对象的表示。

让我们通过一个例子来了解如何解决现实世界中的任务,例如处理金融交易,从而了解我们如何从外到内应用洋葱架构。

一个例子——买咖啡

基础设施层或许会让很多人感到惊讶。我们使用的数据库或外部依赖项难道不属于我们的领域模型层吗?这是一个非常值得探讨的问题,让我们进一步探究。

例如,假设我们在某个 NoSQL 数据库中对用户帐户有如下定义:

{
    "id": "some_user_id",
    "balance": 500.00,
    "currency": "GBP",
}
Enter fullscreen mode Exit fullscreen mode

因此,当我们执行查询并希望与之交互时,很自然地会创建一个模型对象来将这个 JSON 数据序列化,例如:

type Account struct {
    ID       string    `json:"id"`
    Currency string    `json:"currency"`
    Balance  float64   `json:"balance"`
}
Enter fullscreen mode Exit fullscreen mode

听起来很有道理,对吧?但是,假设团队认为 NoSQL 不够完善,而关系型数据库才是当月的潮流。那么,得益于洋葱架构的强大功能,只要新的关系型数据库结构提供了领域模型契约中定义的必要字段,应用程序的业务逻辑就无需更改,您可以继续提供用户所需的功能。这适用于应用程序交互的任何外部依赖项或数据存储,关键在于:

Externalise your dependencies and decouple them through contracts.
Enter fullscreen mode Exit fullscreen mode

现在我们有了账户的领域模型表示,接下来需要定义一个金融交易的用例,以及当我们的用户 Andre 决定购买咖啡时所涉及的步骤:

https://img.barrymcauley.co.uk/transaction.svg

如图所示,我们需要设置一个合约,允许“通用咖啡店”从安德烈的账户余额中扣除他所购买咖啡的费用。假设“通用咖啡店”将通过HTTP POST请求与我们通信,那么我们需要以下内容:

  • Andre 的用户 ID。
  • 购买咖啡所需的金额。

该请求的 JSON表示形式如下:

{
    "userID": "some_user_id",
    "amount": 100.00,
}
Enter fullscreen mode Exit fullscreen mode

此用例的名称为“向用户余额收费”,如前所述,我们在应用服务层定义这些收费方式。为了在代码中实现这一点,我们需要将其融入到我们的 HTTP 处理程序代码中:

package application

import (
    "net/http"
    "encoding/json"
)

// ChargeRequest is our representation of an incoming request.
type ChargeRequest struct {
    UserID string  `json:"userID"`
    Amount float64 `json:"amount"`
}

// ChargeUserHandler takes an incoming HTTP POST request and decodes
// the body into a ChargeRequest so that we can carry out charging
// the users balance for the cost of the transaction.
func ChargeUserHandler(w http.ResponseWriter, r *http.Request) {
    var cr ChargeRequest

    err := json.NewDecoder(r.Body).Decode(&cr)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    if cr.UserID == "" {
        http.Error(w, "user id empty", http.StatusBadRequest)
        return
    }

    // Call our domain services layer

    return
}
Enter fullscreen mode Exit fullscreen mode

太好了!我们现在已经明确了应用服务层中任何希望向用户收取交易费用的操作规范。然而,目前我们并没有对交易进行任何实际操作,因此,基于此并遵循洋葱架构的层级结构,我们需要定义域服务层。

作为工程师,让我们花点时间梳理一下我们的流程:

  1. 查看安德烈斯的账户余额,看看账户里是否有足够的资金支付交易费用。
    • 如果没有 Andre 的帐户,或者没有 Andre 的帐户,则返回一个错误信息。
  2. 现在我们有了账户信息,从安德烈的余额中扣除交易费用,并在我们的数据库中更新该余额。
    • 如果更新失败,我们需要返回一个错误信息说明情况。我们可不想让任何人白拿一杯咖啡。
  3. 现在余额已更新,请向“通用咖啡公司”返回确认信息,从而完成交易,让安德烈可以喝到他美味的咖啡。

幸运的是,我们已经定义了可以使用的 Account 域模型,因此将所有这些封装到代码中看起来会像这样:

package service

import (
    "database/sql"
    "fmt"
)

// DB interface sets out the operations allowed on our database.
type DB interface {
    StartTransaction() (*sql.Tx, error)
    GetUserAccount(tx *sql.Tx, userID string) (Account, error)
    UpdateUserAccount(tx *sql.Tx, userID string, account Account) error
}

// ChargeService carries out the business logic to charge users balances
type ChargeService struct {
    db DB
}

// ChargeUser takes a user ID and an amount then deducts that amount from the
// balance of that user, if they have enough.
func (cs *ChargeService) ChargeUser(userID string, chargeAmount float64) error {
    transaction, err := cs.db.StartTransaction()
    if err != nil {
        return fmt.Errorf("error starting transaction")
    }
    defer func() {
        _ = transaction.Rollback()
    }()

    account, err := cs.db.GetUserAccount(transaction, userID)
    if err != nil {
        return err
    }
    if account.Balance < chargeAmount {
        return fmt.Errorf("insufficient funds")
    }

    account.Balance = account.Balance - chargeAmount
    err = cs.db.UpdateUserAccount(transaction, userID, account)
    if err != nil {
        return err
    }

    err = transaction.Commit()
    if err != nil {
        return fmt.Errorf("error committing transaction")
    }

    return nil
}
Enter fullscreen mode Exit fullscreen mode

回顾我们上面列出的案例,我们可以看到,我们已经在领域服务层代码中实现了所需的业务逻辑,现在我们可以将函数调用添加到我们的处理程序中:

// ChargeUserHandler takes an incoming HTTP POST request and decodes
// the body into a ChargeRequest so that we can carry out charging
// the users balance for the cost of the transaction.
func ChargeUserHandler(w http.ResponseWriter, r *http.Request) {
    var cr ChargeRequest

    err := json.NewDecoder(r.Body).Decode(&cr)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    if cr.UserID == "" {
        http.Error(w, "user id empty", http.StatusBadRequest)
        return
    }

    // New code here
    db := DatabaseAdapter{}
    chargeService := service.ChargeService{db: db}
    err := chargeService.ChargeUser(cr.UserID, cr.Amount)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }

    return
}
Enter fullscreen mode Exit fullscreen mode

现在,如果我们部署这项服务,当安德烈决定买咖啡时,我们可以确信能够满足调查中提出的要求,并且确信我们的逻辑已经分层清晰地组织好(假设我们已经进行了适当的测试)。这是一个简单的用例,但真正的问题是:为什么?

为什么要使用洋葱架构?

但是,使用洋葱架构有哪些好处呢?

洋葱架构高度依赖于我们定义的各层之间的契约,也就是所谓的依赖倒置原则。只要我们的各层都遵循代码中设定的契约/接口,我们就可以像之前在 NoSQL 和 SQL 之争中提到的那样利用它们。一张图胜过千言万语,让我们来可视化一下:

https://img.barrymcauley.co.uk/contract.jpg

使用合约可以让每一层都设定对下一层的期望,并使其与自身所需的功能耦合。此外,只要满足与相邻层的合约义务,每一层的实现细节都可以随时重构,这使得我们更容易应对变化,并提高代码的可测试性。

然而,这种架构模式并非解决所有问题的万灵药。与所有软件问题一样,我们需要评估是否需要这种额外的抽象,因为它更适合由众多工程师共同开发的大型应用程序。作为工程师,我们需要运用批判性思维来判断它是否对当前任务整体有利。此外,定义契约/接口并严格执行它们所带来的额外复杂性,要​​求我们对这种模式有深刻的理解。如果运用得当,它将极大地提高开发效率,并显著增强应用程序的灵活性。

请在推特上告诉我你对洋葱建筑的看法,希望你喜欢这篇文章。

下次再见……

BM

你也可以在我的个人博客上阅读更多内容。

文章来源:https://dev.to/barrymcauley/onion-architecture-3fgl