One popular approach is to subcontract projects at a technical level. But this approach has some drawbacks. Instead, we can subcontract by function and create independent, autonomous packages. The result is an easy-to-understand and error-resistant code base.

The overall analysis

Disadvantages caused by technical subcontracting:

  1. A poor overview of all the classes that belong to a function.
  2. Generic code, reusable code, and complex code tend to be difficult to understand, and changes can easily break other functional use cases because of the difficulty in grasping the impact of changes.

Create a package that contains all the classes required for a function by subcontracting by function. The benefits are:

  1. Better discoverability and overview
  2. Independent and self-governing
  3. Simpler code
  4. testability
  5. Facilitate collaborative team development

Subcontract in layers according to technology

A very popular approach to project structure is layer by layer subcontracting. This will provide a package for each technology group class.

All classes are grouped technically by layers of subcontracting

Let’s add the invocation hierarchy to the picture to make it “clear” which class depends on which other class.

The invocation hierarchy spans the entire project and involves many packages

So what are the downsides of subcontracting by layers?

  1. Poor feature overview. Often, when we work with code in a project, the first thing we think about is the specific area or function we want to change. So we’re going to look at it from a domain perspective. Unfortunately, layering by technology forces us to transition from one package to another to get an overview of functionality.
  2. The trend toward generic, reusable, and complex code.Typically, this approach results in the central class containing all methods for each functional use case. Over time, these methods became more abstract (with additional parameters and generics) to accommodate more cases. The only example in the figure above is ProductDAO, where the ProductController and ExportController methods are placed. The result is:
    • As more methods are added, the class becomes larger. So it’s hard to understand just by the amount of code.
    • Changing common reuse code is dangerous. Although you only want to work with one use case, you can easily break all of them.
    • Abstract and generic methods are difficult to understand for two reasons: First, to be generic, other technical constructs are often required (for example, switches, parameters, generics), which makes it more difficult to see the business logic relevant to the current use case. Second, the cognitive requirements are higher, because you have to understand all the other use cases to make sure you don’t break them.

As Sandi Metz points out:

“I felt LIKE I had to know everything to help.” Sandi Metz. See my post for our coding Wisdom Wall.

⚠️ : We reached DRY, but violated KISS.


Subcontracting according to function (feature)

Let’s rearrange these classes into separate feature packs.

User management feature pack

The new package, userManagement, contains all the classes that belong to this functionality: controllers, DAOs, Dtos, and entities.

👆 Product management function package

The new package productManagement contains the same class type, along with StockServiceClient and the corresponding StockDTO. This fact makes it clear that the inventory service is used only by product managers. UserManagement and productManagement use different domain entities and tables. It’s easy to separate them into different packages. But what happens when one function needs a domain entity that is similar or even identical to another function?

Product export function pack

Now, it’s getting more and more interesting. The exportProduct package also handles product entities, but with different functional use cases. Our goal is to have a self-contained feature pack. Therefore, exportProduct should have its own DAO, DTO, and entity classes, even if they look similar to the classes in productManagement. Resist the urge to reuse classes in productManagement.

  • We can use structures (Dtos, entities) that are tailored to export use cases. They contain only related fields, and entities can be created based on queries with good projections of related columns – nothing else.
  • The dedicated ExportProductDAO contains queries and predictions that are specific to the exit function.

We may have to write more code again, but we end up in a very favorable situation:

  • Changes in productManagement never break exportProduct code and vice versa. They can develop independently.
  • When changing code, we just need to keep the current functionality in mind.
  • The code itself becomes easier to understand because it is not generic and does not have to be used in both use cases.

The above feature pack is great, but in reality, we will always need a universal package.

Generic packages contain technical configuration and reusable code

It contains technical configuration classes (for example, for DI, Spring, object mapping, HTTP clients, database connections, connection pooling, logging, thread pooling) and it contains useful code snippet that can be reused. But be very careful about premature abstraction of your code. I always start by putting the code as close to its usage as possible, which is feature packs, or even use classes. Only move fragments to a generic package if they actually have more use (⚠️ : than I think they might in the future). The three laws provide a good guide. It might make sense to find all entities in a generic package. We also did this for some projects, many of which use the same entities over and over again. Some developers also want to put all entities in a central place so they can see the mapping of the database schema as a whole. At the moment, I’m not dogma, because both positions of the entity can make sense. Initially, however, I always moved as much code as possible into feature packs and relied on custom use-case-specific entities and projections.


