为什么说微服务架构或单体架构仅仅是实现上的细节(二)

前言

本篇上接为什么说微服务架构或单体架构仅仅是实现上的细节(一), 主要内容为作者思想的Go语言版本的实现,篇幅较长,代码较多,有条件的推荐在电脑上对着源代码阅读,会有更加深刻的认识。

Show me the code

在这个例子中,我们将遵循简单的工作流程,其中包括: 下订单,初始化支付和模拟异步支付接收。

下订单应该是同步(synchronous)的,初始化和收到付款是异步(async)的。

下订单

在接口中没有什么特别的,只是解析 HTTP 请求并在应用层执行一个命令。

在每个代码片段的顶部,您可以查看它在存储库中的位置。
您还可以从中读取限界上下文和层,例如pkg/orders/interface/public/http/something.goorders限界上下文中的inferface层。

// pkg/orders/interfaces/public/http/orders.go

func (o ordersResource) Post(w http.ResponseWriter, r *http.Request) {
    req := PostOrderRequest{}
    if err := render.Decode(r, &req); err != nil {
        _ = render.Render(w, r, common_http.ErrBadRequest(err))
        return
    }

    cmd := application.PlaceOrderCommand{
        OrderID:   orders.ID(uuid.NewV1().String()),
        ProductID: req.ProductID,
        Address:   application.PlaceOrderCommandAddress(req.Address),
    }
    if err := o.service.PlaceOrder(cmd); err != nil {
        _ = render.Render(w, r, common_http.ErrInternal(err))
        return
    }

    w.WriteHeader(http.StatusOK)
    render.JSON(w, r, PostOrdersResponse{
        OrderID: string(cmd.OrderID),
    })
}

对于不熟悉 Go 的人: 在 Go 中,你不需要显示的实现某个接口,你只需要实现该接口所有的方法,编译器就认为你实现了这个接口。
(译注: 原文中作者使用了一些代码对Go中的接口进行了解释和说明,为了不影响阅读体验并控制篇幅,这部分内容没有放进本文,感兴趣的同学可以阅读原文查看。)

在application中的pkg/orders/application/orders.go更有趣:
首先,productsService(从 Shop 限界上下文获取产品数据) 和 paymentsService(在 Payments 限界上下文中初始化支付)的接口:

// pkg/orders/application/orders.go

type productsService interface {
    ProductByID(id orders.ProductID) (orders.Product, error)
}

type paymentsService interface {
    InitializeOrderPayment(id orders.ID, price price.Price) error
}

最后是Application,这对于单体和微服务来说是完全一样的。我们只是注入不同的 productsServicepaymentsService 实现。

在这里我们使用领域对象(domain objects)和仓储(repository)来保存数据库中的 Order (在我们的示例中,我们使用内存实现,但它也是一个细节,可以更改为任何存储方式)。

// pkg/orders/application/orders.go

type OrdersService struct {
    productsService productsService
    paymentsService paymentsService

    ordersRepository orders.Repository
}

// ...

func (s OrdersService) PlaceOrder(cmd PlaceOrderCommand) error {
    address, err := orders.NewAddress(
        cmd.Address.Name,
        cmd.Address.Street,
        cmd.Address.City,
        cmd.Address.PostCode,
        cmd.Address.Country,
    )
    if err != nil {
        return errors.Wrap(err, "invalid address")
    }

    product, err := s.productsService.ProductByID(cmd.ProductID)
    if err != nil {
        return errors.Wrap(err, "cannot get product")
    }

    newOrder, err := orders.NewOrder(cmd.OrderID, product, address)
    if err != nil {
        return errors.Wrap(err, "cannot create order")
    }

    if err := s.ordersRepository.Save(newOrder); err != nil {
        return errors.Wrap(err, "cannot save order")
    }

    if err := s.paymentsService.InitializeOrderPayment(newOrder.ID(), newOrder.Product().Price()); err != nil {
        return errors.Wrap(err, "cannot initialize payment")
    }

    log.Printf("order %s placed", cmd.OrderID)

    return nil
}

productsService实现

微服务架构

在微服务版本中,我们使用 HTTP (REST)接口来获取产品信息。我已经将 REST API 分为 private (内部)和 public (例如,通过前端访问)。

// pkg/orders/infrastructure/shop/http.go

