巴拉巴

 找回密码
 立即注册

站内搜索

搜索
热搜: 活动 交友 discuz
查看: 59|回复: 0

Java最佳代码分包方式:不要按照技术层分包,按照功能分包

[复制链接]

4

主题

9

帖子

20

积分

新手上路

Rank: 1

积分
20
发表于 2023-6-4 20:07:21 | 显示全部楼层 |阅读模式
按照功能分包,不要按照技术层分包
目前一种很流行的方法是按技术关注点进行代码分包。但是这种方法有一些缺点。相反,我们可以按功能进行分包,并创建自包含和独立的包。这样做的结果是代码库将更容易理解,也更不容易出错。
如何分包
1、长话短说

  • 按技术关注点分包的缺点:对属于一个功能的所有类没有很好的概述。倾向于通用的、可重用的和复杂的代码,这些代码很难理解,而且变更很容易破坏其他用例,因为变更的影响很难把握。
  • 取而代之的是,我们应该按照功能来创建包,每个包包含实现这个功能所需的所有类。这样做的好处是:

  • 功能更容易被发现,更容易概览全局功能;
  • 代码自包含并具备独立性
  • 更简单的代码
  • 更强的可测试性
2、按技术层分包目前一个很流行的项目结构的方式是按照层来分包。这就会形成每个技术关注点包含一组类,然后这些类组成了一个包。
按技术层(包含一组类)分包
让我们加上调用级别,然后这样可以清楚的看见哪个类依赖哪个类。
整个项目种相关包的调用级别
那按层分包有什么缺点呢?

  • 糟糕的功能概述:通常,当我们需要处理项目中的代码时,我们脑海中就会有一个需要修改的业务领域或者功能。我们是从业务领域的角度出发来思考问题的。不幸的是,按技术关注点分包迫使我们从一个包跳到另一个包,这样才能掌握一个功能的全貌。
  • 倾向于通用的、可重用的和复杂的代码:通常,这种分包方式会导致一些中心类,它们包含每个应用场景要用的所有方法。随着时间的推移,这些方法变得越来越抽象(带有额外的参数和泛型)才能满足更多的应用场景。上图中只有一个例子是ProductDAO ,ProductController 和ExportController 都调用了它的方法。这样做的结果是:

  • 当添加更多的方法时,类也会变得更大。由于代码的数量增加,理解它变得更加困难。
  • 修改通用的重用代码是危险的。尽管你只想处理一个应用场景,但你可以很容易地破坏所有应用场景。
  • 抽象和通用方法更难以理解:首先,要实现通用,通常需要额外的技术构造(if、else、switch、parameter、泛型),这使得很难看到与当前应用场景相关的业务逻辑(信噪比)。其次,认知要求更高,因为你必须了解所有其他应用场景,以确保不会破坏它们。就如sandy Metz说:
