go语言过渡到DDD(五)

1,097 阅读9分钟

这是我参与2022首次更文挑战的第23天,活动详情查看:2022首次更文挑战

本文为译文,原文链接:www.calhoun.io/moving-towa…

接上文,我们接着聊聊DDD。

如果我们将代码耦合到sql实现,这可能很难实现,但因为我们的大部分代码耦合到domain.UserService,我们可以写一个新的实现,然后使用它。

package userapi


type UserStore struct {
  HTTPClient *http.Client
}

func (us *UserStore) Create(newUser domain.NewUser) (*domain.User, domain.RememberToken, error) {
  // interact with a third party API instead of a local SQL database
}

// ...

更一般地说,耦合到一个domain域而不是一个特定的实现,可以让我们不再担心细节,比如:

  • 我们是在与微服务还是本地数据库进行交互? 无论我们的用户管理系统是本地SQL数据库还是微服务,我们都可以编写带有合理超时的代码。
  • 我们是否通过JSON、GraphQL、gRPC或其他方式与用户API通信? 虽然我们的实现将需要知道如何与用户API通信,但我们的代码的其余部分将继续执行相同的操作,无论我们使用的是哪种特定技术。
  • 还有很多...

关键是,我认为这是领域驱动设计的主要好处。它不是花哨的术语,丰富多彩的图形,或在你的同龄人面前看起来很聪明。它纯粹是关于设计能够不断发展以满足您不断变化的需求的软件。

为什么我们不直接从这里开始呢?

在这一点上,显而易见的后续问题是,“如果领域驱动设计这么好,为什么我们不从它开始呢?”

任何有使用模型-视图-控制器(MVC)经验的人都会告诉你,它容易受到紧密耦合的影响。几乎我们的所有应用程序都需要依赖于我们的模型,我们刚刚探讨了这是如何产生问题的。所以带来什么了呢?

尽管从公共领域构建可能很有用,但如果被误用,它也可能是一场噩梦。领域驱动设计有一个相当陡峭的学习曲线;这并不是因为这些想法特别难以理解,而是因为在项目发展到一个合理的规模之前,你很少会知道你在应用它们时哪里出了错。因此,你可能要花上几年的时间才能真正开始掌握所有相关的动态。我写软件已经有一段时间了,但是我仍然觉得我没有完全掌握所有可能出错或变得复杂的方法。

注意:这是我花了这么长时间才发表这篇文章的主要原因之一。在很多方面,我仍然不觉得自己是这方面的专家,所以我不太愿意与大家分享。我最终决定分享这篇文章,因为我相信别人可以从我有限的理解中学到东西,而且我相信随着我与其他开发人员的讨论,这篇文章会随着时间的推移而发展和完善。所以请随时联系我们来讨论这个问题——jon@calhoun.io

MVC为你组织代码提供了一个合理的起点。数据库交互在这里(模型),http处理程序在这里(控制器),呈现代码在这里(视图)。它可能会导致紧耦合,但它允许您快速开始。

与MVC不同,域驱动设计并没有为您提供一个合理的起点来组织您的代码。事实上,从DDD开始与从MVC开始是完全相反的——而不是直接开始构建控制器并查看模型如何发展,相反,您必须提前花费大量的时间来决定您的领域应该是什么。这可能涉及到模拟一些想法,并让同行审查它们,讨论哪些是正确的,哪些是错误的,几个迭代周期,然后才能深入编写一些代码。你可以在本·约翰逊的WTF中看到这一点。

这并不是一件特别糟糕的事情,但它也不容易做好,需要大量的前期工作。因此,我经常发现,如果你有一个更大的团队,其中每个人都需要在开发开始前就一些公共领域达成一致,那么这种方法会更有效。

由于我经常在较小的团队(或自己)中编写代码,我发现如果我从简单的东西开始,我的项目会发展得更自然。也许它是扁平结构,也许它是mvc结构,也许它完全是别的东西。只要我对代码的发展持开放态度,我就不会太在意这些细节。这允许它最终采取类似DDD的形式,但它不要求我从那里开始。正如我之前所说的,在一个大型组织中,每个人都在一起开发相同的应用程序,这可能更难做到,所以需要更多的前期设计讨论。

