使用Go构建商业应用: 关于DRY,你需要知道的

这篇文章是我们《使用Go构建商业应用》[1]系列中的一篇。在此之前,我们介绍了Wild Workouts,这是一个以现代方式构建的、带有一些微妙反模式(anti-patterns)的应用示例。我们特意添加了这些内容,以介绍常见的陷阱和避免这些陷阱的技巧。

在这篇文章中,我们开始对Wild Workouts进行重构。之前的文章会给你更多的背景,但阅读它们并不是理解这篇文章的必要条件。

背景故事

苏珊是一名软件工程师,她对目前的工作感到厌倦,因为她的工作是与传统的企业软件打交道。于是苏珊开始寻找新的工作,她发现了使用无服务器(serverless) Go微服务的创业公司Wild Workouts,这似乎是一些新潮的东西,因此她选择加入了Wild Workouts并开始了她在新公司的生活。

团队里只有几个工程师,所以苏珊的入职速度非常快。第一天,她就被分配了第一个任务,目的是让她熟悉这个应用程序。

我们需要存储每个用户最后登录的IP地址。它将在未来实现新的安全功能,比如从新地点登录时的额外确认。现在,我们只想把它保存在数据库中。

苏珊熟悉了一段时间应用程序,她试图了解每个服务中发生了什么,以及在哪里添加一个新字段来存储IP地址。最后,她发现了一个她可以扩展的User结构体:

 // User defines model for User.
 type User struct {
        Balance     int     `json:"balance"`
        DisplayName string  `json:"displayName"`
        Role        string  `json:"role"`
+       LastIp      string  `json:"lastIp"`
 }
 

没过多久,苏珊就发起了她的第一个PR。高级工程师Dave在代码审查时添加了一条评论:

我认为我们不应该通过REST API公开这个字段。

苏珊很惊讶,因为她确信自己更新了数据库模型。她很困惑,问Dave这是否是添加新字段的正确位置。

Dave解释说,应用程序的数据库Firestore存储由Go结构体序列化后的数据。因此User结构体与前端响应和存储都兼容。

“多亏了这种方法,你不需要重复的代码,只需更改一次YAML定义并重新生成文件就足够了。” Dave热情地说道。

感谢这个提示,现在苏珊又添加了一个更改,以在API的响应中隐藏新字段。

diff --git a/internal/users/http.go b/internal/users/http.go
index 9022e5d..cd8fbdc 100644
--- a/internal/users/http.go
+++ b/internal/users/http.go
@@ -27,5 +30,8 @@ func (h HttpServer) GetCurrentUser(w http.ResponseWriter, r *http.Request) {
        user.Role = authUser.Role
        user.DisplayName = authUser.DisplayName

+       // Don't expose the user's last IP externally
+       user.LastIp = nil
+

        render.Respond(w, r, user)
}

一行代码解决了这个问题。对苏珊来说,这是非常有成效的第一天。

深思熟虑

尽管苏珊的解决方案被批准并合并了,但在回家的路上还是有一些事情困扰着她。对于API的响应和数据库模型,使用同一个模型定义是正确的做法吗?随着应用程序不断扩展,难道不会有意外暴露用户隐私细节的风险吗?如果我们只想更改API的响应而不想更改数据库字段,该怎么办?

使用Go构建商业应用: 关于DRY,你需要知道的

把这两个结构体分开怎么样?苏珊在以前的工作中就是这么做的,但也许这些是不应该在Go微服务中使用的模式?此外,团队似乎很在意“不要重复自己”的原则。

重构

第二天,苏珊向Dave解释了她的疑虑,并征求了他的意见。起初,Dave不理解这种担忧,并告诉苏珊她可能需要习惯这种方式。

苏珊指出了Wild Workouts中的另一段代码,该代码使用了类似的特别解决方案。她分享说,根据她的经验,这样的代码很快就会失控:

user, err := h.db.GetUser(r.Context(), authUser.UUID)
if err != nil {
    httperr.InternalError("cannot-get-user", err, w, r)
    return
}

// HTTP Handler 似乎在适当的位置修改了User
user.Role = authUser.Role
user.DisplayName = authUser.DisplayName

render.Respond(w, r, user)

最终,他们同意通过一个新的PR再次讨论这个问题。不久,Susan准备了一个重构提案:

// 完整的代码在 https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/commit/14d9e7badcf5a91811059d377cfa847ec7b4592f
diff --git a/internal/users/firestore.go b/internal/users/firestore.go
index 7f3fca0..670bfaa 100644
--- a/internal/users/firestore.go
+++ b/internal/users/firestore.go
@@ -9,6 +9,13 @@ import (
        "google.golang.org/grpc/status"
 )

+type UserModel struct {
+       Balance     int
+       DisplayName string
+       Role        string
+       LastIP      string
+}
+

diff --git a/internal/users/http.go b/internal/users/http.go
index 9022e5d..372b5ca 100644
--- a/internal/users/http.go
+++ b/internal/users/http.go
@@ -1,6 +1,7 @@
-       user.Role = authUser.Role
-       user.DisplayName = authUser.DisplayName

-       render.Respond(w, r, user)

