在 Java 项目中使用什么策略来命名包? 为什么?

我前段时间就考虑过这个问题,最近当我的商店正在开发第一个真正的 Java 网络应用程序时,这个问题又浮出了水面。

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

  1. Functional: 根据功能架构来命名包,而不是根据业务领域来命名它们的标识。这个词的另一个术语可能是根据“层”命名。所以,你会有一个 * 。Ui 包和一个 * 。域包和一个 * 。奥姆包裹。您的软件包是水平切片,而不是垂直切片。

    这是比逻辑命名更常见的 很多。事实上,我相信我从来没有见过或听说过这样的项目。当然,这让我感到怀疑(有点像你已经想出了一个 NP 问题的解决方案) ,因为我并不是特别聪明,我认为每个人都必须有很好的理由才能这样做。另一方面,我并不反对人们只是忽略了房间里的大象 还有我从来没有听到过一个实际的论点 为了这样做包命名。只是看起来像是行业标准。

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

    正如我之前提到的,我从未见过或听说过这种情况,但它对我来说意义重大。

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

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

    3. 这似乎可以让您更好地利用 Java 的访问修饰符。这使您能够更清楚地定义子系统的接口,而不是系统的层。因此,如果你有一个订单子系统,你想要透明持久化,你可以在理论上永远不要让任何其他东西知道它是持久化的,不必创建公共接口到道层的持久化类,而是打包道类,只有它处理的类。显然,如果想要公开这个功能,可以为它提供一个接口,或者将其公开。通过将系统特性的一个垂直部分分割到多个包中,您似乎失去了很多这方面的东西。

    4. 我认为我能看到的一个缺点是,它确实使得剥离图层变得有点困难。不能只是删除或重命名一个包,然后使用替代技术删除一个新包,而是必须进入并更改所有包中的所有类。不过,我觉得这没什么大不了的。这可能是因为缺乏经验,但我不得不想象,与在系统中编辑垂直特性切片的次数相比,更换技术的次数相形见绌。

因此,我想接下来的问题会是你,你如何命名你的包裹,以及为什么?请理解,我并不认为我无意中发现了金鹅或其他什么东西。我对这一切都很陌生,大部分是学术经验。然而,我无法发现我的推理中的漏洞,所以我希望你们都能发现,这样我就可以继续前进了。

31644 次浏览

It depends on the granularity of your logical processes?

If they're standalone, you often have a new project for them in source control, rather than a new package.

The project I'm on at the moment is erring towards logical splitting, there's a package for the jython aspect, a package for a rule engine, packages for foo, bar, binglewozzle, etc. I'm looking at having the XML specific parsers/writers for each module within that package, rather than having an XML package (which I have done previously), although there will still be a core XML package where shared logic goes. One reason for this however is that it may be extensible (plugins) and thus each plugin will need to also define its XML (or database, etc) code, so centralising this could introduce problems later on.

In the end it seems to be how it seems most sensible for the particular project. I think it's easy to package along the lines of the typical project layered diagram however. You'll end up with a mix of logical and functional packaging.

What's needed is tagged namespaces. An XML parser for some Jython functionality could be tagged both Jython and XML, rather than having to choose one or the other.

Or maybe I'm wibbling.

Most java projects I've worked on slice the java packages functionally first, then logically.

Usually parts are sufficiently large that they're broken up into separate build artifacts, where you might put core functionality into one jar, apis into another, web frontend stuff into a warfile, etc.

I would personally go for functional naming. The short reason: it avoids code duplication or dependency nightmare.

Let me elaborate a bit. What happens when you are using an external jar file, with its own package tree? You are effectively importing the (compiled) code into your project, and with it a (functionally separated) package tree. Would it make sense to use the two naming conventions at the same time? No, unless that was hidden from you. And it is, if your project is small enough and has a single component. But if you have several logical units, you probably don't want to re-implement, let's say, the data file loading module. You want to share it between logical units, not have artificial dependencies between logically unrelated units, and not have to choose which unit you are going to put that particular shared tool into.

I guess this is why functional naming is the most used in projects that reach, or are meant to reach, a certain size, and logical naming is used in class naming conventions to keep track of the specific role, if any of each class in a package.

I will try to respond more precisely to each of your points on logical naming.

  1. If you have to go fishing in old classes to modify functionalities when you have a change of plans, it's a sign of bad abstraction: you should build classes that provide a well defined functionality, definable in one short sentence. Only a few, top-level classes should assemble all these to reflect your business intelligence. This way, you will be able to reuse more code, have easier maintenance, clearer documentation and less dependency issues.

  2. That mainly depends on the way you grok your project. Definitely, logical and functional view are orthogonal. So if you use one naming convention, you need to apply the other one to class names in order to keep some order, or fork from one naming convention to an other at some depth.

  3. Access modifiers are a good way to allow other classes that understand your processing to access the innards of your class. Logical relationship does not mean an understanding of algorithmic or concurrency constraints. Functional may, although it does not. I am very weary of access modifiers other than public and private, because they often hide a lack of proper architecturing and class abstraction.

  4. In big, commercial projects, changing technologies happens more often than you would believe. For instance, I have had to change 3 times already of XML parser, 2 times of caching technology, and 2 times of geolocalisation software. Good thing I had hid all the gritty details in a dedicated package...