import (
    // ...
    http_interface "github.com/ThreeDotsLabs/monolith-shop/pkg/shop/interfaces/private/http"
    // ...
)

func (h HTTPClient) ProductByID(id orders.ProductID) (orders.Product, error) {
    resp, err := http.Get(fmt.Sprintf("%s/products/%s", h.address, id))
    if err != nil {
        return orders.Product{}, errors.Wrap(err, "request to shop failed")
    }

    // ...
    productView := http_interface.ProductView{}
    if err := json.Unmarshal(b, &productView); err != nil {
        return orders.Product{}, errors.Wrapf(err, "cannot decode response: %s", b)
    }

    return OrderProductFromHTTP(productView)
}

Shop 限界上下文中的 REST endpoint如下所示:

// pkg/shop/interfaces/private/http/products.go

type productsResource struct {
    repo products_domain.Repository
}

// ...

func (p productsResource) Get(w http.ResponseWriter, r *http.Request) {
    product, err := p.repo.ByID(products_domain.ID(chi.URLParam(r, "id")))

    if err != nil {
        _ = render.Render(w, r, common_http.ErrInternal(err))
        return
    }

    render.Respond(w, r, ProductView{
        string(product.ID()),
        product.Name(),
        product.Description(),
        priceViewFromPrice(product.Price()),
    })
}

我们还有一个用于 HTTP 响应的简单类型。理论上,我们可以将 Domain 类型序列化为 JSON,但是如果我们这样做的话,每个域的更改都会改变我们的 API 协议,每个 API 协议更改的请求都会改变域。听起来不太好,不符合 DDD 和 整洁架构。

// pkg/shop/interfaces/private/http/products.go

type ProductView struct {
    ID string `json:"id"`

    Name        string `json:"name"`
    Description string `json:"description"`

    Price PriceView `json:"price"`
}

type PriceView struct {
    Cents    uint   `json:"cents"`
    Currency string `json:"currency"`
}

您可以注意到 ProductView 是在 pkg/order/infrastructure/shop/http.go(上面的示例)中导入的,因为正如我前面所说的,在不同的限界上下文之间,将interfaces导入到infrastructure是没问题的。

单体架构

在单体版本中,它非常简单: 在 Orders 限界上下文中,我们只是从 Shop 限界上下文中调用函数(intraprocess.ProductInterface:ProductByID),而不是调用 REST API。

// pkg/orders/infrastructure/shop/intraprocess.go

import (
    "github.com/ThreeDotsLabs/monolith-shop/pkg/orders/domain/orders"
    "github.com/ThreeDotsLabs/monolith-shop/pkg/shop/interfaces/private/intraprocess"
)

type IntraprocessService struct {
    intraprocessInterface intraprocess.ProductInterface
}

func NewIntraprocessService(intraprocessInterface intraprocess.ProductInterface) IntraprocessService {
    return IntraprocessService{intraprocessInterface}
}

func (i IntraprocessService) ProductByID(id orders.ProductID) (orders.Product, error) {
    shopProduct, err := i.intraprocessInterface.ProductByID(string(id))
    if err != nil {
        return orders.Product{}, err
    }

    return OrderProductFromIntraprocess(shopProduct)
}

在Shop的限界上下文中:

// pkg/shop/interfaces/private/intraprocess/products.go

type ProductInterface struct {
    repo products.Repository
}

// ...

func (i ProductInterface) ProductByID(id string) (Product, error) {
    domainProduct, err := i.repo.ByID(products.ID(id))
    if err != nil {
        return Product{}, errors.Wrap(err, "cannot get product")
    }

    return ProductFromDomainProduct(*domainProduct), nil
}

您可以注意到,在 Orders 限界上下文中,我们不会导入 Shops 限界上下文之外的任何内容(正如 整洁架构 假设的那样)。所以,我们需要一些可以被导入Shops限界上下文的传输类型(transport type)。

type Product struct {
    ID          string
    Name        string
    Description string
    Price       price.Price
}

它可能看起来多余且重复,但在实践中,它有助于在限界上下文之间保持恒定的契约。例如,我们可以完全替换Application层和Domain层而不要修改这种类型。您需要记住,避免重复的成本随着规模的增加而增加。此外,数据的重复不等同于行为的重复。

您是否在微服务版本中看到类似于 ProductView 的内容?

