90

不久前我就想到了这个问题,最近它又出现了,因为我的商店正在开发它的第一个真正的 Java Web 应用程序。

作为介绍,我看到了两种主要的包命名策略。(需要明确的是,我不是指整个 'domain.company.project' 部分,我指的是下面的包约定。)无论如何,我看到的包命名约定如下:

  1. 功能性:根据架构上的功能而不是根据业务领域的身份来命名您的包。 另一个术语可能是根据“层”命名。因此,您将拥有一个 *.ui 包、一个 *.domain 包和一个 *.orm 包。您的包裹是水平切片而不是垂直切片。

    逻辑命名更常见。事实上,我相信我从未见过或听说过这样做的项目。这当然让我怀疑(有点像认为你已经想出了一个解决 NP 问题的方法),因为我不是很聪明,而且我认为每个人都必须有充分的理由这样做。另一方面,我并不反对人们只是想念房间里的大象,而且我从未听说过以这种方式命名包的实际论据。这似乎是事实上的标准。

  2. 逻辑:根据业务域标识命名您的包,并将与该垂直功能切片有关的每个类放入该包中。

    正如我之前提到的,我从未见过或听说过这一点,但这对我来说很有意义。

    1. 我倾向于垂直而不是水平地接近系统。我想进去开发订单处理系统,而不是数据访问层。显然,我很有可能会在该系统的开发中触及数据访问层,但关键是我不这么认为。当然,这意味着当我收到变更单或想要实现一些新功能时,不必为了找到所有相关的类而在一堆包中四处寻找会很好。相反,我只是查看 X 包,因为我所做的与 X 有关。

    2. 从开发的角度来看,我认为让你的包记录你的业务领域而不是你的架构是一个重大的胜利。我觉得域几乎总是系统中更难理解的部分,因为系统的架构,特别是在这一点上,在其实现中几乎变得平凡。事实上,我可以使用这种类型的命名约定并立即从包的命名中知道它处理订单、客户、企业、产品等的系统,这似乎非常方便。

    3. 看起来这样可以让您更好地利用 Java 的访问修饰符。这使您可以更清晰地将接口定义到子系统中,而不是系统的层中。因此,如果您有一个希望透明持久化的订单子系统,理论上您可以通过不必在 dao 层中为其持久性类创建公共接口,而是将 dao 类包装在只有它处理的类。显然,如果您想公开此功能,您可以为其提供一个接口或将其公开。通过将系统功能的垂直部分拆分到多个包中,您似乎失去了很多。

    4. 我想我可以看到的一个缺点是它确实使剥离图层变得更加困难。而不是仅仅删除或重命名一个包,然后用另一种技术将一个新的包放到适当的位置,您必须进入并更改所有包中的所有类。但是,我认为这没什么大不了的。这可能是由于缺乏经验,但我不得不想象,与您在系统中进入和编辑垂直特征切片的次数相比,您更换技术的次数相形见绌。

所以我想这个问题会问你,你如何命名你的包,为什么?请理解,我不一定认为我在这里偶然发现了金鹅或其他东西。我对这一切都很陌生,主要是学术经验。但是,我无法发现我的推理中的漏洞,所以我希望你们都可以,这样我就可以继续前进。

4

15 回答 15

34

对于包装设计,我首先按层划分,然后按其他功能划分。

还有一些额外的规则:

  1. 层从最一般(底部)到最具体(顶部)堆叠
  2. 每层都有一个公共接口(抽象)
  3. 一层只能依赖另一层的公共接口(封装)
  4. 一个层只能依赖于更一般的层(从上到下的依赖关系)
  5. 一个层最好依赖于它正下方的层

因此,例如,对于 Web 应用程序,您的应用程序层中可能有以下层(从上到下):

  • 表示层:生成将在客户端层中显示的 UI
  • 应用层:包含特定于应用程序的逻辑,有状态
  • 服务层:按领域分组功能,无状态
  • 集成层:提供对后端层(db、jms、email...)的访问

对于生成的包布局,这些是一些附加规则:

  • 每个包名的根是<prefix.company>.<appname>.<layer>
  • 层的接口按功能进一步划分:<root>.<logic>
  • 层的私有实现以私有为前缀:<root>.private

这是一个示例布局。

表示层由视图技术划分,并且可选地由(组)应用程序划分。

com.company.appname.presentation.internal
com.company.appname.presentation.springmvc.product
com.company.appname.presentation.servlet
...

应用层分为用例。

com.company.appname.application.lookupproduct
com.company.appname.application.internal.lookupproduct
com.company.appname.application.editclient
com.company.appname.application.internal.editclient
...

