Package by Feature 按功能打包
Posted on Apr 21, 2020. Updated on Jun 12, 2022
发布于 2020 年 4 月 21 日。更新于 2022 年 6 月 12 日
A popular approach is packaging by technical concerns. But this approach has some drawbacks. Instead, we can package by feature and create self-contained and independent packages. The result is a codebase that is easier to understand and less error-prone.
一种流行的方法是通过技术问题进行包装。但这种方法有一些缺点。取而代之的是,我们可以按功能打包,并创建自包含的独立软件包。这样做的结果是,代码库更容易理解,也更不容易出错。
TL;DR 简要说明
- Drawbacks of packaging classes by technical concerns:
按技术分类的包装缺点:- Poor overview of all classes that belong to a feature.
对属于某一特征的所有类别进行较差的概述。 - Tendency to generic, reused and complex code, which is hard to understand and changes can easily break other use cases as the impact of a change is hard to grasp.
倾向于使用通用、重复使用和复杂的代码,这很难理解,而且由于难以把握变更的影响,变更很容易破坏其他用例。
- Poor overview of all classes that belong to a feature.
- Instead, package by feature and create packages that contain all classes that are required for a feature. The benefits are:
取而代之的是按功能打包,创建包含功能所需的所有类的包。这样做的好处是- Better discoverability and overview
更好的可发现性和概览性 - Self-contained and independent
自足和独立 - Simpler code 更简单的代码
- Testability 可测试性
- Better discoverability and overview
Package by Layer 逐层包装
A very popular approach for a project structure is to package by layer. This leads to a package for each technical group of classes.
在项目结构中,一种非常流行的方法是按层打包。这样,每个技术类组都有一个软件包。
Packaging by layer groups all classes by a technical point of view
分层包装从技术角度对所有类别进行分组
Let’s add the call hierarchy to the picture to “clearly” see which class depends on which class.
让我们把调用层次结构添加到图片中,以便 "清楚 "地看到哪个类依赖于哪个类。
The call hierarchy is spread across the whole project and involves many packages
调用层次遍布整个项目,涉及许多软件包
So, what are the drawbacks of packaging by layer?
那么,分层包装有什么缺点呢?
- Poor feature overview. Usually, when we approach the code in a project, we have a certain domain or feature in mind that we want to change. So we are coming from a domain perspective. Unfortunately, technical packaging forces us to jump around from one package to another to grasp the big picture of a feature.
功能概述不清。通常,我们在处理项目中的代码时,都会想到要修改的某个领域或功能。因此,我们会从领域的角度出发。不幸的是,技术封装迫使我们从一个封装跳转到另一个封装,以掌握功能的全貌。 - Tendency to generic, reused, and complex code. Often, this approach leads to central classes containing all methods for every use case. Over time, those methods get more and more abstracted (with extra parameters and generics) to fulfill more use cases. Only one example in the above picture is the
ProductDAO
where the methods for theProductController
andExportController
are located. The consequences are:
倾向于通用、重复使用和复杂的代码。这种方法通常会导致中心类包含每个用例的所有方法。随着时间的推移,这些方法会越来越抽象(使用额外的参数和泛型),以满足更多的用例。上图中的ProductDAO
就是一个例子,其中包含了ProductController
和ExportController
的方法。其后果是- A class gets bigger when more methods are added. So understanding it becomes harder just because of the amount of code.
方法越多,类就越大。因此,仅仅因为代码量大,理解它就变得更加困难。 - Changing the generic reused code is dangerous. You can easily break all use cases although you only want to work on one use case.
更改通用重用代码很危险。虽然你只想处理一个用例,但却很容易破坏所有用例。 - Abstracted and generic methods are harder to understand for the following two reasons: First, to be generic, you usually need additional technical constructs (if, else, switch, parameter, generics) which makes it harder to see the business logic that the relevant for the current use case (signal-noise-ratio). Second, the cognitive demand is higher because you have to know all the other use cases to be sure you don’t break them. Sandi Metz nailed this:
抽象方法和泛型方法更难理解,原因有二:首先,要实现泛型,通常需要额外的技术构造(if、else、switch、参数、泛型),这使得查看与当前用例相关的业务逻辑变得更加困难(信噪比)。其次,对认知的要求更高,因为你必须了解所有其他用例,以确保不会破坏它们。桑迪-梅兹(Sandi Metz)指出了这一点:
- A class gets bigger when more methods are added. So understanding it becomes harder just because of the amount of code.
“I felt like I had to understand everything in order to help with anything.” Sandi Metz. See my post about our Wall of Coding Wisdom.
"我觉得我必须了解一切,才能帮上忙"。桑迪-梅兹。请参阅我关于 "编码智慧墙 "的文章。
We achieve DRY but violate KISS.
我们实现了 DRY,但违反了 KISS。
Package by Feature 按功能打包
Let’s rearrange the classes into self-contained feature packages.
让我们把这些类重新排列成独立的功能包。
The feature package for the user management
用户管理功能包
The new package userManagement
contains all classes that belong to this feature: the controller, the DAO, the DTOs and the entities.
新软件包 userManagement
包含属于该功能的所有类:控制器、DAO、DTO 和实体。
The feature package for the product management
产品管理功能包
The new package productManagement
contains the same class types plus the StockServiceClient
and the corresponding StockDTO
. This fact states clearly: The stock service is only used by product management.
新包 productManagement
包含相同的类类型,加上 StockServiceClient
和相应的 StockDTO
。这一事实清楚地说明了这一点:库存服务仅用于产品管理。
The userManagement
and productManagement
are using different domain entities and tables. Separating them into different packages is simple. But what happens when a feature needs similar or the even same domain entities than another feature?
userManagement
和 productManagement
使用不同的域实体和表。将它们分离到不同的软件包中很简单。但是,如果一个功能需要与另一个功能相似甚至相同的域实体,该怎么办呢?
The feature package for the product export
产品出口的功能包
Now, it’s getting interesting. The package exportProduct
also deals with the product entity but has a different use case.
现在,情况越来越有趣了。软件包 exportProduct
也与产品实体打交道,但用途不同。
Our goal is to have self-contained independent feature packages. Consequently, the exportProduct
should have it’s own DAO, DTO classes, and entities classes even if they may look similar to the classes in the productManagement
. Resist the urge to reuse the classes from productManagement
.
我们的目标是拥有自成一体的独立功能包。因此, exportProduct
应该有自己的 DAO、DTO 类和实体类,即使它们看起来与 productManagement
中的类相似。不要急于重用 productManagement
中的类。
- We can use structs (DTO, entites) that are tailored for the export use case. They only contain the relevant fields and the entities can be created based on a query with a nice projection of the relevant columns - and nothing else.
我们可以使用专为导出用例定制的结构体(DTO、entites)。它们只包含相关字段,而且实体可以根据查询创建,并对相关列进行漂亮的投影,除此之外别无其他。 - The dedicated
ExportProductDAO
contains export-specific queries and projections.
专门的ExportProductDAO
包含针对出口的查询和预测。
We may have to write more code (again) but end up with a very beneficial situation:
我们可能需要编写更多的代码(再次),但最终情况会非常有利:
- Changes in the
productManagement
will never break theexportProduct
code and vice versa. They can evolve independently.
productManagement
的变化永远不会破坏exportProduct
的代码,反之亦然。它们可以独立发展。 - When changing code, we only have to keep the current feature in mind.
在修改代码时,我们只需牢记当前的功能。 - The code itself will become much simpler and easier to understand because it’s not generic and doesn’t have to work for both use-cases.
代码本身将变得更简单、更容易理解,因为它不是通用的,不必同时适用于两种用例。
The above feature packages are great but in reality, we will always need a common
package.
上述功能包固然很好,但在现实中,我们总是需要一个 common
功能包。
The common package contains technical configuration and reusable code
通用软件包包含技术配置和可重复使用的代码
- It contains technical configuration classes (e.g. for DI, Spring, object mapping, http clients, database connection, connection pooling, logging, thread pools)
它包含技术配置类(如 DI、Spring、对象映射、http 客户端、数据库连接、连接池、日志、线程池等)。 - It contains small useful code snippets that can be reused. But be very careful with the premature abstraction of your code. I always start by putting util code as close as possible to its usage, which is the feature package or even the using class. Only if I really have more usages for a snippet (not: I think I might have in the future), I move it to the
common
package. The Rule Of Three gives good guidance.
它包含一些有用的小代码片段,可以重复使用。但要注意不要过早抽象代码。我总是先将实用代码尽可能地贴近其用途,也就是功能包,甚至是使用类。只有当某个代码段真的有更多的用途时(而不是:我认为将来可能会有),我才会把它移到common
包中。三原则提供了很好的指导。 - It might make sense to locate all entities in the
common
package. We also did this for some projects, where many feature packages are using the same entities again and again. Some devs also prefer to have all entities in a central place to be able to see the mapping of the database schema as a whole. I’m not dogmatic at this point because both locations for entities can be reasonable. Still, I always start to move as much code to the feature package as possible and rely on tailored use-case-specific entities and projections.
在common
软件包中定位所有实体可能是有意义的。我们在一些项目中也这样做了,在这些项目中,许多功能包都反复使用相同的实体。有些开发人员也喜欢把所有实体放在一个中心位置,以便查看整个数据库模式的映射。在这一点上,我并不固执己见,因为这两种实体位置都是合理的。不过,我在开始时总是尽可能多地将代码移到功能包中,并依赖于为特定用例量身定制的实体和投影。
Big Picture 大图片
Finally, our big picture looks like this:
最后,我们的大局是这样的
Big picture of the package-by-feature approach
逐个功能包方法的全貌
Benefits 益处
Let’s briefly wrap up the benefits:
让我们简要总结一下这些好处:
- Better discoverability and overview from the domain point of view. Most of the code that belongs to a business feature is located together. This is crucial because we are approaching a codebase usually with a certain business requirement in mind.
从领域角度看,可发现性和概览性更好。属于业务功能的大部分代码都位于一起。这一点至关重要,因为我们在处理代码库时通常会考虑到特定的业务需求。 - Self-contained and independent. Most of the code that a feature needs, is located in the package. So we are avoiding dependencies to other feature packages. The consequences are:
独立自足。功能所需的大部分代码都在软件包中。因此,我们避免了对其他功能包的依赖。这样做的后果是- It’s less likely that we break other features while evolving a feature.
我们在开发一项功能时破坏其他功能的可能性较小。 - Less cognitive capacity is required to estimate the impact of changes. Often, we only have to keep the current package in mind.
估计变化的影响所需的认知能力较低。通常情况下,我们只需牢记当前的一揽子计划。
- It’s less likely that we break other features while evolving a feature.
- Simpler code. As we are avoiding generic and abstracted code, the code becomes simpler because it only has to handle a single use-case. Hence, it’s easier to understand and to evolve the code.
代码更简单。由于我们避免使用通用和抽象代码,代码变得更加简单,因为它只需处理单一用例。因此,代码更易于理解和发展。 - Testability. Usually, a class in a feature package has fewer dependencies compared to a “god-class” in a technical package that tries to fulfill all use-cases. So testing becomes easier as we have to create less test fixture.
可测试性。通常情况下,功能包中的类与技术包中的 "神级 "类相比,依赖关系较少,而技术包中的 "神级 "类则试图满足所有的用例。因此,测试变得更容易,因为我们只需创建更少的测试夹具。
Drawbacks 缺点
- We have to write more code.
我们必须编写更多的代码。 - We might write similar code multiple times.
我们可能会多次编写类似的代码。 - It’s tricky to decide when we are better off moving code to the
common
package and to reuse it. The Rule of Three is useful when in doubt. I like to highlight that reuse is still allowed and useful.
要决定何时将代码转移到common
软件包并重复使用会更好,这很棘手。在有疑问时,"三原则 "很有用。我喜欢强调重复使用仍然是允许和有用的。 - It’s also tricky to find out the adequate scope and size of a feature package. See the questions sections for details about this.
确定功能包的适当范围和大小也很棘手。有关详情,请参阅问题部分。
However, I believe that the advantages outweigh the drawbacks.
不过,我认为利大于弊。
The Principles Behind 背后的原则
The proposed package-by-feature approach follows a principle that’s very close to my heart:
建议的按功能打包的方法遵循的原则与我的想法非常接近:
KISS > DRY 吻 > 干
Again, I like to quote Sandi Metz
我想再次引用桑迪-梅兹的话
“Prefer duplication over the wrong abstraction.” Sandi Metz. See The Wall Of Coding Wisdom.
"宁愿重复,也不要错误的抽象"。桑迪-梅兹请参阅《编码智慧墙》。
A Recipe to Package by Feature
按功能包装的食谱
Our team documents its coding guidelines and principles that it commits on. The section about packaging by feature looks like this:
我们的团队记录了自己的编码指南和原则,并以此为基础进行提交。关于按功能打包的部分是这样的:
We package our code based on features. Each feature package contains most of the code that is required to serve the feature. Each feature package should be self-contained and independent.
我们根据功能打包代码。每个功能包都包含该功能所需的大部分代码。每个功能包都应该是自足和独立的。
├── feature1
│ ├── Feature1Controller
│ ├── Feature1DAO
│ ├── Feature1Client
│ ├── Feature1DTOs.kt
│ ├── Feature1Entities.kt
│ └── Feature1Configuration
├── feature2
├── feature3
└── common
- This approach affects all layers. For instance, each package has its own DAO and client. There should be no huge god DAO class.
这种方法会影响所有层。例如,每个软件包都有自己的 DAO 和客户端。不应该有一个庞大的 DAO 类。 - A package should have only a few relationships with other packages. Everything that is required for the feature should be placed inside the package.
一个软件包与其他软件包之间的关系应该很少。功能所需的一切都应放在软件包内。 - Rule of thumb: If you want to delete a feature, you should only have to delete the corresponding package.
经验法则:如果要删除某个功能,只需删除相应的软件包即可。 - Still, it’s okay to reuse stuff in a
common
package but it should only contain the code that is used multiple times (see rule of three). It doesn’t contain business logic. Technical utils are okay.
尽管如此,在common
包中重复使用某些内容也是可以的,但它只应包含多次使用的代码(参见三原则)。它不包含业务逻辑。技术实用程序可以。 - If there are feature-specific Spring beans, we place their configuration in the feature package.
如果有特定功能的 Spring Bean,我们会将其配置放在功能包中。
Questions 问题
What About the Structure Within a Feature Package?
功能包内部结构如何?
This depends on the size of your project and feature packages.
这取决于项目的规模和功能包。
For small and mid-size projects, I like to avoid defining rules that may add more ceremony than value (e.g. by requiring certain interfaces and subpackages). As long as you build independent and self-contained packages derived from your domain you are on the right track.
对于中小型项目,我倾向于避免定义可能会带来更多麻烦而非价值的规则(例如,通过要求某些接口和子软件包)。只要你从自己的领域出发,构建独立、自足的软件包,你就走对了路。
If you are dealing with a bigger codebase you may want to define more rules about the subpackage structure and the way, one feature package is allowed to access another one. The notion of “modules” or “components” instead of “feature packages” may be more helpful. For example, Tom Hombergs suggests adding api
and internal
packages in each component package that defines which parts of the component are allowed to be used by other components. See his post “Clean Architecture Boundaries with Spring Boot and ArchUnit” for details.
如果您要处理的是一个更大的代码库,您可能需要定义更多有关子包结构的规则,以及允许一个功能包访问另一个功能包的方式。模块 "或 "组件 "的概念可能比 "功能包 "更有帮助。例如,Tom Hombergs 建议在每个组件包中添加 api
和 internal
包,定义允许其他组件使用组件的哪些部分。详情请参见他的文章《用 Spring Boot 和 ArchUnit 清理架构边界》。
Will I End Up Writing the Same Code Again and Again?
我最终会反复编写相同的代码吗?
Yes, there will be some duplication but after my experience, there is not so much 100% identical code as you may believe. As the similar code covers different use-cases it is often different. For instance, two methods may query for products by the product name but they differ in the projected fields, sorting and additional criteria. So it’s totally fine to keep the methods separated in different packages.
是的,会有一些重复,但根据我的经验,并不像你认为的那样有那么多 100% 相同的代码。因为相似的代码涵盖了不同的用例,所以往往是不同的。例如,两种方法都可以通过产品名称查询产品,但它们在预测字段、排序和附加条件方面有所不同。因此,完全可以将这些方法分开放在不同的包中。
Moreover, duplication is not evil per se. I like to apply the rule of three before I start to extract code to a generic reused methods.
此外,重复本身并不邪恶。我喜欢在开始提取通用重用方法的代码之前应用三原则。
Finally, I like to highlight that centralizing reusable code is still allowed and sometimes reasonable, BUT those cases are not so frequent anymore.
最后,我想强调的是,集中管理可重用代码仍然是允许的,有时也是合理的,但这种情况已不再常见。
Can Kotlin Support This Approach?
Kotlin 能否支持这种方法?
The packaging approach is language-independent. But Kotlin makes it easier to follow it:
打包方法与语言无关。但 Kotlin 让我们更容易遵循它:
- With data classes, writing tailored feature-specific structs (like DTOs or entities) takes only a few lines and no boilerplate.
有了数据类,编写定制的特定功能结构体(如 DTO 或实体)只需几行,无需模板。 - Kotlin allows putting multiple classes in one file. So instead of having a subpackage
dtos
orentities
containing many Java files for each POJO class, we can have a singleDTOs.kt
orEntities.kt
file containing all data class definitions.
Kotlin 允许将多个类放在一个文件中。因此,我们可以用一个包含所有数据类定义的DTOs.kt
或Entities.kt
文件来代替包含每个 POJO 类的多个 Java 文件的子包dtos
或entities
。