Go 扁平化项目结构

Go

无需花时间试图弄清楚如何将代码分解为软件包,而是采用扁平结构的应用程序会将所有.go文件放置在一个软件包中。

myapp/
  main.go
  server.go
  user.go
  lesson.go
  course.go

进入Go时,几乎每个人都从一个平面应用程序结构开始。 Go tour中的每个程序,Gophercises中的大多数练习以及许多其他早期的Go程序都没有被分解成任何包装。取而代之的是,我们只创建几个.go文件,然后将所有代码放入相同的(通常是main)包中。

起初,这听起来很糟糕。代码会很快变得笨拙吗?如何将业务逻辑与UI渲染代码分开?我如何找到正确的源文件?毕竟,我们使用软件包的很大一部分原因是要分离关注点,同时使更容易快速地导航到正确的源文件。

有效使用平面结构

使用平面结构时,您仍应尝试遵守编码最佳实践。您将需要使用不同的.go文件分隔应用程序的不同部分:

myapp /
  main.go#阅读配置并在此处启动您的应用
  server.go#总体HTTP处理逻辑在这里
  user_handler.go#用户http处理程序逻辑在这里
  user_store.go#用户数据库逻辑在这里
  # 等等...

全局变量仍然可能成为问题,因此您应考虑将类型与方法配合使用,以使它们脱离代码:

type Server struct {
  apiClient *someapi.Client
  router *some.Router
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  s.router.ServeHTTP(w, r)
}

而且您的main()函数可能仍应在设置应用程序之外删除大多数逻辑:

//警告:此示例非常人为设计,甚至可能无法编译。

type Config struct {
  SomeAPIKey     string
  Port           string
  EnableTheThing bool
}

func main() {
  var config Config
  config.SomeAPIKey = os.Getenv("SOMEAPI_KEY")
  config.Port = os.Getenv("MYAPP_PORT")
  if config.Port == "" {
    config.Port = "3000"
  }
  config.EnableTheThing = true

  if err := run(config); err != nil {
    log.Fatal(err)
  }
}

func run(config Config) error {
  server := myapp.Server{
    APIClient: someapi.NewClient(config.SomeAPIKey),
  }
  return http.ListenAndServe(":" + config.Port, server)
}

实际上,您实际上可以将基本上是平面结构的代码全部使用在一个软件包中,并在单独的main软件包中定义命令。这将允许您使用常见的cmd子目录模式:

myapp/
  cmd/
    web/
      # package main
      main.go
    cli/
      # package main
      main.go
  # package myapp
  server.go
  user_handler.go
  user_store.go
  ...

在此示例中,您的应用程序基本上仍然是平坦的,但是您拔出了main软件包是因为您有需要-例如可能需要使用同一核心应用程序来支持两个命令。

为什么要使用扁平结构?

扁平结构的主要好处不是将所有代码都保存在一个目录中,也不是那样愚蠢的东西。这种结构的核心好处是您可以不必担心如何组织事物,而可以继续解决您打算通过应用程序解决的问题。

我绝对喜欢这个应用程序结构让我回想起PHP的日子。当我第一次学习编码时,我开始使用随机PHP文件,其逻辑与各种HTML混合在一起,这真是一团糟。我并不是在建议我们以大型应用程序的方式构建-那样会很糟糕-但是我并不担心一切都应该放在哪里,而是更加关注学习如何编写代码和解决我的特定问题。无论您是要了解应用程序的需求,您的域还是一般的编码方式,使用扁平结构都可以使您更轻松地专注于学习和构建。

这是正确的,因为我们可以不再担心诸如“这种逻辑应该去哪里?”之类的问题。因为如果我们犯了一个错误,很容易解决。如果它是一个函数,我们可以将其移动到包中的任何新源文件中。如果它是错误类型的方法,我们可以创建两个新类型并将逻辑与原始类型分开。有了这些,我们就不必担心会遇到奇怪的周期性依赖问题,因为我们只有一个软件包。

