<<Clean Code>> Quotes: 11. Systems
How Would You Build a City?
Separate Constructing a System from Using It
Software systems should separate the startup process, when the application objects are constructed and the dependencies are “wired” together, from the runtime logic that takes over after startup.
The startup process is a concern that any application must address… The separation of concerns is one of the oldest and most important design techniques in our craft.
Lazy initialization/evaluation:
public Service getService() {
if (service == null)
service = new MyserviceImpl(...);
return service;
}
One occurrence of LAZY_INITIALIZATION isn’t a serious problem, of course. However, there are normally many instances of little setup idioms like this in application Hence, the global setup strategy (if there is one) is scattered across the application, with little modularity and often significant duplication.
If we are diligent about building well-formed and robust systems, we should never let little, convenient idioms lead to modularity breakdown. The startup process of object construction and wiring is no exception.
Separation of Main
One way to separate construction from use is simply to move all aspects of construction to main, or modules called by main, and to design the rest of the system assuming that all objects have been constructed and wired up appropriately.
This means that the application has no knowledge of main or of the construction process. It simply expects that everything has been built properly.
Factories
Sometimes, of course, we need to make the application responsible for when an object gets created.
I this case we can use the ABSTRACT FACTORY pattern to give the application control of when to build the LineItems, but keep the details of that construction separate from the application code.
This means that the application is decoupled from the details of how to build a LineItem. Any yet the application is in complete control of when the LineItem instances get built and can even provide application-specific constructor arguments.
Dependency Injection
A powerful mechanism for separating construction from use is Dependency Injection(DI), the application of Inversion of Control(IoC) to dependency management. In the context of dependency management, an object should not take responsibility for instantiating dependencies itself. Instead, it should pass this responsibility to another “authoritative” mechanism, thereby inverting the control.
During the construction process, the ID container instantiates the required object(usually on demand) and uses the constructor arguments or setter methods provided to wire together the dependencies. Which dependent objects are actually used is specified through a configuration file or programmatically in a special-purpose construction module.
First, mostDI containers won’t construct an object until needed. Second, many of these containers provide mechanisms for invoking factories or for constructing proxies, which could be used for LAZY-EVALUATION and similar optimizations.
Scaling Up
We should implement only today’s stories, then refactor and expend the system to implement new stories tomorrow. This is the essence of iterative and incremental agillity. Test-driven development, refactoring, and the clean code they produce make this work at the code level.
Notice that logic for adding a new account. It is common in EJB2 beans to define “data transfer object” (DTOs) that are essentially “structs” with no behavior. This usually leads to redundant types holding essentially the same data, and it requires boilerplate code to copy data from on object to another.
Cross-Cutting Concerns
Note that concerns like persistence tend to cut across the natural object boundaries of a domain, You want to persist all your objects using generally the same strategy.
In principle, you can reason about your persistence strategy in a modular, encapsulated way. Yet, in practice, you have to spread essentially the same code that implements the persistence strategy across many objects.
In fact, the way the EJB architecture handled persistence, security, and transactions, “anticipated” aspect-oriented programming(AOP), which is a general-purpose approach to restoring modularity for cross-cutting concerns.
In AOP, modular constructs called aspects specify which points in the system should have their behavior modified in some consistent way to support a particular concern. This specification is done using a succinct declarative or programmatic mechanism.
Java Proxies
Using one of the byte-manipulation libraries is similarly challenging. This code “volume” and complexity are two of the drawbacks of proxies. They make it hard to create clean code! Also, proxies don’t provide a mechanism for specifiying system-wide execution “points” of interest, which is needed for a true AOP solution.
Pure Java AOP Frameworks
You incorporate the required application infrastructure, including cross-cutting concerns like persistence, transactions, security, caching, failover, and so on, using declarative configuration files or APIs. In many cases, you are actually specifying Spring or JBoss library aspects, where the framwork handles the mechanics of using Java proxies or byte-code libraries transparently to the user. These declarations drive the dependency injection(DI) container, which instantiates the major objects and wires them together on demand.
The client believes it is invoking getAccounts() on a Bank object, but is actually talking to the outermost of a set of nested DECORATOR objects that extend the basic behavior of the Bank POJO. We could add other decorators for transactions, caching, and so forth.
AspectJ Aspects
The drawback of AspectJ is the need to adopt several new tools and to learn new language constructs and usage idioms.
Test Drive the System Architecture
If you can write your application’s domain logic using POJOs, decoupled from any architecture concerns at the code level, then it is possible to truly test drive your architecture.
However, we must maintain the ability to change course in response to evolving circumstances.
A good API should largely disappear from view most of the time, so the team expends the majority of its creative efforts focused on the user stories being implemented. If not, then the architectural constraints will inhibit the efficient delivery of optimal value to the customer.
An optimal system architecture consists of modularized domains of concern, each of which is implemented with Plain Old Java( or other) Objects. The different domains are integrated together with minimally invasive Aspects or Aspect-like tools. This architecture can be test-driven, just like the code.
Optimize Decision Making
We all know it is best to give responsibilities to the most qualified persons. We often forget that it is also best to postpone decisions until the last possible moment.
A premature decision is a decision made with suboptimal knowledge.
The agility provided by a POJO system with modularized concerns allows us to make optimal, just-in-time decisions, based on the most recent knowledge. The complexity of these decisions is also reduced.
Use Standards Wisely, When They Add Demonstrable Value
Standards make it easier to reuse ideas and components, recruite people with relevant experience, encapsulate good ideas, and wire components together. However, the process of creating standards can sometimes take too long for industry to wait, and some standards lose touch with the real needs of the adopters they are intended to serve.
Systems Need Domain-Specific Languages
Conclusion
At all levels of abstraction, the intent should be clear.
Whether you are designing systems or individual modules, never foret to use the simplest thing that can possibly work.