+       userResponse := User{
+               DisplayName: authUser.DisplayName,
+               Balance:     user.Balance,
+               Role:        authUser.Role,
+       }
+
+       render.Respond(w, r, userResponse)

 }

这一次,苏珊没有触及OpenAPI的yaml定义。毕竟她不应该对REST API进行任何更改。相反,她新增了另一个结构体,就像User一样,但它是数据库模型独有的。

新解决方案的代码稍多,但它消除了REST API和数据库层之间的代码耦合(所有这些改动都没有扩散到别的微服务)。下次有人想添加字段时,可以通过只更新对应的结构体来完成。

使用Go构建商业应用: 关于DRY,你需要知道的

原则上的冲突

Dave最担心的是,第二个解决方案打破了DRY原则[2](Don’t repeat yourself)并引入了额外的模板。而苏珊担心原来的方法违反了单一职责原则(SOLID中的 “S”)。究竟谁是对的?

严格遵循原则是很难的。有时,重复的代码似乎是一个模板,但它是对抗代码耦合最好工具之一。问问自己,使用同一个结构体的代码是否有可能一起改变?这会很有帮助。如果不会,就可以安全地认为重复是正确的选择。

这有什么大不了的?

这样一个小的变化也能称得上是“架构”吗?

苏珊引入了一个她不知道的后果的小变更。这对其他工程师来说是显而易见的,但对一个新人来说却不是。我猜你也知道那种不敢在一个未知的系统中引入变化的感觉,因为你无法知道它可能会引发什么。

如果你做了很多错误的决定,哪怕是很小的决定,它们往往会变得复杂。最终,开发人员开始抱怨说很难在这个应用程序上工作。转折点是当有人提到”重写”时,你知道你将面临一个大问题。

好的设计和坏的设计是相对的,没有所谓的没有设计。
——Adam Judge

在你遇到问题之前,讨论架构决策是值得的。“没有架构”只会给你留下糟糕的架构。

微服务能拯救你吗?

在微服务给我们带来好处的同时,一些“如何构建微服务”指南也提出了一个危险的想法。它们说微服务将简化您的应用程序。因为构建大型软件项目很难,有些人承诺,如果你把应用程序分成小块,你就不需要担心了。

这个想法听起来不错,但它没有抓住拆分软件的要点。你怎么知道界限在哪里?您会根据每个数据库实体来分离一个服务吗?REST Endpoint?特征?如何确保服务之间的低耦合

分布式单体

如果你的服务拆分不当,你很可能最后得到的是一个你当初想要避免的单体服务,并且额外增加了网络开销和管理这些混乱的复杂工具(也称为分布式单体)。你只是用高度耦合的服务来取代了高度耦合的模块。而且因为现在大家都在使用Kubernetes集群,你甚至可能认为你在遵循行业标准。

即使你能在一个下午重写一个服务,你能同样迅速地改变服务之间的通信方式吗?如果它们是由多个团队开发的,错误的边界呢?考虑一下,重构一个单体的应用程序是多么简单的事情。

上述所有内容都不会影响微服务的其他优点,如独立部署(对持续交付[3]至关重要)和更容易的横向扩展。像所有的模式一样,确保你在正确的时间使用正确的工具。

我们在Wild Workouts中特意引入了类似的问题。我们将在未来的文章中对此进行研究,并讨论其他拆分技术。

你可以我们之前的文章中找到一些想法: 《为什么说微服务架构或单体架构仅仅是实现上的细节》
(译者注: 上下两篇都已更新,可在微信公众号往期文章中内查看)

这些都适用于Go吗?

开源Go项目通常是低级别的应用程序和基础设施工具。在早期更是如此,但现在仍然很难找到处理领域逻辑(domain logic)的Go应用程序的好例子。

我所说的”领域逻辑”,并不是指金融应用或复杂的商业软件。如果你开发任何类型的web应用程序,那么你很有可能有一些复杂的领域案例需要以某种方式建模。

遵循一些DDD的例子会让你觉得你不是在写Go。我很清楚在Java中直接强制使用OOP模式并不是一件有趣的事情,但我认为一些语言无关的想法是值得引人深思的。

还有什么选择?

在过去的几年里,我们一直在探讨这个话题。我们非常喜欢Go的简单性,但也成功地实现了领域驱动设计和整洁架构[4]的理念。

出于某些原因,开发人员并没有因为微服务的到来而停止谈论技术债务和遗留软件。我们更喜欢以务实的方式将面向业务的模式与微服务结合使用,而不是寻找所谓的银弹。

我们还没有完成对Wild Workouts的重构。在接下来的一篇文章中,我们将看到如何将 整洁架构 引入到项目中。

原文地址: https://threedots.tech/post/things-to-know-about-dry/

参考资料

[1]

使用Go构建现代商业软件: https://threedots.tech/series/modern-business-software-in-go/

[2]

DRY原则: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself

[3]

持续交付: ContinuousDelivery

[4]

整洁架构: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html


原文始发于微信公众号(梦真日记):使用Go构建商业应用: 关于DRY,你需要知道的

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

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

(0)
小半的头像小半

相关推荐

发表回复

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