考虑平面结构的另一个重要原因是,随着应用程序复杂性的提高,结构的发展变得容易得多。当您明显可以从将代码拆分到一个单独的程序包中受益时,您通常需要做的就是将一些源文件移到一个子目录中,更改其程序包,并更新任何引用以使用新的程序包前缀。例如,如果我们有SqlUser并决定从一个单独的sql包中处理所有与数据库相关的逻辑中受益,我们将更新所有引用以现在使用sql.User将类型移动到新软件包后。我发现,像MVC这样的结构在重构方面更具挑战性,尽管并非没有其他编程语言那样困难或困难。

扁平结构对于通常太快无法创建包的初学者特别有用。我真的不能说为什么会发生这种现象,但是Go的新手喜欢创建大量的软件包,这几乎总是导致口吃(user.User),周期性依赖关系或其他一些问题。

在下一篇有关MVC的文章中,我们将探讨这种创建过多包的现象如何使MVC在Go中显得不可能的方法,尽管事实并非如此。

通过推迟创建新程序包的决定,直到我们的应用程序增长一点并更好地了解它,发芽的Gophers犯此错误的可能性就大大降低了。

这也是为什么很多人会鼓励开发人员避免过早将其代码分解到微服务中的原因-您通常没有足够的知识来真正知道应该和不应该将哪些内容分解为微服务以及抢先式微服务( I kinda希望能成为一句俗语)只会在将来带来更多工作。

平坦的结构并不全是阳光和彩虹

假装使用扁平结构没有任何不利之处,这对我来说是不诚实的,所以我们也应该讨论这些。

对于初学者来说,扁平的结构只能使您受益匪浅。它会工作一段时间(可能比您想象的更长),但是到某个时候,您的应用程序将变得足够复杂,您需要开始分解它。使用平面结构的好处是您可以推迟使用它,并且在分解时可能会更好地理解您的代码。缺点是,您将需要花一些时间进行重构,并且您可能(也许-但这很麻烦)发现自己已经重构为您想从任何地方开始的结构。

使用平面结构时,命名冲突有时也会很尴尬。例如,假设您想要在应用程序中使用Course类型,但是在数据库中表示课程的方式与在JSON中呈现课程的方式不同。一个快速的解决方案是创建两种类型,但是由于它们都在同一个包中,因此每种类型都需要使用不同的名称,并且可能最终以类似以下内容的形式出现:SqlCourseJsonCourse。这确实没什么大不了的,但是有点令人遗憾的是我们最终得到了零类型,简单地称为Course

将代码重构为新程序包也不总是那么简单。是的,这通常很容易,但是由于所有代码都在一个包中,因此您有时可能会遇到天生具有周期性的代码。例如,假设我们的课程是否具有在JSON响应中始终以crs_开头的ID,并且我们想以各种货币返回价格。我们可以创建一个JsonCourse来处理:

输入JsonCourse struct {
  ID字符串`json:“ id”`
  价格结构{
    USD字符串`json:“ usd”`
  }`json:“价格”`
}

同时,SqlCourse仅需要存储一个整数ID和一个以美分为单位的单一价格,我们可以使用各种货币对其进行格式化。

type SqlCourse struct {
  ID    int
  Price int
}

现在我们需要一种将SqlCourse转换为JsonCourse的方法,因此我们可以将其作为SqlCourse类型的方法:

func (sc SqlCourse) ToJson() (JsonCourse, error) {
  jsonCourse := JsonCourse{
    ID: fmt.Sprintf("crs_%v", sc.ID),
  }
  jsonCourse.Price.USD = Price: fmt.Sprintf("%d.%2d", sc.Price/100, sc.Price%100)
  return jsonCourse, nil
}

然后稍后我们可能需要一种方法来解析传入的JSON并将其转换为SQL等效项,因此我们将其添加到JsonCourse类型中作为另一种方法:

func (jc JsonCourse) ToSql() (SqlCourse, error) {
  var sqlCourse SqlCourse
  // JSON ID is "crs_123" and we convert to "123"
  // for SQL IDs
  id, err := strconv.Atoi(strings.TrimPrefix(jc.ID, "crs_"))
  if err != nil {
    // Note: %w is a Go 1.13 thing that I haven't really
    // tested out, so let me know if I'm using it wrong 😂
    return SqlCourse{}, fmt.Errorf("converting json course to sql: %w", err)
  }
  sqlCourse.ID = id
  // JSON price is 1.00 and we may convert to 100 cents
  sqlCourse.Price = ...
  return sqlCourse, nil
}

