ACE Dev - Adaptive Compositional Evolutionary Development

Jakob Jenkov
Last update: 2023-02-01

ACE Dev, or Adaptive Compositional Evolutionary Development is the term I use to describe the development style I have adopted since I started doing object-oriented programming professionally in 1999. ACE dev consists of a simple set of principles which when applied will often naturally lead to flexible and maintainable code - in my experience.

The individual elements within ACE are not all my invention. Most of the ideas I have learned from experts and veterans via books, presentations and by simply working with them. Even the one or two ideas, or twists on ideas, I feel might be "mine", are most likely inspired by others - and most likely others have reached the same conclusions independently.

My contribution probably mostly consists of the synthesis of all the techniques I have learned into a simple set of guiding principles. When you understand these principles, I find that it is easier to figure out when to use which design patterns, when to use dependency injection etc. I also find it easier to decide when and how to let agile principles such as YAGNI and MVP (Lean Startup) influence your code design.

ACE Dev Destilled

Before I get into the detail, let me just summarize the main principles of ACE Dev, so you can see how simple the principle set is:

  • Adaptive (A)
    • Adapt to Requirements (R)
    • Adapt to Context (C)
  • Compositional (C)
    • Narrow Responsibility (N)
    • Widen Applicability (W)
    • Reduce Required Effort (R)
  • Evolutionary (E)
    • Adapt design to this delivery (T)
    • Adapt design to next delivery (N)

That is really it!

Of course there are more details under each of these principles - but the above list is the core of ACE. I will explain each of these principles in more detail in the later sections. I will first explain adaptive development - and then evolutionary development - and then finish off with compositional design as I have much more to say about compositional design techniques than the first two aspects.

Here is an illustration that sums up the 3 + 6 core principles in an easy-to-remember diagram. The inner circles match Adaptive (A), Compositional (C) and Evolutionary (E). The next levels match the levels below in the above list.

ACE Dev - Adaptive Compositional Evolutionary Development - Overview

Adaptive Development

Adaptive development to me means adapting the design (and process, for that matter) to the concrete requirements (needs) and context of the software project. Since different software projects have different needs and contexts, it does not make sense in my opinion to use a fixed set of rules or doctrines to steer the design. You use the techniques and tools that enable the software and code base to meet your needs.

Being adaptive means you can freely mix programming paradigms (FP, OOP, DOP etc.) as you see fit. Don't feel that you have to abide by a specific paradigm - just because it "feels more pure" to do so. We don't write software to adhere to doctrine. We write software to solve problems.

A natural consequence of letting needs drive design and process is to down-prioritize programming paradigm doctrine. Use the doctrines as guidelines - but deviate from them when your needs require it. In fact, when it comes to the classical OOP doctrines you might benefit from completely ignoring them. I will into more detail about that in the section about Compositional Design.

Being adaptive also means that you adapt the design and process to the context of the software. Typically, the context includes factors such as:

  • Code base size
  • Expected lifetime of software
  • Number of developers working on the software
  • Potential revenue of software
  • Consequence severity of errors in the software
  • etc.

Typically, the smaller the code base is, the shorter the lifetime and the fewer developers etc. - the less "structure" is needed in the code base - meaning you can get away with a less well-designed code base. Contrarily, the larger the code base, the longer the lifespan and the more developers etc. - the more structure you will need to keep continued development flowing steadily.

Evolutionary Development

Evolutionary development to me means splitting up the development of a larger system into many subsequent smaller deliveries. The internal design of the code base is adapted primarily to the needs of the current and perhaps the next one or two deliveries.

