In "Test-driven design, part 1th" and "test-driven design, part 2nd," I introduced how testing can achieve better design for new projects. In "Combining methods and SLAP," I discussed two key patterns-the combinatorial approach (composed method) and the single abstraction layer principle-to provide the overall goal for your code structure. These patterns need to be kept in mind. Once you have an existing software project, the main way to discover and utilize design elements is to refactor. In Martin Fowler's classic Refactoring, he defined refactoring as "a rigorous technique that reconstructs the existing code body, modifies the internal structure of the code, but does not affect the external behavior of the code". Refactoring is a kind of structural transformation with some purpose. For any project, it is commendable to have a code base that can be easily refactored. In this article, I'll discuss how to use refactoring techniques to find inadequately designed code that is hidden in a code base.
About this series
This series aims to introduce a new perspective on software architecture and design concepts that are often discussed but difficult to understand. With a concrete example, Neal Ford will help you lay a solid foundation in the evolutionary architecture and agile practices of emergent design. By deferring important architectural and design decisions to the final critical moment, you can prevent unnecessary complexity from reducing the quality of your software project.
Unit tests can provide the most important security barrier, allowing you to refactor the code base as you wish. If your project has a code coverage of 100%, you can safely refactor the code. If this level of testing has not yet been achieved, then a hasty refactoring becomes more dangerous. Localized modifications can be easily applied and immediately visible, but the damage caused by side effects can be very upsetting. The software generates unpredictable coupling points, and minor modifications to one part of the code can affect the entire code base, causing hundreds of of lines of code to fail. To safely modify the code and find a large number of errors, extensive unit testing is required. For a 2-year ThoughtWorks project, the technical director performed 53 different refactoring of the Code the day before the project was run. He is confident in refactoring because the project has extensive code coverage.
How do I implement a code base that can be significantly refactored? One way to do this is to not write any code until you add the test to the entire project. When you make this recommendation, you will be fired, and then you can go to another company that takes unit testing seriously. This method may not be very good. Another good approach is to get other members of the team to recognize the value of the test and start slowly adding tests around the most critical parts of the code. Plan and announce a date in the near future: "Starting from next Thursday, our code coverage will continue to grow." Each time you write a new code, add a test, and each time you fix a bug, write a test. By gradually adding tests around the most sensitive parts of the code (the new features and the parts that are prone to bugs), the test can play the most important role.
Unit tests examine atomic sex. But what if your code base doesn't have the idea of sticking to the combinatorial approach? In other words, if all your methods have dozens of or hundreds of lines of code, and each method performs a large number of tasks, what should you do? You can use the Unit test framework to write coarse-grained functional tests around these methods, focusing on the conversion of the input and output states of the method. This approach is not as good as unit testing, because behavior is not thoroughly tested, but it is better than not taking any action. For the really critical part of your code, you might need to add some functional tests before refactoring as a security guarantee.
The refactoring mechanism is simple, and all major Ides currently provide excellent refactoring support. The more difficult part is deciding what to refactor. This is the problem to be solved in the rest of this article.
Coupled with infrastructure
All developers in the Java world use the framework to launch development and deliver the best critical infrastructure (infrastructure that you do not need to write). But one of the dangers implied by frameworks, both commercial and open source, is that they always try to keep you tightly coupled, making it harder to find hidden designs in your code.
Both the framework and the application server provide a helper class that lures you into implementing a simpler development: if you are simply importing and using some of their classes, it becomes very easy to accomplish specific tasks. A typical example is Struts, which is a very popular open source Web framework. Struts includes a set of helper classes to help you deal with common problems. For example, if you allow your domain class to extend the Struts Actionform class, struts will automatically populate the form fields from the request, handle validation and lifecycle events, and perform other simpler behaviors. In other words, Struts offers a trade-off: using our classes will make your development work very easy. It encourages you to create a structure similar to the one shown in Figure 1:
Figure 1. Using Struts Actionform class