我们在这里采取的每一个步骤都有意义并且很有逻辑性,但是现在我们剩下两种类型,它们必须位于同一程序包中,否则它们将呈现周期性依赖性。

我发现在提前使用MVC,域驱动设计和其他应用程序结构时很少会发生此类问题,但是如果说实话,这并不是很难解决的问题。我们真正需要做的就是提取转换逻辑并将其放置在我们同时使用两种类型的位置。

func JsonCourseToSql(jsonCourse json.Course) (sql.Course, error) {
  // 在此处移动`ToSql()`功能
}

func SqlCourseToJson(sqlCourse sql.Course) (json.Course, error) {
  // 在此处移动`ToJson()`功能
}

最后,扁平的结构不时髦;如果您想摇晃甜甜的胡须并在咖啡店里向您的好友展示自己的能力,那么这可能不会为您带来加分。另一方面,如果您只是想让代码正常工作,那么这很合适。 ♂️

扁平结构适合我吗?

首先,让我提出一个一般性建议:不要试图跳到最后,以免再需要按自己的方式重构代码。它永远都行不通,而且很可能会结束这样可以做更多的工作。几乎不可能预测您的软件的未来需求,而这仅仅是我们作为开发人员尝试实现的另一种方式。

这不仅不大可能为您节省任何时间,还可能给您自己造成损害。大型企业组织需要使用更复杂的代码结构。无论是因为他们需要使用各种配置进行测试,需要坚如磐石的单元测试,还是其他原因,几乎总是有他们使用复杂结构的原因。如果您是一个学习编程的单独开发人员,或者是一个试图快速迁移的小型团队,则您的需求是不同的。在不理解他们为什么选择他们选择的结构的情况下,假装自己是一个大型组织,这很可能会使您的工作减速而不是实际帮助您。

这里的警告是,如果您知道自己在做什么,这并不总是正确的,但我发现在很多情况下它都是正确的。

这一切意味着您应该选择最适合您情况的结构。如果您不确定应用程序将要变得多么复杂或正在学习中,那么扁平结构可能是一个很好的起点。然后,一旦对应用程序需求有了更好的了解,便可以重构和/或提取软件包。这是许多开发人员喜欢忽略的一点-如果不构建应用程序,通常很难理解应如何拆分。当人们跳到微服务的速度太快时,也会出现此问题。

另一方面,如果您已经知道您的应用程序将是庞大的-也许您正在将一个大型应用程序从一个堆栈移植到另一个堆栈-那么这可能是一个不好的起点,因为您已经有很多可以使用的上下文。

其他注意事项

如果您选择尝试采用扁平结构,则需要牢记一些其他事项:

-仅仅因为您只使用一个软件包,并不意味着您应该避免最佳实践;全局变量通常是不好的,配置可能应该在main()中进行配置(或者如果使用该模式,则可能是run()),并且init()是几乎总是一个错误。
-从扁平结构开始并不会将您锁定在一个包装中。一旦清楚将代码分成单独的软件包,这将是有益的。
-您仍然可以从将代码分解为单独的源文件并使用自定义类型中受益。

免责声明-欢迎您在学习时尝试使用init()和globals之类的东西。实际上,作为一个初级开发人员,我认为使您的代码正常工作和理解它比完善结构更重要,因为您将通过编码学习更多的知识,而不是为做错事情而烦恼。编写初始的工作版本通常比使用Go最佳实践对其进行重构要困难得多,并且您更有可能理解为什么经验丰富的开发人员会在编写“不良”方式后提出他们的建议。一个类似的例子是在React中使用Redux。在没有经历过redux解决的问题的情况下,您无法真正理解它的作用。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://www.calhoun.io/flat-application-...

译文地址:https://learnku.com/go/t/48241

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!