这篇文章是我们《使用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微服务中使用的模式?此外,团队似乎很在意“不要重复自己”的原则。
重构
第二天,苏珊向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和数据库层之间的代码耦合(所有这些改动都没有扩散到别的微服务)。下次有人想添加字段时,可以通过只更新对应的结构体来完成。

原则上的冲突
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/
参考资料
使用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