Clean Code
Chapter 11
Separate Constructing and Using a System
-
Software systems should separate the startup process
- When application objects are constructed and the dependencies are "wired" together
- The runtime logic that takes over after startup
- The following example has the startup process mixed in with the runtime logic
public Service getService() { if (service == null) { service = new MyServiceImpl(...); // Lazy initialisation } return service; }
-
However, MyServiceImpl is now a hard-coded dependency
- Can't compile without resolving these dependencies
- Testing is an issue if MyServiceImpl is a heavyweight object
- Need to ensure the test object is assigned to the service field before the method is called during unit testing
- Violating SRP by testing all execution paths, since there's construction logic mixed with runtime processing (testing null and block paths)
-
A simple solution is to move all aspects of construction to the main function
-
Easy to identify flow of control, objects are constructed and passed to the application which creates a uni-directional dependency between the main and application barrier
Dependency Injection
- Design pattern that facilitates loose coupling and improves testability
- Dependencies are "injected" into a class rather than being created or managed internally
- The application of Inversion of Control, moving secondary responsibilities from an object to other objects dedicated to the purpose, supporting SRP
- DI containers manage instantiation and wiring of dependencies, reducing the need for explicit instantiation in client code
- True DI is completely passive, the invoking object provides setter methods and/or constructor arguments that are used to inject dependencies
Cross-Cutting Concerns
- Cross-cutting concerns are features that impact multiple parts of the system
- Examples include logging, security and transactional management
- We can handle these concerns using aspects, modules or other framework libraries providing support for managing concerns to prevent duplication and ensure consistency
Java Proxies
- Java proxies help developers modularise cross-cutting concerns and separate them from the core application logic
- Allow us to create objects that intercept method invocations and perform additional logic before or after forwarding the invocation to the target object
- Suitable for simple scenarios, such as wrapping method calls in individual objects or classes
- However, dynamic proxies in JDK only work with interfaces
- Dynamic proxies are created at runtime and require the target object to implement at least one interface
- Static proxies are created at compile time by explicitly defining a proxy class that implements the same interface as the target object
Pure Java AOP Frameworks
- Most proxy boilerplate can be automated by tools
- Proxies are used internally in several Java frameworks, such as Spring AOP and JBoss AOP to implement aspects
- In Spring AOP, you write business logic as Plain-Old Java Objects which are purely focused on their domain
- POJOs don't have dependencies on enterprise frameworks or any other domains
- Conceptually, they are simpler and easier to test
AspectJ Aspects
- The most full-featured tool for separating concerns through aspects
- An extension of Java that provides "first-class" support for aspects as modularity constructs
- The pure Java approaches provided by Spring AOP and JBoss AOP are sufficient for most cases where aspects are useful, but AspectJ has more diverse tools
- The drawback of AspectJ is the need to adopt several new tools and learn new language constructs as well as usage idioms
- Though, adoption issues are partially mitigated by introducing "annotation form"
- Java 5 annotations are used to define aspects using pure Java code
Test Drive the System Architecture and Optimise Decision Making
-
An optimal system architecture consists of moduralised domains of concern, each of which is implemented with POJOs
- The different domains are intergrated together with minimally invasive Aspects or tools
- This architecture can be test-driven, just like the code
-
Modularity and separation of concerns make decentralised management and decision making possible
- No one person can make all the decisions, whether in a city system or software project system
-
At all levels of abstraction, the intent should be clear
-
When designing systems or single modules, always use the simplest thing that can possibly work