在我们的示例应用程序中,我们做了一些非常类似于“让它发展”概念的事情。每一步都是为了一个特定的目的;我们添加了一个UserService接口,因为我们需要测试我们的身份验证中间件。当我们开始从GitHub迁移到GitLab时,我们意识到我们的界面是不够的,所以我们探索了其他选择。在这一点上,我认为更多的DDD方法开始有意义,而不是猜测用户和UserService应该是什么样子,我们有真正的实现作为它的基础。

以DDD开始的另一个潜在问题是,类型可能定义得很差,因为我们经常在有具体的用例之前定义它们。例如,我们可能会决定这样验证一个用户:

type UserAuthenticator interface {
	Authenticate(email, pw string) (error)
}

直到后来,我们才可能意识到,在实践中,每次验证用户时,我们都希望返回用户(或者可能是一个记住的令牌),而通过预先定义这个接口,我们错过了这个细节。现在,我们需要引入第二个方法来检索该信息,或者我们需要更改UserAuthenticator类型并重构任何实现或利用该类型的代码。

同样的道理也适用于你的模型。在实际实现github和gitlab包之前我们可能认为我们需要用户的唯一标识信息模型是一个电子邮件字段,但我们会通过实现这些服务学习,一个电子邮件地址可以改变,我们也需要一个惟一标识用户的ID字段。

在使用域模型之前定义它是一个挑战。我们几乎不可能知道我们需要什么信息,或者不需要什么信息,除非我们已经非常了解我们所从事的领域。是的,这可能意味着我们必须稍后重构代码,但是这样做要比重构整个代码库容易得多,因为您错误地定义了域。这是我不介意从紧密耦合的代码开始,然后再进行重构的另一个原因。

最后,并不是所有代码都需要这种解耦,它并不总是提供它所承诺的好处,而且在某些情况下(如db),我们很少利用这种解耦。

对于一个没有发展的项目,您可能不需要花费所有的时间去解耦代码。如果代码没有演进,那么发生更改的可能性就会大大降低,为更改所做的额外准备工作可能只是白费力气。

此外,解耦并不总是提供它所承诺的好处,我们也不总是利用这种解耦。正如Mat Ryer喜欢指出的,我们很少只是交换我们的数据库实现。即使我们把所有的东西都解耦了,即使我们碰巧是在转换数据库的少数应用程序中,这种转换通常需要我们对如何与我们的数据存储交互进行彻底的重新思考;毕竟,NoSQL数据库的行为与SQL数据库完全不同,要真正利用这两者,我们必须编写特定于所使用的数据库的代码。最终的结果是,这些抽象并不总是为我们提供我们想要的神奇的、“实现无关紧要”的结果。

这并不意味着DDD不能带来好处,但它确实意味着我们不应该简单地喝下Kool-Aid然后期待神奇的结果。我们得停下来,自己想想。

总结

在本文中,我们首先查看了代码紧密耦合时遇到的问题,并探讨了如何定义域类型和接口来帮助改善这种耦合。我们还讨论了一些原因,为什么从这种解耦设计开始可能不是最好的主意,而是让我们的代码随着时间发展。

在本系列的下一篇文章中,我希望扩展使用域驱动设计编写Go代码的想法。具体来说,我想讨论:

  • 接口测试如何帮助确保可以毫无问题地交换实现。
  • 子域如何也可以源于不同的上下文。
  • 您可以使用传统的DDD六边形来可视化这一切,以及像第三方库这样的代码如何适合这个等式。

我还想指出,本文绝不是一组硬性规则。这只是我微薄的尝试,分享一些见解和想法,帮助我提高我的go软件开发能力。

我也不是第一个讨论或探索go中DDD和设计模式的人。你一定要看看下面的一些更全面的理解:

github.com/marcusolsso… 这是一个GitHub仓库,连同一篇文章和一个谈话,Marcus Olsen探索移植传统DDD Java应用程序到Go的示例。 medium.com/wtf-dial 我已经链接到Ben的Standard package Layout文章;本将在本系列中应用他在那篇文章中讨论的内容。我也建议你看看附带的PRs,并仔细阅读评论。 www.youtube.com/watch?v=oL6… 在这次的演讲中,Kat列举了许多构建Go应用的方法。也可以看看本次演讲的回购和幻灯片。 www.ardanlabs.com/blog/2017/0… 虽然不是专门讨论应用程序的结构,但本系列讨论了与结构紧密联系的包设计。