[翻译]在 Go 应用中使用简明架构(1)

原文在此,很长,好文,不解释。不快点翻译,就翻译不完了。

—————-翻译分隔线—————-

在 Go 应用中使用简明架构(1)

关于这篇文章

我想通过展示如何将 Bob 大叔的简明架构使用到 Go 应用,来向这个概念做一些贡献。这里并未对 Bob 大叔的博文进行过多的修改,因此阅读他的文章是理解我的内容的先决条件。

其中,他主要描述了依赖原则,也就是软件的不同部分组织成环的形式一个套一个的应用到架构中。“……也就是说代码的依赖应当是内敛的。内环对外环的一切都一无所知。尤其是那些定义在外环的名字,不应当在内环的代码中出现。包括函数、类、变量或任何命名的软件模型。”

我认为,依赖原则是构建可对框架、UI或数据库进行局部测试并解藕的软件系统的最为重要的条件。当遵循这个条件时,将得到一个有着明确关注分离的低耦合系统。

解藕系统

一个可以局部进行测试并且具有较低耦合的系统保证了在扩容时不会有灾难发生,也就是说,系统应当容易理解、可修改、可扩展,并且可伸缩。我将尽可能的展示是在怎么样的一个场景下使用依赖原则。

为此,我将会带领你创建一个简单且完整的 Go 应用,并对什么时候、如何应用、为什么要应用简明架构的概念的原因进行论证。

这个应用非常(非常!)简单,只是通过 HTTP web 服务器访问允许向订单添加物品的在线购物系统,并可以浏览订单中的物品。

为了让代码简单明了,其他用例,如浏览商城、确认、付款并未被实现。同样,我集中精力实现那些对讨论架构有帮助的代码上——因此,代码缺少许多其他保证,例如,缺少许多在一个像样的应用所必须有的错误处理。它同样也有许多冗余——显而易见是代码风格问题,但这样允许从上到下的阅读代码而无需为了降低冗余而导致的各种理解困难。

例程的架构

现在先从了解我们软件的不同部分开始,以及确定它们在架构中的位置。软件的架构将被分隔到四个层次:领域层、用例层、接口层和基础层。我们将从最内层开始,在比较高的视角来分析每一层。然后同样由内而外的通过实际实现的代码来了解每层的具体细节。

购物系统的领域层,或称作业务逻辑层与日常的购买行为一致,或者说是更加普遍的情况,客户添加购物条目到订单。我们需要实现整个业务并且在最内部的领域层用代码来实现这些规则。

将什么放在哪,以及为什么

我将选择讨论客户,而不是用户。虽然我们的应用自然而然是需要用户的,但讨论应用的领域问题时这不重要。我相信如果真得严肃面对解藕,就必须对讨论将什么东西放到哪一层去非常谨慎——“用户”是关于用例的一个概念,而无关业务本身。

作为软件开发者,我们通常都从以软件为中心的视角去看待问题,这并不出奇。然而,如果问题是关于架构的时候,一旦我有这样的感觉,那么自己可能已经落入了一个由恶心的精妙设计带来的、在相当长的时期内会导致巨大问题的陷阱中,这时我会尝试某些不涉及计算机的比喻。举个例子,如果业务领域不是软件程序的一部分,而是盘国际象棋桌游会如何呢?

设想一下,像一盘国际象棋桌游那样的来实现 eBay 或 Amazon——那么事情的核心就是关于实现,而与使用计算机应用还是一盘象棋桌游来应用到这个业务领域无关。

网站 eBay 和象棋桌游 eBay 都需要买家,卖家,商品和竞拍——但是只有网站 eBay 需要用户、会话、cookie、登录/退出等等。

我将此做了细致的区分是因为,当你的程序还很小时,不论是用户还是客户都可能说的是一件事情,这不是什么大问题。在很久很久以后为了纠正这个错误而带来的痛苦才会显现。原因在于 99% 的应用功能都需要被实现,用户和客户可以用相同的方式来对待——但是用相同的方式来对带并不意味着他们相同,一旦达到剩下的 1% 显现的时候,他们的差别就自然而然的显现出来了。我们的应用将对此加以展示。

因此,当订单和商品属于领域层时,用户作为应用的一个概念,应该属于其下一层:用例层。

还有什么应当属于用例层呢?在软件中的用例层是实现用例的地方,用户在这里使用软件来对底层的模型“做”某些实际的事情。用例的一个例子如“客户添加商品到订单”。 将业务模型放入某些操作中的方法是实现这些用例所必须的。