I try to avoid designing for requirements too far into the future - unless I know for sure what the software will look like at that time. Requirements may change as I learn more about how the software actually works in practice. You may not need the features you thought you would need. This is where the classic YAGNI principle (You Aren't Going to Need It) is a useful guiding principle.

A natural consequence of not designing for the future is that I may have to realign the design of the software with the needs of future releases - when I get to them. I try not to let myself be stuck with a too old, inappropriate design.

When breaking the development into smaller deliveries, I try to start with as small a delivery as makes sense. This is where the MVP (Minimum Viable Product) from the Lean Startup philosophy is useful. From this first delivery, I evolve the software in small deliveries, one by one. That is what makes it an evolutionary process.

Compositional Design

By compositional design I mean designing your code base to consist of small pieces that can be composed together into larger structures - enabling you to solve many similar problems with the same components. Compositional design techniques exist in both functional programming (FP) and object oriented programming (OOP).

In FP compositional design comes in the shape of functional composition - meaning functions that are composed of other functions. Functional composition is typically done by passing one function to a second function - which then returns a third function as result. This third function calls the first function as part of its code. There is plenty of literature about functional composition, so I will not get into more detail about this here. My focus will be on object-oriented composition instead - because I feel this is not as well covered.

In an object-oriented design it is often less obvious how to construct the classes and objects in a way that makes the code base easy to work with. There does not seem to be any official OOP guidelines (that are part of official OOP canon) that actually work well in practice. Therefore, I have written down the OOP composition techniques I use myself. These techniques seem to be working reasonably well for me most of the time.

Decompositional Expansion + Compositional Contraction

A common argument against splitting a code base up into many small components is, that the code base gets confusing and much harder to navigate with all the extra files the splitting results in.

First of all, having a lot of files in a code base is not a big deal if you are using a modern IDE such as IntelliJ IDEA. Modern IDEs have functionality that makes it easy to navigate even large code bases with many files.

Second, I find that I typically do not end up with an over-abundance of files using compositional design. In fact, I find that I often end up with a smaller, more elegant code base than I do otherwise. To understand why - we have to look at two phenomena that occur in compositional design:

  • Decompositional Expansion
  • Compositional Contraction

At first, when you start decomposing your larger components into smaller components your code base will expand. This is what I call the decompositional expansion.

Later, when you start being able to compose the new code using existing components, your code base grows a lot slower than it would have if you had not designed it for composition. The is what I call the compositional contraction.

When trying out compositional design you have to stick with it long enough to get past the decompositional expansion - to experience the compositional contraction that comes later.

Compositional Design Techniques

The two main principles in my compositional design philosophy are to "narrow the responsibility" and "widen the applicability" of components. However, there are more principles nested under these two major categories. Here is my list of OOP design principles which I refer to as compositional OOP:

  • Narrow Responsibility
    • Split domain logic from domain logic
    • Split domain logic from non-domain logic
    • Separate state from action
    • Separate action from context
  • Widen Applicability
    • Use more general types in methods in interface
    • Add more methods matching more types
    • Design for dynamic composability (dependency injection)
    • Design for extensibility
    • Design for pluggability
    • Design for configurability
  • Reduce Required Effort

I will explain each of these principles in more detail in the following sections.

Narrow Responsibility

The first principle of compositional design is to look for ways to narrow the responsibility of the current components of your system. When narrowing the responsibility of a component - you typically end up splitting bigger classes or methods into multiple smaller classes or methods - each with narrower responsibilities.

By narrowing the responsibility you can often increase the probability of the resulting classes or methods being reusable. Higher degrees of usability tend to lead to a smaller, less error-prone code base as the project grows in functionality.

Narrowing the responsibility of a component also tends to make it easier to test - as there is less behaviour (responsibility) to test. It may also make it easier to create an instance of the component in the state you need for your tests. Easier to configure the component - in other words.

The narrow responsibility principle is also sometimes referred to as single responsibility, but I find that to be a too strict formulation of the principle. What exactly does a single responsibility look like? What is its delimitation? The term narrow conveys the same idea, but without feeling absolutist.

There are many ways you can narrow the responsibility of a class or method. I will not pretend that I know them all - but I do know a good handful that I use regularly. I will explain some of them in the following sections.

Split Domain Logic From Domain Logic

A common way to narrow the responsibility of component is to split its domain logic responsibility into multiple domain logic components. You might have a component that does both A and B, but now you need the B functionality from a new C component.

I also sometimes refer to splitting domain logic from domain logic as a vertical split. This comes from the tendency to draw architecture diagrams with domain logic layered on top of non-domain logic layers. When splitting a domain logic component in such a diagram it would become two components positioned within the same layer. It would look as if the larger component had been "split vertically" into two adjacent components.

Split Domain Logic From Non-Domain Logic

Another common way to narrow the responsibility of a component is to split its domain logic from non-domain logic. For instance, a component performing a domain-specific computation on a file may also contain the logic needed to load the file into memory. Such a component could have its logic split into 2 components: One component performs the domain specific computation, and one component that loads the file into memory (non-domain specific logic).

The component that loads the file into memory might be reusable from another domain-specific component that also needs to operate on file data.

I also sometimes refer to splitting domain logic from non-domain logic as a horizontal split. This comes from the tendency to draw architecture diagrams with domain logic layered on top of non-domain logic. When splitting a larger component into the domain specific part and non-domain specific part in such a diagram it would become two components positioned in different layers, the domain specific component on top of the non-domain specific component.

Jakob Jenkov

Featured Videos





Core Software Performance Optimization Principles

Thread Congestion in Java - Video Tutorial












Advertisements
Close TOC

All Trails

Trail TOC

Page TOC

Previous

Next