初始化付款

在前面的示例中,我们用一个函数调用替换了 HTTP 调用,该函数调用是同步的。但是如何处理异步操作呢?看情况。在 Go 中,由于采用了并发原语,所以很容易实现。如果在你的语言中很难实现,你可以使用 Rabbit MQ。

与前面的示例一样,两个版本在应用程序和域层中看起来是相同的。

// pkg/orders/application/orders.go

type paymentsService interface {
    InitializeOrderPayment(id orders.ID, price price.Price) error
}


func (s OrdersService) PlaceOrder(cmd PlaceOrderCommand) error {
    // ..

    if err := s.paymentsService.InitializeOrderPayment(newOrder.ID(), newOrder.Product().Price()); err != nil {
        return errors.Wrap(err, "cannot initialize payment")
    }

    // ..
}
微服务

在微服务中,我们使用 RabbitMQ 发送消息:

// pkg/orders/infrastructure/payments/amqp.go

// ...

func (i AMQPService) InitializeOrderPayment(id orders.ID, price price.Price) error {
    order := payments_amqp_interface.OrderToProcessView{
        ID: string(id),
        Price: payments_amqp_interface.PriceView{
            Cents:    price.Cents(),
            Currency: price.Currency(),
        },
    }

    b, err := json.Marshal(order)
    if err != nil {
        return errors.Wrap(err, "cannot marshal order for amqp")
    }

    err = i.channel.Publish(
        "",
        i.queue.Name,
        false,
        false,
        amqp.Publishing{
            ContentType: "application/json",
            Body:        b,
        })
    if err != nil {
        return errors.Wrap(err, "cannot send order to amqp")
    }

    log.Printf("sent order %s to amqp", id)

    return nil
}

并接收消息:

// pkg/orders/interfaces/public/http/orders.go

// ...

type PaymentsInterface struct {
    conn    *amqp.Connection
    queue   amqp.Queue
    channel *amqp.Channel

    service application.PaymentsService
}

// ...

func (o PaymentsInterface) Run(ctx context.Context) error {
    // ...

    for {
        select {
        case msg := <-msgs:
            err := o.processMsg(msg)
            if err != nil {
                log.Printf("cannot process msg: %s, err: %s", msg.Body, err)
            }
        case <-done:
            return nil
        }
    }
}

func (o PaymentsInterface) processMsg(msg amqp.Delivery) error {
    orderView := OrderToProcessView{}
    err := json.Unmarshal(msg.Body, &orderView)
    if err != nil {
        log.Printf("cannot decode msg: %s, error: %s"string(msg.Body), err)
    }

    orderPrice, err := price.NewPrice(orderView.Price.Cents, orderView.Price.Currency)
    if err != nil {
        log.Printf("cannot decode price for msg %s: %s"string(msg.Body), err)

    }

    return o.service.InitializeOrderPayment(orderView.ID, orderPrice)
}
单体架构

在单体版本发送到channel,简单到不能更简单了

// pkg/orders/infrastructure/payments/intraprocess.go

type IntraprocessService struct {
    orders chan <- intraprocess.OrderToProcess
}

func NewIntraprocessService(ordersChannel chan <- intraprocess.OrderToProcess) IntraprocessService {
    return IntraprocessService{ordersChannel}
}

func (i IntraprocessService) InitializeOrderPayment(id orders.ID, price price.Price) error {
    i.orders <- intraprocess.OrderToProcess{string(id), price}
    return nil
}

以及接收(我删除了关机(关闭)支持,以免代码过于复杂) :

// pkg/payments/interfaces/intraprocess/orders.go

// ...

type PaymentsInterface struct {
    orders            <-chan OrderToProcess
    service           application.PaymentsService
    orderProcessingWg *sync.WaitGroup
    runEnded          chan struct{}
}

// ..

func (o PaymentsInterface) Run() {
    // ...

    for order := range o.orders {
        go func(orderToPay OrderToProcess) {
            // ...

            if err := o.service.InitializeOrderPayment(orderToPay.ID, orderToPay.Price); err != nil {
                log.Print("Cannot initialize payment:", err)
            }
        }(order)
    }
}

更多…

将订单标记为已支付几乎与下订单一样(REST API/函数调用)。如果你好奇它是如何工作的,请查看完整的源代码。