服务层被划分为业务域,受后端层中的域逻辑的影响。

com.company.appname.service.clientservice
com.company.appname.service.internal.jmsclientservice
com.company.appname.service.internal.xmlclientservice
com.company.appname.service.productservice
...

集成层分为“技术”和访问对象。

com.company.appname.integration.jmsgateway
com.company.appname.integration.internal.mqjmsgateway
com.company.appname.integration.productdao
com.company.appname.integration.internal.dbproductdao
com.company.appname.integration.internal.mockproductdao
...

像这样分离包的优点是更容易管理复杂性,并且增加了可测试性和可重用性。虽然这看起来像是很多开销,但根据我的经验,它实际上非常自然,并且在这个结构(或类似结构)上工作的每个人都可以在几天内完成它。

为什么我认为垂直方法不太好?

在分层模型中,几个不同的高层模块可以使用同一个底层模块。例如:可以为同一个应用构建多个视图,多个应用可以使用同一个服务,多个服务可以使用同一个网关。这里的诀窍是,当通过层移动时,功能级别会发生变化。更具体层中的模块不会在更通用层的模块上映射 1-1,因为它们表达的功能级别不会映射 1-1。

当您使用垂直方法进行包装设计时,即首先按功能划分,然后将具有不同功能级别的所有构建块强制放入相同的“功能外套”中。您可以为更具体的模块设计通用模块。但这违反了更一般的层不应该知道更具体的层的重要原则。例如,服务层不应该以应用层的概念为模型。

于 2009-02-10T22:27:28.103 回答
18

我发现自己坚持鲍勃叔叔的包装设计原则。简而言之,要一起重用和一起更改的类(出于相同的原因,例如依赖项更改或框架更改)应该放在同一个包中。IMO,在大多数应用程序中,功能分解比垂直/业务特定分解更有可能实现这些目标。

例如,领域对象的水平切片可以被不同类型的前端甚至应用程序重用,而当底层 Web 框架需要更改时,Web 前端的水平切片可能会一起更改。另一方面,如果将跨不同功能区域的类分组在这些包中,则很容易想象这些更改在许多包中的连锁反应。

显然,并非所有类型的软件都是相同的,并且在某些项目中垂直分解可能是有意义的(就实现可重用性和可更改性的目标而言)。

于 2009-02-10T17:35:29.100 回答
5

通常存在两个级别的划分。从顶部开始,有部署单元。这些被命名为“逻辑上”(用你的话说,想想 Eclipse 特性)。在部署单元内部,您可以对包进行功能划分(想想 Eclipse 插件)。

例如,功能是com.feature,它由com.feature.clientcom.feature.corecom.feature.ui插件组成。在插件内部,我对其他包的划分很少,尽管这也并不罕见。

更新:顺便说一句,Juergen Hoeller 在 InfoQ 上有关于代码组织的精彩演讲:http: //www.infoq.com/presentations/code-organization-large-projects。Juergen 是 Spring 的架构师之一,并且对这些东西非常了解。

于 2009-02-10T17:44:15.367 回答
4

我从事的大多数 java 项目首先从功能上对 java 包进行切片,然后再从逻辑上切片。

通常部分足够大,以至于它们被分解成单独的构建工件,您可以将核心功能放入一个 jar,将 apis 放入另一个 jar,将 Web 前端内容放入 warfile 等。

于 2009-02-10T17:00:31.353 回答
3

包将作为一个单元进行编译和分发。在考虑哪些类属于一个包时,关键标准之一是它的依赖关系。此类依赖于哪些其他包(包括第三方库)。一个组织良好的系统会将具有相似依赖关系的类聚集在一个包中。这限制了对一个库的更改的影响,因为只有少数定义明确的包会依赖它。

听起来您的逻辑垂直系统可能倾向于“涂抹”大多数包之间的依赖关系。也就是说,如果每个功能都打包为一个垂直切片,那么每个包都将依赖于您使用的每个第三方库。对库的任何更改都可能波及整个系统。

于 2009-02-10T17:14:12.473 回答
3

我个人更喜欢对类进行逻辑分组,然后在其中包含每个功能参与的子包。

包装目标

包毕竟是将事物组合在一起 - 相关类的想法彼此靠近。如果他们住在同一个包中,他们可以利用包私有来限制可见性。问题是将所有视图和持久性内容集中到一个包中可能会导致许多类混入一个包中。因此,下一个明智的做法是创建视图、持久性、util 子包并相应地重构类。不幸的是,受保护的包私有作用域不支持当前包和子包的概念,因为这将有助于执行此类可见性规则。

