11.- Systems

19-02-2025 - Xavier Salvador

Chapter 11 applies the lessons of clean code to the system level. The same separation of concerns, single responsibility, and avoidance of tight coupling that guide functions and classes are equally important when designing the architecture of an entire system.

Separate System Construction from Use

Object construction and object use are two very different activities. Mixing them violates SRP and makes both harder to test.

The lazy-initialisation idiom is a common offender:

public Service getService() {
    if (service == null)
        service = new MyServiceImpl(/* hard-coded dependency */);
    return service;
}

This couples the class to MyServiceImpl and prevents substituting a test double. It also means the class has two reasons to change: the construction logic and the runtime logic.

The clean approach is to move all construction to main() (or a dedicated initialisation module) and pass fully constructed objects to the rest of the application. The application code never knows how objects are built.

Factories

When the application must decide when to create an object (not just at startup), the Abstract Factory pattern keeps that decision with the application while hiding the construction details:

public interface LineItemFactory {
    LineItem makeLineItem();
}
// Application calls factory.makeLineItem() at the right moment.
// LineItemFactoryImplementation lives on the "main" side of the boundary.

Dependency Injection

Dependency Injection (DI) is the most thorough mechanism for separating construction from use. The class declares its dependencies through constructor arguments or setter methods; a container or main() injects them. The class itself is completely passive.

Spring’s XML configuration wires beans together:

<bean id="bankDataAccessObject"
      class="com.example.banking.persistence.BankDataAccessObject"
      p:dataSource-ref="appDataSource"/>
<bean id="bank"
      class="com.example.banking.model.Bank"
      p:dataAccessObject-ref="bankDataAccessObject"/>

The Bank POJO receives its DAO through configuration, not through hard-coded instantiation. This is decoupled enough to swap implementations for tests.

Scaling Up

Cities grow incrementally. Roads start narrow and are widened over time; new infrastructure is added as demand grows. Software systems can do the same — if concerns are properly separated.

It is a myth that systems must be designed right the first time. The correct approach is to implement only today’s stories, then refactor and expand the system as new stories arrive. This is possible in software because architecture can grow incrementally, unlike physical structures.

EJB2: An Example of Invasive Architecture

EJB2 violated this principle by forcing business logic to depend on container types. A persistent Bank entity bean required:

  • A local or remote interface with every field exposed as getter/setter methods.
  • An abstract implementation class extending javax.ejb.EntityBean.
  • Lifecycle methods (ejbCreate, ejbActivate, ejbLoad, ejbStore, ejbRemove, etc.) required by the container but irrelevant to the business logic.
  • XML deployment descriptors for persistence and transactional semantics.

The result: tightly coupled code that was difficult to unit-test, impossible to reuse outside of EJB2, and which undermined OOP (beans could not inherit from other beans).

Cross-Cutting Concerns and AOP

Concerns like persistence, transactions, security, and logging cut across many objects. Every object needs to be persisted in the same way, yet the persistence code cannot live in every domain class without violating DRY.

Aspect-Oriented Programming (AOP) addresses this by defining aspects: modular constructs that specify, declaratively, which points in the system should have their behaviour modified.

Java Proxies can wrap individual objects but are verbose and only support interfaces. The proxy boilerplate for a simple Bank bean with two methods already fills a page.

Spring AOP handles proxy creation automatically. Business logic is written as POJOs; cross-cutting behaviour is declared in configuration or annotations:

<!-- persistence, transactions, caching declared here, not in domain objects -->

EJB3 adopted the same model: domain objects are plain POJOs annotated with JPA annotations, with no container-lifecycle methods required:

@Entity
@Table(name = "BANKS")
public class Bank implements java.io.Serializable {
    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "bank")
    private Collection<Account> accounts = new ArrayList<>();
    // ...
}

AspectJ provides the most powerful toolset for separating concerns at the language level, at the cost of adopting new syntax and tools.

Test-Driving the Architecture

When domain logic is written as POJOs decoupled from infrastructure by DI and AOP, the architecture itself becomes test-driveable. This makes Big Design Up Front (BDUF) unnecessary and harmful:

  • BDUF inhibits adapting to change by creating psychological resistance to discarding prior effort.
  • Architectural choices made too early are made with suboptimal information.

The correct approach: start with a naively simple but nicely decoupled architecture, deliver working stories, then add infrastructure on demand. Many large-scale Web systems have achieved high availability and performance this way.

An optimal system architecture consists of modularised domains of concern, each implemented with Plain Old Java Objects. The different domains are integrated with minimally invasive Aspects or Aspect-like tools. This architecture can be test-driven, just like the code.

Optimise Decision-Making

In a large system no single person can make all decisions. Modularity and separation of concerns enable decentralised management. Decisions should be postponed to the last responsible moment, when the most information is available. A premature decision is made with suboptimal knowledge.

Use Standards Wisely

Standards make it easier to reuse ideas, recruit experienced people, and wire components together. But the process of creating standards can take too long, and some standards lose touch with the needs they were meant to serve. Many teams adopted EJB2 because it was a standard, even when lighter-weight designs would have been sufficient. Do not adopt technology because it is a standard; adopt it when it adds demonstrable value.

Domain-Specific Languages (DSLs)

A good DSL minimises the communication gap between a domain concept and the code that implements it. When domain logic is expressed in the language of the domain expert, the risk of incorrect translation is reduced. DSLs raise the abstraction level above code idioms and design patterns, allowing intent to be expressed at the appropriate level of abstraction.

Key Rules

Principle Guideline
Separate construction from use Build in main(); inject into the application
Avoid invasive architecture Favour POJOs over container-bound objects
Decouple cross-cutting concerns Use AOP or declarative configuration
Design for evolution Start simple; add infrastructure on demand
Use standards wisely Adopt only when they add demonstrable value
Use DSLs Narrow the gap between domain language and code

Summary

Systems must be clean too. Invasive architectures that mix construction with use, or that couple domain logic to infrastructure, make testing difficult, slow delivery, and prevent organic growth. Separating construction from use — through DI containers, factories, and main()-driven wiring — keeps domain objects as simple, testable POJOs. Cross-cutting concerns belong in aspects, not scattered across domain classes. An architecture built this way can evolve incrementally, with decisions deferred until the last responsible moment, and can be test-driven just like individual classes and functions.

results matching ""

    No results matching ""