完整的代码可以在这里找到: https://github.com/ThreeDotsLabs/monolith-microservice-shop

我已经实现了一些验收测试,这些测试将检查所有流程对于单体和微服务的工作方式是否完全相同。测试可以在tests/acceptance_test.go中找到。

您可以在 README.md 中找到关于如何运行项目和测试的更多信息。

还有一些这里没有提到的代码,如果你想更深入地了解这些代码,请在 Twitter 上关注我(@roblaszczak)或订阅我们,当文章准备好时,我们会通知你。如果你还不知道 Go语言 的一些基本概念,你可以学习一下,而我也将使这个代码更生产级。

我们还计划撰写一些DevOps文章(Packer,Terraform,Ansible)

总结

微服务的另一个优势是什么?您可以独立地部署单体应用,但它的灵活性将会降低,您必须使用不同的流程。例如,您可以使用特性分支,其中主分支将用于生产,未发布更改的提交应该合并到这个特性分支中。这些分支应该在临时环境中使用。单体更难以扩展,这是事实,因为您必须扩展整个应用程序,而不是单个模块。但在许多情况下,这已经足够好了。在这种情况下,我们不能为每个代码库分配一个团队,但幸运的是,如果模块分离得很好,就不会有太多冲突。唯一的冲突将发生在负责跨模块通信的层中,但同样的冲突也发生在 REST/Queue API 中。在单体中,它将有编译检查,不像在微服务中,您必须验证API兼容性,需要额外的工作。此外,使用单体架构,您将收到共享类型的编译检查。模块(微服务架构中的微服务)的快速重写是一个很好的模块分离问题-如果模块被正确地分离和设计,您可以在不触碰其他任何东西的情况下替换它。

我再重复一遍: 没有什么银弹。但在大多数情况下,使用整洁单体来开始已经足够了。如果设计良好,转向微服务架构将不是什么问题。
在某些情况下(例如小型项目) ,为接口层/基础设施层编写这些额外的代码可能有些过分。但什么是小项目呢?看情况…

在开始实施之前要做好计划。没有“缺乏设计”,只有好的设计和坏的设计。事件风暴是启动项目的一个好主意。

我知道我已经介绍了很多技巧,其中很大一部分对你来说可能是新的。好消息是,要拥有好的架构,您不需要同时学习所有这些技术。同时也很难正确理解这些技术。我建议从 Clean Architecture 开始,然后查看一些 CQRS 的基础知识,然后就可以进入 DDD 了。在本文的最后,我将提供一些有用的资源,这些资源是我在学习这些技术时使用过的。

如果你有任何问题,请在 Twitter 上私信我。

延伸阅读

整洁架构

鲍勃大叔的文章:

  • https://8thlight.com/blog/uncle-bob/2011/11/22/Clean-Architecture.html
  • https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html

事件风暴

  • 这项技术的创造者的电子书,还没有完成,但已经足够让你理解这项技术,并在实践中使用它: https://leanpub.com/introducing_eventstorming

DDD

  • 很好的讲座来理解 DDD 的基础知识,对于非技术人员来说也是一个很好的方向: https://www.amazon.com/Domain-Driven-Design-Distilled-Vaughn-Vernon/dp/0134434420
  • 如果您了解 DDD 的理论基础,您可以看到如何实现它的实际例子: https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577

Go

Go是一门非常简单的语言,我建议通过以下例子来学习: https://gobyexample.com/。不管你信不信,一个晚上就足够了解Go了。

以及一些其他的内容…

  • 关于微服务(以及单体服务)的一些重要(经常被遗忘)的想法——数据: http://blog.christianposta.com/microservices/the-hardest-part-about-microservices-data/
  • 这是一篇很棒的文章,有助于理解 DDD、 Clean Architecture 和 CQRS 应该如何协同工作: https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/
  • 斯蒂芬•蒂尔科夫(Stefan Tilkov)认为,为什么我们不应该从单体开始,这说明了为什么即使我们想要构建一个单体应用,耦合也是不好的: https://martinfowler.com/articles/dont-start-monolith.html

原文链接: https://threedots.tech/post/microservices-or-monolith-its-detail/
原作者: Robert Laszczak
译者: 赵不贪


原文始发于微信公众号(梦真日记):为什么说微服务架构或单体架构仅仅是实现上的细节(二)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/167832.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!