我现在通过功能看到了分离的价值,因为对所有与视图相关的东西进行分组有什么价值。此命名策略中的事物与视图中的某些类断开连接,而另一些则处于持久性等等。

我的逻辑包装结构示例

为了说明的目的,让我们命名两个模块 - 将名称模块用作将类分组到包树的特定分支下的概念。

apple.model apple.store 香蕉.model 香蕉.store

优点

使用 Banana.store.BananaStore 的客户端只暴露给我们希望提供的功能。hibernate 版本是一个实现细节,他们不需要知道也不应该看到这些类,因为它们给存储操作增加了混乱。

其他逻辑 v 功能优势

越靠近根,范围就越广,属于一个包的东西开始表现出越来越多的依赖于属于她的模块的东西。例如,如果要检查“香蕉”模块,大多数依赖项将仅限于该模块内。事实上,“香蕉”下的大多数助手根本不会在这个包范围之外被引用。

为什么是功能?

通过基于功能将事物集中起来可以实现什么价值。在这种情况下,大多数类相互独立,很少或不需要利用包私有方法或类。将它们重构到自己的子包中几乎没有什么好处,但确实有助于减少混乱。

开发人员对系统的更改

当开发人员的任务是进行不那么微不足道的更改时,他们可能会进行包含来自包树所有区域的文件的更改,这似乎很愚蠢。使用逻辑结构化方法,它们的更改在包树的同一部分内更加本地化,​​这似乎是正确的。

于 2009-02-13T22:12:44.243 回答
3

包装的功能(架构)和逻辑(特性)方法都有一席之地。许多示例应用程序(那些在教科书等中找到的应用程序)遵循将表示、业务服务、数据映射和其他架构层放入单独的包中的功能方法。在示例应用程序中,每个包通常只有几个或只有一个类。

这种初始方法很好,因为一个人为的示例通常用于:1)从概念上绘制出所呈现框架的架构,2)这样做是出于单一的逻辑目的(例如从诊所添加/删除/更新/删除宠物) . 问题是许多读者将此视为没有界限的标准。

随着“业务”应用程序扩展到包含越来越多的功能,遵循功能方法成为一种负担。虽然我知道在哪里寻找基于架构层的类型(例如“web”或“ui”包下的 web 控制器等),但开发单个逻辑功能开始需要在多个包之间来回跳转。至少,这很麻烦,但比这更糟。

由于逻辑上相关的类型没有打包在一起,API过于公开;逻辑相关类型之间的交互被强制为“公共”,以便类型可以导入并相互交互(失去了最小化默认/包可见性的能力)。

如果我正在构建一个框架库,我的包无论如何都将遵循功能/架构打包方法。我的 API 消费者甚至可能会欣赏他们的导入语句包含以架构命名的直观包。

相反,在构建业务应用程序时,我将按功能打包。我可以将 Widget、WidgetService 和 WidgetController 都放在同一个“ com.myorg.widget. ”包中,然后利用默认可见性(并且具有更少的导入语句以及包间依赖项)。

但是,也有交叉的情况。如果我的 WidgetService 被许多逻辑域(功能)使用,我可能会创建一个“ com.myorg.common.service. ”包。还有一个很好的机会是,我创建类的目的是为了跨功能重用,并最终得到诸如“ com.myorg.common.ui.helpers. ”和“ com.myorg.common.util. ”之类的包。我什至可能最终将所有这些后来的“通用”类移动到一个单独的项目中,并将它们作为 myorg-commons.jar 依赖项包含在我的业务应用程序中。

于 2013-09-13T17:56:55.007 回答
2

这取决于您的逻辑流程的粒度?

如果它们是独立的,您通常会在源代码控制中为它们创建一个新项目,而不是一个新包。

我目前正在进行的项目在逻辑拆分方面犯了错误,有一个用于 jython 方面的包,一个用于规则引擎的包,用于 foo、bar、binglewozzle 等的包。我正在考虑使用特定于 XML 的解析器/writers 用于该包中的每个模块,而不是拥有一个 XML 包(我之前做过),尽管仍然会有一个核心 XML 包用于共享逻辑。然而,这样做的一个原因是它可能是可扩展的(插件),因此每个插件还需要定义其 XML(或数据库等)代码,因此集中这可能会在以后引入问题。

最后,这似乎是对特定项目最明智的方式。但是,我认为按照典型项目分层图的方式进行打包很容易。您最终会得到逻辑和功能包装的混合。

需要的是标记的命名空间。一些 Jython 功能的 XML 解析器可以同时标记 Jython 和 XML,而不必选择其中之一。