Big picture

Finally, our big picture looks something like this:

Big picture of subcontracting by function

benefits

Let’s briefly summarize the benefits:

  1. Better discoverability and overview from a domain perspective. Most of the code belonging to business functions is located together. This is critical because we usually access the code base with a business requirement in mind.
  2. Independent and self-governing. Most of the code required for functionality is in a package. Therefore, we avoided relying on other feature packs. The upshot: when we develop features, we’re less likely to break other features. Less cognitive ability is required to estimate the impact of change. Usually, we just need to remember the current package.
  3. Simpler code. Because we avoided generic and abstract code, the code became simpler because it only had to deal with one use case. As a result, the code is easier to understand and improve.
  4. Testability. In general, classes in a feature pack have fewer dependencies than “God classes” in a technology pack that tries to satisfy all use cases. Therefore, testing is easier because we can create fewer test dependencies.

disadvantages

  1. We have to write more code.
  2. We might write code like this many times.
  3. It’s hard to decide when it’s best to move code to a generic package and reuse it. When in doubt, the Three Laws are useful. I want to emphasize that reuse is still allowed and useful.
  4. Figuring out the right range and size of a feature pack can also be tricky. See the Questions section for more information.

However, I think the advantages outweigh the disadvantages.


The principle behind it

The principles of the proposed approach to functional subcontracting are apt:

KISS > DRY

Again, I want to quote Sandi Metz.

“I felt LIKE I had to know everything to help.” Sandi Metz. See my post for our coding Wisdom Wall.


The method of functional packaging

Our team documented the coding guidelines and principles it followed. The section on functional subcontracting is as follows: We are based on functional subcontracting. Each feature pack contains most of the code needed to provide that feature. Each feature pack should be independent and autonomous.

  1. This approach affects all layers. For example, each package has its own DAO and client. There should be no huge DAO gods.
  2. A package should have only a few relationships to other packages. All the logical things needed for this functionality should be in the package.
  3. Rule of thumb: If you want to remove functionality, just remove the corresponding package.
  4. However, it is possible to reuse things in a generic package, but it should only contain code that is used more than once (see The Three Laws). It should not contain business logic. But technically useful is ok.
  5. If feature-specific Spring beans exist, we will put their configuration in the feature pack.

The problem

What is the structure in the feature pack?

This depends on the size of the project and feature pack. For small and medium-sized projects, I like to avoid defining rules that might add more ritual than value (for example, requiring certain interfaces and subpackages to be defined). As long as you build independent, autonomous packages that derive from your particular business domain, you’re on the right track. If you are dealing with a larger code base, you may need to define more rules about the structure and manner of subpackages that allow one feature package to access another. The concept of “modules” or “components” rather than “feature packs” may be more helpful. For example, Tom Hombergs suggests adding apis and internals to each component package that define which parts of the component are allowed to be used by other components. For more information, see his article “Cleaning architectural boundaries with Spring Boot and ArchUnit.”

Will I end up writing the same code over and over again?

Yes, there will be some duplication, but based on my experience, you probably won’t believe that much code that is 100% the same. Similar code is usually different because it covers different use cases. For example, the two methods can query products by product name, but they differ in planned fields, ordering, and other criteria. Therefore, it is best to separate the methods into different packages. And copying itself is not evil. I like to apply the three laws before I start extracting code into a general reuse approach. Finally, I want to emphasize that it is still permissible, and sometimes even reasonable, to use reusable code centrally, but these situations are no longer so common.

Can Kotlin support this approach?

The subcontracting method is language independent. But Kotlin makes it easy to follow: Using data classes, it takes only a few lines to write a customized function-specific structure (such as A DTO or entity), rather than templates. Kotlin allows you to put multiple classes in a single file. Thus, we can make a tos.kt or entities.kt file that contains all of the data class definitions a single tos.kt or entities.kt file, rather than an entity with a subpackage DTO or many Java files containing each POJO class.

Translation of this article:Phauer.com/2020/packag…


🌟 🌟 🌟 🌟 🌟 🌟 🌟 🌟 🌟 🌟 🌟 🌟 🌟 🌟 🌟 🌟 🌟 🌟

Welcome to my blog:blog.dongxishaonian.tech

Pay attention to the author’s public account, push all kinds of original/high quality technical articles ⬇️