From a purely practical standpoint, java's visibility constructs allow classes in the same package to access methods and properties with protected and default visibility, as well as the public ones. Using non-public methods from a completely different layer of the code would definitely be a big code smell. So I tend to put classes from the same layer into the same package.

I don't often use these protected or default methods elsewhere - except possibly in the unit tests for the class - but when I do, it is always from a class at the same layer

Packages are to be compiled and distributed as a unit. When considering what classes belong in a package, one of the key criteria is its dependencies. What other packages (including third-party libraries) does this class depend on. A well-organized system will cluster classes with similar dependencies in a package. This limits the impact of a change in one library, since only a few well-defined packages will depend on it.

It sounds like your logical, vertical system might tend to "smear" dependencies across most packages. That is, if every feature is packaged as a vertical slice, every package will depend on every third party library that you use. Any change to a library is likely to ripple through your whole system.

I find myself sticking with Uncle Bob's package design principles. In short, classes which are to be reused together and changed together (for the same reason, e.g. a dependency change or a framework change) should be put in the same package. IMO, the functional breakdown would have better chance of achieving these goals than the vertical/business-specific break-down in most applications.

For example, a horizontal slice of domain objects can be reused by different kinds of front-ends or even applications and a horizontal slice of the web front-end is likely to change together when the underlying web framework needs to be changed. On the other hand, it's easy to imagine the ripple effect of these changes across many packages if classes across different functional areas are grouped in those packages.

Obviously, not all kinds of software are the same and the vertical breakdown may make sense (in terms of achieving the goals of reusability and closeability-to-change) in certain projects.

I try to design package structures in such a way that if I were to draw a dependency graph, it would be easy to follow and use a consistent pattern, with as few circular references as possible.

For me, this is much easier to maintain and visualize in a vertical naming system rather than horizontal. if component1.display has a reference to component2.dataaccess, that throws off more warning bells than if display.component1 has a reference to dataaccess. component2.

Of course, components shared by both go in their own package.

There are usually both levels of division present. From the top, there are deployment units. These are named 'logically' (in your terms, think Eclipse features). Inside deployment unit, you have functional division of packages (think Eclipse plugins).

For example, feature is com.feature, and it consists of com.feature.client, com.feature.core and com.feature.ui plugins. Inside plugins, I have very little division to other packages, although that's not unusual too.

Update: Btw, there is great talk by Juergen Hoeller about code organization at InfoQ: http://www.infoq.com/presentations/code-organization-large-projects. Juergen is one of architects of Spring, and knows a lot about this stuff.

It depends. In my line of work, we sometimes split packages by functions (data access, analytics) or by asset class (credit, equities, interest rates). Just select the structure which is most convenient for your team.

For package design, I first divide by layer, then by some other functionality.

There are some additional rules:

  1. layers are stacked from most general (bottom) to most specific (top)
  2. each layer has a public interface (abstraction)
  3. a layer can only depend on the public interface of another layer (encapsulation)
  4. a layer can only depend on more general layers (dependencies from top to bottom)
  5. a layer preferably depends on the layer directly below it

So, for a web application for example, you could have the following layers in your application tier (from top to bottom):

  • presentation layer: generates the UI that will be shown in the client tier
  • application layer: contains logic that is specific to an application, stateful
  • service layer: groups functionality by domain, stateless
  • integration layer: provides access to the backend tier (db, jms, email, ...)

For the resulting package layout, these are some additional rules:

  • the root of every package name is <prefix.company>.<appname>.<layer>
  • the interface of a layer is further split up by functionality: <root>.<logic>
  • the private implementation of a layer is prefixed with private: <root>.private

Here is an example layout.

The presentation layer is divided by view technology, and optionally by (groups of) applications.

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

The application layer is divided into use cases.

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

The service layer is divided into business domains, influenced by the domain logic in a backend tier.

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

The integration layer is divided into 'technologies' and access objects.

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
...

Advantages of separating packages like this is that it is easier to manage complexity, and it increases testability and reusability. While it seems like a lot of overhead, in my experience it actually comes very natural and everyone working on this structure (or similar) picks it up in a matter of days.

Why do I think the vertical approach is not so good?

In the layered model, several different high-level modules can use the same lower-level module. For example: you can build multiple views for the same application, multiple applications can use the same service, multiple services can use the same gateway. The trick here is that when moving through the layers, the level of functionality changes. Modules in more specific layers don't map 1-1 on modules from the more general layer, because the levels of functionality they express don't map 1-1.