或者,也许我在颤抖。

于 2009-02-10T16:51:23.980 回答
2

我尝试以这样一种方式设计包结构,即如果我要绘制依赖关系图,它会很容易遵循并使用一致的模式,并且尽可能少地循环引用。

对我来说,这在垂直命名系统中比在水平命名系统中更容易维护和可视化。如果 component1.display 有对 component2.dataaccess 的引用,这会比 display.component1 有对 dataaccess 的引用发出更多的警告。组件2。

当然,两者共享的组件都放在自己的包中。

于 2009-02-10T17:42:49.787 回答
2

我完全遵循并提出逻辑(“按功能”)组织!一个包应该尽可能地遵循“模块”的概念。职能组织可能会将一个模块分散到一个项目中,导致封装较少,并且易于更改实现细节。

让我们以一个 Eclipse 插件为例:将所有视图或操作放在一个包中会一团糟。相反,一个特性的每个组件都应该进入特性的包,或者如果有很多,则进入子包(featureA.handlers、featureA.preferences 等)

当然,问题在于分层包系统(其中包括 Java 有),这使得正交关注点的处理变得不可能或至少非常困难——尽管它们无处不在!

于 2010-04-16T15:36:41.583 回答
1

完全不使用包(根包除外)是一个有趣的实验。

那么出现的问题是,何时以及为什么引入软件包是有意义的。据推测,答案将与您在项目开始时回答的不同。

我认为您的问题完全出现了,因为包就像类别,有时很难决定其中一个。有时标签会更适合传达一个类在许多上下文中可用的信息。

于 2010-04-28T19:23:07.827 回答
1

我个人会选择功能命名。简短的原因:它避免了代码重复或依赖噩梦。

让我详细说明一下。当您使用带有自己的包树的外部 jar 文件时会发生什么?您正在有效地将(编译的)代码导入到您的项目中,并带有一个(功能分离的)包树。同时使用这两种命名约定是否有意义?不,除非那是对你隐藏的。如果你的项目足够小并且只有一个组件,它就是这样。但是如果你有几个逻辑单元,你可能不想重新实现,比如说数据文件加载模块。您希望在逻辑单元之间共享它,在逻辑上不相关的单元之间没有人为的依赖关系,并且不必选择要将特定共享工具放入哪个单元。

我想这就是为什么功能命名最常用于达到或打算达到一定大小的项目中,并且在类命名约定中使用逻辑命名来跟踪特定角色,如果每个类中的任何一个包裹。

我将尝试更准确地回应您关于逻辑命名的每一点。

  1. 如果您在计划更改时必须在旧类中钓鱼以修改功能,这是抽象不良的标志:您应该构建提供明确定义的功能的类,可以在一个简短的句子中定义。只有少数顶级课程应该组合所有这些以反映您的商业智能。这样,您将能够重用更多代码、更容易维护、更清晰的文档和更少的依赖问题。

  2. 这主要取决于您了解项目的方式。当然,逻辑和功能视图是正交的。因此,如果您使用一种命名约定,则需要将另一种应用于类名以保持某种顺序,或者在某个深度从一种命名约定分叉到另一种命名约定。

  3. 访问修饰符是允许其他了解您的处理的类访问您的类的内部的好方法。逻辑关系并不意味着理解算法或并发约束。功能可能,虽然它没有。我非常厌倦除公共和私有之外的访问修饰符,因为它们经常隐藏缺乏适当的架构和类抽象。

  4. 在大型商业项目中,技术变化比您想象的更频繁。例如,我已经更改了 3 次 XML 解析器、2 次缓存技术和 2 次地理定位软件。幸好我把所有的细节都藏在了一个专门的包里……

于 2009-02-10T17:09:59.443 回答
0

这取决于。在我的工作中,我们有时会按功能(数据访问、分析)或资产类别(信贷、股票、利率)来拆分包。只需选择对您的团队最方便的结构。

于 2009-02-10T18:16:24.993 回答
0

从纯粹实用的角度来看,java 的可见性构造允许同一包中的类访问具有可见性protected以及default可见性的方法和属性public。从完全不同的代码层使用非公共方法肯定会产生很大的代码气味。所以我倾向于将同一层的类放入同一个包中。

我不经常在其他地方使用这些受保护或默认的方法——除了可能在类的单元测试中——但是当我这样做时,它总是来自同一层的一个类

于 2009-02-10T17:10:31.970 回答
-3

根据我的经验,可重用性带来的问题多于解决问题。使用最新且便宜的处理器和内存,我更喜欢重复代码而不是紧密集成以重用。

于 2013-07-02T23:21:17.083 回答