Page 3 of 3
While there is some research into automated architectural refactoring, it is not clear whether this will result in robust tools anytime soon. For now, reorganising dependencies has to be done manually. Refactoring has to address two problems:
Patterns and metrics will provide some guidance to locate the dependencies to be removed. They will help to identify important and critical dependencies. To measure the importance of a dependency, standard network analysis metrics such as edge betweenness can be used. However, important dependencies are not necessary critical dependencies. A possible approach to find critical edges is to measure in how many antipatterns a dependency participates. To illustrate this approach, consider the following program:
- Which dependencies should be removed?
- How can these dependencies be removed?
There are several antipattern instances in this design: two circular dependencies (between packages 1, 2, 3 and 2,3) and a subtype knowledge instance (B indirectly uses its subtype A). The dependency “B uses A” is part of all three antipattern instances, and therefore has a antipattern score (apsc) of 3. By removing this dependency, all antipattern instances disappear. We have performed some experiments that show that this approach is promising: by removing a small number of dependencies most antipattern instances disappear, reflecting a much better modular design of the refactored system. The Massey Architecture Explorer computes betweenness and antipattern participation score for all dependencies.
The second problem, how to break dependencies, is harder. There are several refactoring patterns that can be applied:
- Type abstraction. For instance, a method parameter type java.util.ArrayList can often be changed to java.util.List or even java.util.Collection without breaking the code. This may break the dependency to an implementation type (java.util.ArrayList in this case). This refactoring requires that only members of the subtype that are also defined in the supertype are referenced. Type abstraction is potentially recursive (for instance, if references to the parameter are leaked to other methods), and verifying pre-and post conditions can be tricky.
- Use dependency injection (DI) or a service locators (aka service registries). For instance, consider the following code snippet: java.util.List list = new java.util.ArrayList(). Using dependency injection, the value of list is set by a DI container at runtime and the class would not depend on java.util.ArrayList. A service registry works similar - the class would ask the service registry for an instance of java.util.List, avoiding a direct reference to java.util.ArrayList. There are various DI frameworks available such as Spring and Guice. Examples for service registries include the Eclipse extension registry and the java.util.ServiceLoader utility that is part of the JDK. Many Java APIs have custom built-in service registries to minimise dependencies on particular implementations. Examples include the JDBC driver manager, the JAXP pluggability layer and the JNDI service provider interface.
- Relocating classes and packages. Sometimes, patterns such as circular dependencies are caused by classes being in the wrong package and packages being in the wrong jar. In this case, a straight-forward “move class” refactoring (supported by many IDEs) can be used.
- Inlining. Finally, inlining can be used to move or copy parts (=members) of a class that is the target of a dependency into the class that depends on these parts. This only works if those parts are not coupled to other parts of the respective artifact (class or package). Copying creates redundancies and should be used with care.
In general, architectural refactoring is complex and requires great care. It is sometimes not straight forward to verify preconditions that should be satisfied before a refactoring is performed. This is in particular the case if dynamic programming techniques such as reflection, multiple classloaders, dependency injection or aspect-oriented programming are used.
After each refactoring, postconditions should be checked. This includes the following steps:
- Check whether the program can still be compiled and built.
- Check whether the refactoring was behaviour-preserving. Usually this is verified by running tests. This is easier if the program has a high test coverage.
- Check whether the architecture has actually improved by reassessing this using patterns and metrics as described above.
Once the dependencies have been refactored, modules can be built. This also requires the definition of modules in build scripts, and the definition or generation of module meta data. There is some tool support emerging in this area such as Spring Bundlor and BND.
This article first appeared in D Zone's Architect Zone and is re-published here with permission of the author, Jens Dietrich, creator of the Massey Architecture Explorer.