When you use the vertical approach for package design, i.e. you divide by functionality first, then you force all building blocks with different levels of functionality into the same 'functionality jacket'. You might design your general modules for the more specific one. But this violates the important principle that the more general layer should not know about more specific layers. The service layer for example shouldn't be modeled after concepts from the application layer.

I personally prefer grouping classes logically then within that include a subpackage for each functional participation.

Goals of packaging

Packages are after all about grouping things together - the idea being related classes live close to each other. If they live in the same package they can take advantage of package private to limit visibility. The problem is lumping all your view and persitance stuff into one package can lead to a lot of classes being mixed up into a single package. The next sensible thing to do is thus create view, persistance, util sub packages and refactor classes accordingly. Underfortunately protected and package private scoping does not support the concept of the current package and sub package as this would aide in enforcing such visibility rules.

I see now value in separation via functionality becase what value is there to group all the view related stuff. Things in this naming strategy become disconnected with some classes in the view whilst others are in persistance and so on.

An example of my logical packaging structure

For purposes of illustration lets name two modules - ill use the name module as a concept that groups classes under a particular branch of a pacckage tree.

apple.model apple.store banana.model banana.store

Advantages

A client using the Banana.store.BananaStore is only exposed to the functionality we wish to make available. The hibernate version is an implementation detail which they do not need to be aware nor should they see these classes as they add clutter to storage operations.

Other Logical v Functional advantages

The further up towards the root the broader the scope becomes and things belonging to one package start to exhibit more and more dependencies on things belonging to toher modules. If one were to examine for example the "banana" module most of the dependencies would be limited to within that module. In fact most helpers under "banana" would not be referenced at all outside this package scope.

Why functionality ?

What value does one achieve by lumping things based on functionality. Most classes in such a case are independent of each other with little or no need to take advantage of package private methods or classes. Refactoring them so into their own subpackages gains little but does help reduce the clutter.

Developer changes to the system

When developers are tasked to make changes that are a bit more than trivial it seems silly that potentially they have changes that include files from all areas of the package tree. With the logical structured approach their changes are more local within the same part of the package tree which just seems right.

I totally follow and propose the logical ("by-feature") organization! A package should follow the concept of a "module" as closely as possible. The functional organization may spread a module over a project, resulting in less encapsulation, and prone to changes in implementation details.

Let's take an Eclipse plugin for example: putting all the views or actions in one package would be a mess. Instead, each component of a feature should go to the feature's package, or if there are many, into subpackages (featureA.handlers, featureA.preferences etc.)

Of course, the problem lies in the hierarchical package system (which among others Java has), which makes the handling of orthogonal concerns impossible or at least very difficult - although they occur everywhere!

It is an interesting experiment not to use packages at all (except for the root package.)

The question that arises then, is, when and why it makes sense to introduce packages. Presumably, the answer will be different from what you would have answered at the beginning of the project.

I presume that your question arises at all, because packages are like categories and it's sometimes hard to decide for one or the other. Sometimes tags would be more appreciate to communicate that a class is usable in many contexts.

From my experience, re-usability creates more problems than solving. With the latest & cheap processors and memory, I would prefer duplication of code rather than tightly integrating in order to reuse.

Both functional (architectural) and logical (feature) approaches to packaging have a place. Many example applications (those found in text books etc.) follow the functional approach of placing presentation, business services, data mapping, and other architectural layers into separate packages. In example applications, each package often has only a few or just one class.

This initial approach is fine since a contrived example often serves to: 1) conceptually map out the architecture of the framework being presented, 2) is done so with a single logical purpose (e.g. add/remove/update/delete pets from a clinic). The problem is that many readers take this as a standard that has no bounds.

As a "business" application expands to include more and more features, following the functional approach becomes a burden. Although I know where to look for types based on architecture layer (e.g. web controllers under a "web" or "ui" package, etc.), developing a single logical feature begins to require jumping back and forth between many packages. This is cumbersome, at the very least, but its worse than that.

Since logically related types are not packaged together, the API is overly publicized; the interaction between logically related types is forced to be 'public' so that types can import and interact with each other (the ability to minimize to default/package visibility is lost).

If I am building a framework library, by all means my packages will follow a functional/architectural packaging approach. My API consumers might even appreciate that their import statements contain intuitive package named after the architecture.

Conversely, when building a business application I will package by feature. I have no problem placing Widget, WidgetService, and WidgetController all in the same "com.myorg.widget." package and then taking advantage of default visibility (and having fewer import statements as well as inter-package dependencies).

There are, however, cross-over cases. If my WidgetService is used by many logical domains (features), I might create a "com.myorg.common.service." package. There is also a good chance that I create classes with intention to be re-usable across features and end up with packages such as "com.myorg.common.ui.helpers." and "com.myorg.common.util.". I may even end up moving all these later "common" classes in a separate project and include them in my business application as a myorg-commons.jar dependency.