当然这个也可以实现在领域层,但我强烈反对这么做。原因在于用例是应用特有的,而领域模型不是。设想我们商城的两个程序,一个允许客户借或购买,而另一个是由管理员使用来管理和完成订单。虽然领域模型模型在两个应用中是相同的,但是用例确完全不同,例如:“添加商品到订单”和“标记订单已经发货”。领域和用例层都是来自应用的核心,描绘了我们操作的业务的实际情况。其他的东西实现了与业务本质无关的细节。我们的商城可能作为一个网站或独立的 GUI 应用实现——只要不修改领域模型或应用的用例,否则总是一个很类似的商城,这是商业头脑。

我们可能将 Web 服务的实现从 HTTP 迁移到 SPDY,或者将数据库从 MySQL 迁移到 Oracle——这没有改变这是一个商城的事实:客户有记录了商品的订单(领域模型),并且允许客户设定订单、修改数量和付款(用例)。

同时,对于内部的层次来说有一个严峻的考验,就是从 MySQL 切换到 Oracle(或普通文本)时,是否不得不在用例或领域层对某一行代码进行修改呢?

如果答案是肯定的,那么我们就违反了依赖原则,因为这至少在一个地方让内部的层次依赖于外部的层次。

不过当然啦,总有一个地方的代码需要访问数据库或处理 HTTP 请求。在这个应用中与外部设施,如 web 或 数据库服务器进行交互的功能,放在接口层中。

例如,如果商城作为一个网站来使用,由于处理 HTTP 请求的控制器作为接口在 HTTP 服务器和应用层之间,所以就将其放在接口层中。通过外部如 HTTP 请求、鼠标在 GUI 界面上点击,或者远程调用触发的各种事件来让商城运作。没有这些,在用例层的方法和领域层的模型只能“呆在那”,什么都做不了。不过由于在内部层次的这些元素无法与外部交互,甚至都不知道外面的情况,所以需要用接口向内部层次传递外部世界的事件。

如果我们想要将商城的数据,如商品、订单和用户保存到数据库中,同样也需要一个数据库的接口。这是依赖原则在实际应用时,最有趣的地方:如果底层 SQL 语句的代码放在接口层中,而应用层不允许调用外部层次的任何东西,但是持久化领域模型又发生在用例层——那么又如何保证不打破依赖原则呢?在展示代码的时候将会详细了解其中的细节。

最后一个层次是基础层。决定什么东西放到接口层,什么东西放到基础层并不总是那么明了的。对于我来说可以这样决策:含有与外部环境交互的代码,如与数据库交互,同时是程序特有的作为接口存在。基础层代码没有这些东西,并且可以用在完全不同的应用中。例如,某个函数是用来处理仅在自己的应用中存在的 Web 服务的 HTTP 请求的,而来自 Go 标准库的 net/http 是更加通用的代码,可以用来在任何应用中创建 Web 服务。在这种情况下,Go 标准库的大部分(概念上)位于基础层中。

让我们用一个列表来总结一下所有的层次和在我们软件中的地位:

领域:

  • 客户实体
  • 商品实体
  • 订单实体

用例:

  • 用户实体
  • 用例:添加商品到订单
  • 用例:从订单获得商品
  • 用例:管理员添加商品到订单

接口

  • 商品/订单处理的 Web 服务
  • 持久化用例和领域模型的存储区

基础

  • 数据库
  • HTTP 服务器
  • Go 标准库(net/http, …)

你已经看到了,这个列表中的有些内容还没有进行讨论,管理用例和存储区将会在实现细节的时候加以解释。

在深入代码之前,还有一件事情要明白。了解了如何分解应用后,就会得到一个模式。如果了解多个层次,并将它们与代码和业务特性相关的东西隔离后,模式就会更加清晰:

基础 接口 用例 领域

应用无关 应用相关 应用相关 应用无关
业务无关 业务无关 业务相关 业务相关

越靠左,代码越底层(“将这个字节在 80 端口上传输……”);越靠右,越高层(“添加商品到订单……”)。

—————-翻译分隔线—————-

最近太忙了,未完待续……

Join the Conversation

6 Comments

  1. 感觉board game翻译为桌游比较合适一点,国际象棋没有买家卖家吧。。。

Leave a comment

Your email address will not be published. Required fields are marked *