一种流行的方法是经过技术层面对项目进行分包。可是这种方法有一些缺点。相反,咱们能够按功能分包并建立独立自治的程序包。结果是一个易于理解且不易出错的代码库。java
按照技术分包形成的缺点:数据库
对属于某个功能的全部类的概述不佳。api
通用代码、重用代码和复杂代码趋向于难以理解,而且因为难以把握变动的影响,所以变动很容易破坏其余功能用例。架构
按功能分包从而建立包含功能所需的全部类的程序包。好处是:dom
项目结构的一种很是流行的方法是逐层分包。这将为每一个技术组所属类提供一个软件包。测试
⚠️:按层分包从技术角度对全部类进行分组编码
让咱们将调用层次结构添加到图片中,以“清楚地”了解哪一个类取决于其余哪一个类。线程
⚠️:调用层次结构遍布整个项目,涉及许多包翻译
那么,按层分包的缺点是什么?日志
功能概述不佳。一般,当咱们在项目中处理代码时,咱们首先会想到要更改的特定领域或功能。所以,咱们会从领域的角度出发。不幸的是,按技术分层分包迫使咱们从一种软件包过渡到另外一种软件包,才能掌握功能的概况。
通用,重用和复杂代码的趋势。一般,这种方法致使中心类包含每一个功能用例的全部方法。随着时间的流逝,这些方法愈来愈抽象化(带有额外的参数和泛型)来知足更多用例。上图中仅一个示例是ProductDAO,其中放置了ProductController和ExportController的方法。结果是:
当添加更多方法时,类将变得更大。所以,仅凭代码量,就很难理解它。
更改通用重用代码很危险。尽管您只想处理一个用例,但您能够轻松地打破全部用例。
因为如下两个缘由,难以理解抽象方法和通用方法:首先,要通用,一般须要其余技术构造(例如,switch,参数,泛型),这使得查看与当前用例相关的业务逻辑更加困难。其次,认知需求更高,由于您必须了解全部其余用例,以确保您不会破坏它们。
桑迪·梅斯(Sandi Metz)指出:
“我以为我必须了解全部内容才能提供帮助。”桑迪·梅斯(Sandi Metz)。请参阅个人帖子,了解咱们的编码智慧墙。
⚠️:咱们达到了DRY,但违反了KISS。
让咱们将这些类从新排列成独立的功能包。
👆用户管理功能包
新的包userManagement包含属于此功能的全部类:控制器,DAO,DTO和实体。
👆产品管理功能包
新软件包productManagement包含相同的类类型,以及StockServiceClient和相应的StockDTO。这个事实清楚地代表:库存服务仅由产品管理人员使用。
userManagement和productManagement使用不一样的域实体和表。将它们分红不一样的包很简单。可是,当一个功能须要与另外一个功能类似或甚至相同的域实体时,会发生什么?
👆产品出口的功能包
如今,它变得愈来愈有趣。exportProduct包也处理产品实体,但具备不一样的功能用例。
咱们的目标是拥有独立自治的功能包。所以,exportProduct应该具备本身的DAO,DTO类和实体类,即便它们看起来与productManagement中的类类似。抵制重用productManagement中的类的冲动。
咱们可能不得再也不次编写更多代码,但最终会遇到很是有利的状况:
上面的功能包很棒,但实际上,咱们将始终须要一个通用的包。
👆通用软件包包含技术配置和可重复使用的代码
它包含技术配置类(例如用于DI,Spring,对象映射,http客户端,数据库链接,链接池,日志记录,线程池)
它包含可重用的有用代码片断。可是要很是当心代码的过早抽象。我老是先把代码放到尽量接近它的用法的地方,也就是特性包,甚至是使用类。仅当片断确实有更多用途(⚠️:而不是我认为未来可能会使用)时,才将其移动到通用包中。三定律提供了很好的指导。
在通用包中找到全部实体多是有意义的。咱们还对某些项目执行了此操做,其中许多功能包一次又一次地使用相同的实体。一些开发人员还但愿将全部实体放在中心位置,以便可以总体查看数据库架构的映射。目前,我并非教条,由于实体的两个位置均可以合理。不过,一开始我老是尽量多地将代码转移到功能包中,并依赖于定制的特定于用例的实体和投影。
最终,咱们的大图看起来像这样:
👆按功能分包的大图
让咱们简要总结一下好处:
可是,我认为优势大于缺点。
拟议的按功能分包方法遵循的原则很是贴切:
KISS > DRY
再次,我想引用桑迪·梅斯(Sandi Metz)
“我以为我必须了解全部内容才能提供帮助。”桑迪·梅斯(Sandi Metz)。请参阅个人帖子,了解咱们的编码智慧墙。
咱们的团队记录了其遵循的编码准则和原则。关于按功能分包的部分以下所示:
咱们基于功能分包。每一个功能包均包含提供该功能所需的大多数代码。每一个功能包都应独立且自治。
├── feature1 │ ├── Feature1Controller │ ├── Feature1DAO │ ├── Feature1Client │ ├── Feature1DTOs.kt │ ├── Feature1Entities.kt │ └── Feature1Configuration ├── feature2 ├── feature3 └── common
这取决于项目和功能包的大小。
对于中小型项目,我喜欢避免定义可能会增长更多仪式而非价值的规则(例如,要求定义某些接口和子包)。只要您构建独立的、自治的、从您的特定业务领域派生的包,您就在正确的轨道上。
若是要处理更大的代码库,则可能须要定义有关子包结构和方式的更多规则,则容许一个功能包访问另外一个功能包。“模块”或“组件”而不是“功能包”的概念可能更有帮助。例如,Tom Hombergs建议在每一个组件包中添加api和内部包,这些组件包定义组件的哪些部分容许其余组件使用。有关详细信息,请参阅他的文章“使用Spring Boot和ArchUnit清理架构边界”。
是的,会有一些重复,可是根据个人经验,您可能不会相信那么多100%相同的代码。因为类似的代码涵盖了不一样的用例,所以一般是不一样的。例如,两种方法能够按产品名称查询产品,可是它们在计划的字段,排序和其余条件方面有所不一样。所以,最好将方法分开放在不一样的程序包中。
并且,复制自己并非邪恶的。在开始将代码提取到通用重用方法以前,我喜欢应用三定律。
最后,我想强调指出,仍然容许集中使用可重用的代码,有时甚至是合理的,可是这些状况再也不那么常见了。
分包方法与语言无关。可是Kotlin使其易于遵循:
使用数据类,编写量身定制的特定于功能的结构(如DTO或实体)仅需几行,而无需样板。
Kotlin容许将多个类放在一个文件中。所以,咱们可使一个包含全部数据类定义的DTOs.kt或Entities.kt文件成为一个单独的DTOs.kt或Entities.kt文件,而不是有一个子包DTO或包含每一个POJO类的许多Java文件的实体。
本文翻译自:https://phauer.com/2020/package-by-feature/
关注笔者公众号,推送各种原创/优质技术文章 ⬇️