Here are some ways to put these ideas into practice.
Emergent design, sometimes called just-in-time design, is an advanced technique for incrementally building software. When done correctly, it can be a highly efficient way of building quality software. But it is not a beginner technique and requires a deep understanding in many areas. Here are seven strategies to help you master emergent design:
Just using an object-oriented language doesn’t make software object oriented. Most of the software written between the curly braces of a class statement is procedural. Good object-oriented code is made up of well-encapsulated entities that accurately model the problem it’s solving.
Design patterns are valuable for managing complexity and isolating varying behavior so that new variations can be added without impacting the rest of the system. Patterns are more relevant when practicing emergent design than when designing up front. You’ll find many more opportunities to apply patterns as you’re building software.
Beyond the safety net of having a suite of regression tests to support any changes to a system, when done correctly test-driven development supports good design principles and practices.
Refactoring is the process of changing one design to another without changing external behavior. It provides the perfect opportunity to redesign in the small or in the large with working code. I do most of my design during refactoring once I’ve already worked out what needs to be done. This allows me to focus on doing it well and so the right design can emerge.
Code quality underlies all good software. Without making code CLEAN—cohesive, loosely coupled, encapsulated, assertive, and nonredundant—it quickly degrades into legacy code that people are afraid to touch. Paying attention to code quality will reveal better ways of building more maintainable software that gives you design resilience and makes code easier to change.
It’s easy to get attached to a design, even when you don’t have all the facts. Knowing the limits of a design and being willing to change it as needed is one of the most important skills for doing emergent design.
The Agile practices of Scrum and Extreme Programming are valuable tools for doing design, but tools do not create designs. To create good designs, first understand the principles behind the practices and make good development practices into habits. That way, you’ll derive benefit from using them all the time.
Emergent design is about knowing your options as you build software and how to avoid painting yourself into a corner. When you understand and are able to use good development practices, you’ll have the ability to easily change designs with the confidence that you can handle any changes in the future. This makes developing software less stressful and more fun.
Okay, you got the time and management approval to clean up some code. What do you do with it? Refactoring legacy code can be like unraveling a knotted rope, and it can be hard to figure out where to start. Here are seven strategies for cleaning up code.
Write code clearly using intention-revealing names so it’s obvious what the code does. Make the code self-expressive and avoid excessive comments that describe what the code is doing. When I see a lot of comments explaining code, I wonder if the developer who wrote it was nervous I might not understand the code just by reading it.
One of the most valuable things to do with legacy code is to add tests to support further reworking of the code. But often, legacy code is so intertwined it’s difficult to isolate what needs to be tested. In his book Working Effectively with Legacy Code [Fea04], Michael Feathers shares a series of techniques for adding seams to make legacy code more testable. These techniques make software more independent and simpler to test.
Perhaps the two most important and useful refactorings are Extract Method and Extract Class. Methods are often made to do too much. Other methods and sometimes entire classes can be lurking in long methods. Break up long methods by extracting new methods from little bits of functionality that you can name. Uncle Bob Martin says that ideally methods should be no longer than four lines of code. While that may sound a bit extreme, it’s a good policy to break out code into smaller methods, if you can write a method name that describes what you’re doing.
Another typical problem with legacy code is that classes try to do too much. This makes them difficult to name. Large classes become coupling points for multiple issues, making them more tightly coupled than they need to be. Hiding classes within classes gives those classes too many responsibilities and makes them hard to change later. Breaking out multiple classes makes them easier to work with and improves the understandability of the design.
As classes and methods become more cohesive, it’s possible for business rules to become spread out across a system, making it difficult to read and modify. Try to centralize the rules for any given process. Extract business rules into factories if at all possible. When decisions are centralized, it removes redundancies, making code more understandable and easier to maintain.
Introduce polymorphism when you have a varying behavior you want to hide. For example, I may have more than one way of doing a task, like sorting a document or compressing a file. If I don’t want my callers to be concerned with which variation they’re using, then I may want to introduce polymorphism. This lets me add new variations later that existing clients can use without having to change those clients.
An important part of making polymorphism work is based on clients using derived types through a base type. Clients call sort() without knowing which type of sort they’re using. Since you want to hide from clients the type of sort they’re using, the client can’t instantiate the object. Give the object the responsibility of instantiating itself by giving it a static method that invokes new on itself, or by delegating that responsibility to a factory.
Refactoring code is a necessary part of development. It drops the cost of accommodating new features. They say hindsight is 20/20, and refactoring code can take advantage of that fact to clean up your design and make code more maintainable.