本文由 简悦SimpRead 转码,原文地址 codewithandrea.com
在为med...... 选择项目结构时,功能优先和层优先方法的概述
在构建大型Flutter应用程序时,我们首先应该决定的是如何我们的项目结构。
这可以确保整个团队可以遵循一个明确的惯例,并以一致的方式添加功能。
因此,在这篇文章中,我们将探讨两种常见的项目结构化方法。"功能优先"和 "层级优先"。
我们将了解它们的差异,并确定在现实世界的应用程序中实施它们时常见的陷阱。最后,我们将为你提供一个清晰的步骤指南,指导你如何构建你的项目,避免在这个过程中出现代价高昂的错误。
项目结构与应用架构的关系
在实践中,我们只有在决定使用何种应用架构后才能选择项目结构。
这是因为应用架构迫使我们定义具有明确边界的独立层。而这些层会在我们的项目中以文件夹的形式显示出来。
所以在本文的其余部分,我们将使用我的Riverpod App Architecture作为参考。
(使用数据层、领域层、应用层和表现层的Flutter应用程序架构。)
这个架构由四个不同的层组成,每个层都包含我们的应用程序所需要的组件。
- 呈现:部件、状态和控制器
- 应用:服务
- 领域:模型
- 数据:存储库、数据源和DTO(数据传输对象)。
当然,如果我们只是建立一个单页的应用程序,我们可以把所有的文件放在一个文件夹里,然后就可以了。😎
但是,一旦我们开始添加更多的页面,并且有各种数据模型需要处理,我们如何才能以一致的方式组织所有的文件?
在实践中,经常使用功能优先或层优先的方法。
因此,让我们更详细地探讨这两种惯例,并了解它们的权衡。
层优先(层内特征)
为了简单起见,假设我们的应用程序中只有两个功能。
如果我们采用层先的方法,我们的项目结构可能是这样的。
‣ lib
‣ src
‣ presentation
‣ feature1
‣ feature2
‣ application
‣ feature1
‣ feature2
‣ domain
‣ feature1
‣ feature2
‣ data
‣ feature1
‣ feature2
严格来说,这是一种"层内特征"方法,因为我们不直接将Dart文件放在每个层内,而是在其中创建文件夹。
通过这种方法,我们可以在每个特性文件夹中添加所有相关的Dart文件,确保它们属于正确的层(widgets和控制器属于presentation,模型属于domain,等等)。
如果我们想添加feature3,我们需要在每个层内添加一个feature3文件夹,并重复上述过程。
‣ lib
‣ src
‣ presentation
‣ feature1
‣ feature2
‣ feature3 <--
‣ application
‣ feature1
‣ feature2
‣ feature3 <-- only create this when needed
‣ domain
‣ feature1
‣ feature2
‣ feature3 <--
‣ data
‣ feature1
‣ feature2
‣ feature3 <--
Layer-first: Drawbacks
这种方法在实践中很容易使用,但随着应用程序的增长,它的扩展性并不强。
对于任何给定的功能,属于不同层的文件都是相互远离的。而这使得我们更难在单个功能上工作,因为我们必须不断地跳到项目的不同部分。
而且,如果我们决定要删除一个特征,就很容易忘记某些文件,因为它们都是按层组织的。
由于这些原因,在构建中型/大型应用程序时,功能优先的方法通常是一个更好的选择。
特征优先(特征内的层)
功能优先的方法要求我们为每一个添加到我们应用程序的新功能创建一个新的文件夹。而在这个文件夹中,我们可以将图层本身作为子文件夹添加。
使用上述相同的例子,我们将这样组织我们的项目。
‣ lib
‣ src
‣ features
‣ feature1
‣ presentation
‣ application
‣ domain
‣ data
‣ feature2
‣ presentation
‣ application
‣ domain
‣ data
我发现这种方法更符合逻辑,因为我们可以很容易地看到属于某个特征的所有文件,并按层分组。
与层优先的方法相比,有一些优势。
- 每当我们想添加一个新的特征或修改一个现有的特征时,我们可以只关注一个文件夹。
- 如果我们想删除一个特征,只有一个文件夹需要删除(如果算上相应的
tests文件夹,就是两个)。
这样看来,功能优先的方法胜券在握 🙌
然而,在现实世界中,事情并不那么容易。
共享代码怎么办?
当然,在构建真正的应用程序时,你会发现你的代码并不总是按照你的意图整齐地放在特定的文件夹里。
如果两个或更多的独立功能需要共享一些部件或模型类,怎么办?
在这种情况下,很容易出现名为 "共享 "或 "通用 "或 "工具 "的文件夹。
但是这些文件夹本身应该如何组织?如何防止它们成为各种文件的堆积地?
如果你的应用程序有20个功能,并且有一些代码只需要被其中两个共享,那么它真的应该属于一个顶级的共享文件夹吗?
如果是在5个功能之间共享呢?或者10个?
在这种情况下,没有正确或错误的答案,你必须根据具体情况作出最佳判断。
除此以外,还有一个很常见的错误,我们应该避免。
功能优先不是关于用户界面的!
当我们把注意力放在UI上时,我们很可能会把一个功能为应用程序中的一个页面或屏幕。
在为我的即将推出的Flutter课程构建电子商务应用时,我自己也犯了这个错误。
而我最终得到的是一个看起来有点像这样的项目结构。
‣ lib
‣ src
‣ features
‣ account
‣ admin
‣ checkout
‣ leave_review_page
‣ orders_list
‣ product_page
‣ products_list
‣ shopping_cart
‣ sign_in
以上所有的功能都代表了电子商务应用程序中的实际屏幕。
但是,当要把演示、应用、域和数据层放在其中时,我遇到了麻烦,因为一些模型和资源库被多个页面共享(如产品_页面和产品_列表)。
因此,我最终为服务、模型和存储库创建了顶级文件夹。
‣ lib
‣ src
‣ features
‣ account
‣ admin
‣ checkout
‣ leave_review_page
‣ orders_list
‣ product_page
‣ products_list
‣ shopping_cart
‣ sign_in
‣ models <-- should this go here?
‣ repositories <-- should this go here?
‣ services <-- should this go here?
换句话说,我对 "功能 "文件夹采用了功能优先的方法,它代表了我的整个表现层。但是我把自己逼到了一个层先的方法来处理其余的层,这以一种意想不到的方式影响了我的项目结构。
不要试图通过查看用户界面来应用功能优先的方法。这将导致一个 "不平衡 "的项目结构,以后会咬你一口。
什么是 "功能"?
所以我退一步问自己。"什么是特征"?
我意识到这不是关于用户看到的东西,而是用户做的事情。
- 认证
- 管理购物车
- 结账
- 查看所有过去的订单
- 留下评论
换句话说,特征是一个功能需求,它可以帮助用户完成一个特定的任务。
从域驱动的设计中得到一些提示,我决定围绕域层组织项目结构。
一旦我想通了这一点,一切就都水到渠成了。我最终确定了七个功能区。
‣ lib
‣ src
‣ features
‣ address
‣ application
‣ data
‣ domain
‣ presentation
‣ authentication
...
‣ cart
...
‣ checkout
...
‣ orders
...
‣ products
‣ application
‣ data
‣ domain
‣ presentation
‣ admin
‣ product_screen
‣ products_list
‣ reviews
...
请注意,在这种方法下,一个给定的特性中的代码仍然有可能依赖于不同特性中的代码。举例来说。
- 产品页面显示一个评论的列表。
- 订单页显示一些产品的信息
- 结账流程要求用户先进行验证。
但是,我们最终在所有功能中共享的文件要少得多,而且整个结构也更加平衡。
如何以正确的方式进行功能优先
总而言之,功能优先的方法让我们围绕我们的应用的功能需求来构建我们的项目。
因此,以下是如何在你自己的应用程序中正确使用这一方法。
- 从域层开始,确定模型类和操作它们的业务逻辑
- 为每一个模型(或一组模型)创建一个文件夹,这些模型属于一起的。
- 在该文件夹中,根据需要创建 "呈现"、"应用"、"域"、"数据 "子文件夹
- 在每个子文件夹中,添加所有你需要的文件
在构建Flutter应用程序时,UI代码和业务逻辑之间的比例为5:1(或更多)是非常常见的。如果你的 "presentation "文件夹最终有很多文件,不要害怕把它们归入代表较小 "子功能 "的子文件夹。
作为参考,我最终的项目结构是这样的。
‣ lib
‣ src
‣ common_widgets
‣ constants
‣ exceptions
‣ features
‣ address
‣ authentication
‣ cart
‣ checkout
‣ orders
‣ products
‣ reviews
‣ localization
‣ routing
‣ utils
即使不看诸如common_widgets、constants、exceptions、localization、routing和utils等文件夹,我们也能猜到它们都包含了真正的跨功能共享的代码,或者因为一个好的理由需要集中的代码(如本地化和路由)。
而且这些文件夹都包含相对较少的代码。
奖励:测试文件夹
直到现在我还没有谈到这个问题。但是,让test文件夹与lib文件夹遵循相同的项目结构是非常有意义的。
通过使用VSCode中的 "转到测试 "选项,这一点非常容易做到。
(从 "lib "文件夹中的任何文件转到测试的VSCode选项)
对于lib中的任何给定文件,这将在test中的相应位置创建一个test.dart文件。👍
结论
如果做得好,功能优先比层优先有很多好处。
在用它构建了一个10K LOC的中型电子商务应用之后,我相信这是一个可扩展的方法,对更大的代码库应该很有效。
当然,在构建非常大的应用程序时,我们将面临额外的限制。而且在某些时候,我们可能需要混合和匹配不同的方法,甚至将代码库分解成多个包,这些包生活在一个单一的版本中。
但是,如果我们从一开始就采用域驱动设计,我们最终会在应用程序的不同层和组件之间建立明确的界限。这将使以后的依赖关系更容易管理。
即将推出:完整的Flutter课程
我即将推出的Flutter课程将更深入地介绍Flutter应用程序架构,以及其他重要的主题,如状态管理、导航和路由以及测试。
如果您对此感兴趣,您可以查看课程页面或在此报名。