“I felt like I had to understand everything in order to help with anything.” 我觉得我必须了解一切才能帮助别人。
我们为了实现DRY(Don't Repeat Yourself,复用原则)原则而违反了KISS(Keep it Simple,Stupid。简单性原则)原则。
3、按功能分包
让我们重新组织类,将它们放入按照自包含的按功能分的包里面。
用户管理的功能包
新的包userManagement 包含所有属于这个功能的类:controller、DAO、DTO和实体。
产品管理的功能包
新的包productManagement 包含所有相同的类,并额外添加了StockServiceClient 和相应的StockDTO 。 我们可以看出:stock service只能被项目管理模块所使用。
userManagement和productManagement使用不同的业务领域实体和表。将它们拆入不同的包是非常简单的。但是当一个功能需要和其他功能类似甚至相同的业务领域实体的时候会发生什么呢?
产品出口功能包
现在的情况就变得越来越有趣了,exportProduct 包也需要使用产品的实体,但是却有不同的应用场景。
我们的目标是需要拥有自包含独立的功能包。因此,exportProduct 应该拥有自己的DAO,DTO,和实体类,就算这些类看上去和productManagement 的类很类似。我们要抵制复用productManagement 类的冲动。

  • 我们可以使用为产品出口应用场景量身定制的类(DTO、实体)的结构。它们只需要包含和当前业务相关的属性,实体可以基于适合当前业务场景的查询所需的字段来创建——仅此而已。
  • 专用的ExportProductDAO 类也只包含特定于出口的查询。
虽然我们需要写更多的代码,但是最终会得到一个很好的收益:

  • 在productManagement 模块修改代码将永远不会影响exportProduct 的代码,反过来也一样。它们彼此之间独立的演变。
  • 当修改代码的时候,我们只需要在脑中记住当前的功能就可以了。
  • 代码本身会变得更简单更容易被理解,因为这些代码不是通用的,它不需要为多种应用场景考虑。
上面按照功能分包在现实工作中是一个很棒的选择,但我们总还是需要一个common 包。
common包中包含技术配置和复用代码

  • 它包含所有的技术配置类(如,依赖注入、Spring、对象映射、http client、数据库连接、连接池、日志和线程池等)
  • 它包含可复用的小的代码片段。但是一定要注意不要过早抽象你的代码。刚开始要经常把工具代码放在最接近使用它的代码的地方,也就是在功能包的内部甚至使用它的类的内部。只有在真正(而不是我认为未来会使用)需要多次使用这个代码片段的时候,才将它移到common包里,三次原则是一个很好的指南。
Rule of Three(三次原则) 1、The first time you do something, you just do it. 1、第一次做某件事(代码)的时候,你仅仅需要做就可以了。 2、The second time you do something similar, you wince at the duplication, but you do the duplicate thing anyway. 2、第二次遇到类似的事情的时候,你对重复感到畏惧,但是你依然选择重复。 3、The third time you do something similar, you refactor. 3、第三次遇到类似的事情时,你重构。


  • 在common包中放置所有实体也有可能是有意义的。在一些项目中有时候也可以这样做,当许多功能包一次又一次地使用相同的实体。一些开发人员还喜欢将所有实体放在一个中心位置,这样就能方便从整体上看到数据库结构的映射。在这一点上,也不需要做个教条主义者,将实体的放置在这两个位置都是合理的。尽管如此,总是在开始时将尽可能多的将代码移到各自的功能包中,并依赖于定制的特定应用场景的实体。
3.1 整体最后我们整体的图片如下:
按照功能分包方式的整体图片
3.2 好处
我们来快速总结一下按功能分包的好处:

  • 功能更容易被发现,更容易概览全局功能:从业务领域的角度来看。属于业务特性的大多数代码都放在一起。这是至关重要的,因为我们在处理代码时脑中通常思考的是特定的业务需求的。
  • 代码自包含并具备独立性:特定功能所需的大部分代码都位于包中。 所以我们避免了依赖于其他功能的包。 这样我们在演进一个功能的同时不大可能破坏其他功能,并且我们需要更少的认知能力来评估代码变化的所能导致的影响。 通常,我们只需要考虑当前的包就可以。
  • 更简单的代码:由于我们避免使用通用和抽象的代码,因此代码变得更简单,代码只需要处理一个应用场景。 这样就更容易理解和改进这些代码。
  • 更强的可测试性:通常,与试图满足所有应用场景的技术包中的“神级类”(god-class)相比,功能包中的类具有更少的依赖性。 所以测试变得更容易。
3.3 缺点

  • 我们必须写更多的代码。
  • 我们可能会多次写类似的代码。
  • 决定将代码移至common 包并重用它的时机是很棘手的。 三次原则在我们有疑问时候很有帮助。 需要强调的复用仍然是允许并且是有用的。
  • 找出一个功能包的适当范围和大小也很棘手的。DDD领域驱动设计的界限上下文的理念会帮助我们来找出一个功能包的适当范围和大小。
但是,按功能分保的优势是远远大于它的缺点的。
3.4 背后的原则
按功能打包的方法遵循了一个原则:
KISS > DRY (简单性原则 > 复用原则)
Sandi Metz还说过:
“Prefer duplication over the wrong abstraction.” “与错误的抽象相比,更偏向重复。”
3.5 按功能分包的指南我们基于功能来给代码分包。每个功能包包含完成这个功能的大部分代码。每个功能包应该是自包含并且是独立的。
├── feature1 │ ├── Feature1Controller │ ├── Feature1DAO │ ├── Feature1Client │ ├── Feature1DTOs.kt │ ├── Feature1Entities.kt │ └── Feature1Configuration ├── feature2 ├── feature3 └── common

  • 这种方式影响所有层。例如,每个功能包都有自己的DAO和client。不应该有巨大的功能很多的DAO类。
  • 一个功能包与其他功能包之间应该只有很少的关系。功能所需的一切都应该放在自己的包中。
  • 经验法则:如果你想删除一个功能,你只需要删除相应的功能包。
  • 尽管如此,在common 包中复用代码也是可以的,但它应该只能包含多次被使用的代码(三次原则)。它不应应该包含业务逻辑代码,技术相关的工具类是可以的。
  • 如果存在特定于功能的Spring bean,我们还是将它们的配置放在功能包中。
4、问题 4.1 功能保内部的结构是什么样的?这取决于你的项目和功能包的大小。
对于小型和中型项目,要避免定义只能增加更多仪式感而不是价值的规则(例如,要求定义接口和子包)。 只要你是从业务领域构出发建独立且自包含的包,你就在正确的轨道上。
对于更大的代码库,你可能需要定义更多关于子包结构和规则,允许一个功能包访问另一个功能包。 “模块”或“组件”而不是“功能包”的概念可能更有帮助。 例如,Tom Hombergs建议在每个组件包中添加api 和internal 包,定义组件的哪些部分允许被其他组件使用。
你可以使用Spring官方最新发布的Spring Modulith,它提供了功能包交互的内外部结构和规则。
4.2 我会一次又一次地写同样的代码吗?
是的,会有一些重复,但根据经验,100%相同的代码并不像你所相信的那么多。 由于相似的代码涵盖不同的应用场景,因此它们通常是不同的。 例如,两个方法可能都是按产品名称查询产品,但它们在字段、排序和查询条件方面都有所不同。 所以将方法分开放在不同的包中是完全可以的。
此外,复制本身并不是邪恶的。 在开始将代码提取到通用的复用方法之前,要多应用三次原则。
最后,需要强调的是,集中可复用代码仍然是允许的,有时也是合理的,但这些情况将不再那么频繁。
4.3 其他语言支持这种方式吗
按功能分包的方式是语言无关的,即技术无关的,它只和业务领域和功能有关。
原文参考地址:https://phauer.com/2020/package-by-feature/

来源:http://www.yidianzixun.com/article/0or03A2N
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

  • 返回顶部