I've been refactoring the StructureMap codebase in preparation for wrapping up the 2.5 release. I've been doing this work with a couple goals in mind.
- Experimentation. I'm purposely revisiting some code just to see if I can come up with a better solution and structure.
- Improving the structural quality of the code in order to make the code more approachable to others.
- Some of the code is flat out embarrassing.
- Changing the structure to allow for easier extensions. There's a couple specific new pieces of functionality that I thought would be easier to implement if I did some refactoring first.
- To more closely align the architecture with its functionality and usage. StructureMap was originally envisioned as a generic mechanism to build an object graph from some sort of Xml representation. Today, we do vastly more things with an IoC tool than I had in mind back in the summer of 2003.
As the effort proceeds, I've been thinking about the reasons that we do refactoring, or more importantly, the reasons that justify refactoring. When the topic of refactoring as a described practice was first being broached earlier in this decade, I read a lot of people upset over the idea of refactoring. Refactoring was variously described as:
- A result of insufficient upfront design (which I still think is silly because refactoring is often a result of doing upfront design too early and/or getting that upfront design wrong)
- Undisciplined hacking
- A dangerous activity that needlessly risked destabilizing working code (a very real fear that can nevertheless be mitigated by good automated test coverage with well written code and well written tests)
- A waste of resources. Useless goldplating, or as my buddy Chad Myers would say, "polishing the nosecone."
That being said, what are the responsible reasons that lead us to refactor our code? I can think of two major reasons.
- To remedy a deficiency in code, design, or architectural quality.
- We shouldn't declare a coding task complete until it reaches the quality demanded by the team. These refactoring's are generally small, cheap items like renaming methods and extracting methods that can be performed safely and quickly with an automated tool.
- I'm doing a lot of work on StructureMap as a result of static analysis results from NDepend. NDepend helpfully pointed out some flaws in the class structure that I've since remedied. In specific, I've been working on reducing the complexity of any class with a high Cyclomatic Complexity (all the classes are now under 30 with a few exceptions that I'm ignoring because the methods are all simple). I would also worry about efferent and afferent coupling plus the size of the classes and methods in your system.
- To make a forthcoming change to the code easier.
- If I'm changing a big method or complex class I'll often start by refactoring towards a Composed Method by extracting methods or even smaller, more focused classes. On a project last year I did some quick renaming of variables from "m0, m1, m3" to more descriptive names. My goal in these cases is to simply make the existing code easier to understand *before* I start to make modifications.
- For a new feature, I might need part of the functionality of an existing method or a class, but not the rest of it. Rather than duplicate that functionality by copying and pasting, I'll extract the functionality to be shared into a new class or method so that it can be easily reused by the code for the new feature.
- In several cases, I've wanted to change one aspect of the behavior of an existing class while leaving the rest of the class's behavior intact. In that case, I think the class is violating the Single Responsibility Principle, and I've split the class up along responsibility lines to isolate the changes to a smaller surface area.
- Before making a change, you may need to decrease or even eliminate coupling on an existing code structure. In StructureMap, I've frequently found myself wanting to change the internal structure of a class for various reasons, but first being blocked because other classes are coupled to internal details of the existing code structure. In many cases, the culprit has been a Law of Demeter violation.
- Introducing new abstractions often gives us a new seam in the class structure that we can exploit to add new behavior with minimal change to existing classes
- If we have to change any functionality that has duplicated representations in the code, it's often worth the while to first refactor the code to eliminate that duplication before making the change.
Either way, you're main goal with refactoring is to make the code easier to work with in the future. In some cases (the second category), that desired change is immediate. In the first category, you're simply keeping the code cleaner to handle any type of extension and make the system easier to understand. In regards to Lean thinking, you only refactor on a project when the proposed refactoring will lead to better throughput of the development work. If a structural problem in the code isn't causing any immediate harm or friction, you probably leave it alone for now.
Any thoughts? What did I miss?