SOLID Agile Development
Agile development is about more than churning out code quickly; it also forces you to think about writing software that's more extendable, robust and maintainable.
- By Peter Provost
Most of the recent writings about Agile development focus on what I'll call "process stuff." You see articles about work item and backlog management, burndowns and burn-ups. There are books and articles about Kanban and Scrum, collaboration and communication, culture, and scaling to the enterprise.
What you don't find very much of anymore is guidance about actually writing code in a way that supports Agile processes. Sure, you see people saying, "You should do test-driven development (TDD)," but there isn't much about the what, why and how of writing good code in an Agile world. It's almost as if this part of it is assumed.
In this article I'll focus on what I think is the key element of being a developer in an Agile environment. It involves unit testing and testability (one of my passions), but the real focus is on how you think about your code, its design, and the idioms and patterns you use when writing code.
Developers on Agile teams are in an interesting position that's quite different from those on traditional or waterfall teams. In a traditional project management model, the requirements and plans are set before development begins, so you're in a position to see the road ahead of you, and also have reasonable confidence that things won't change drastically in the middle.
But the whole point of Agile methods is to enable and embrace the idea of change. On an Agile project, you expect to get changing requirements at any point in the project cycle. But how do you put yourself and your code in a position where you can happily say yes to a new requirement or change?
From reading the Agile lore, you might think that the only answer people give to this is, "Always do TDD and pair programming." But this answer is incomplete. You can do TDD (badly) and pair programming (ineffectively) and not end up in the right place. Alternatively, you can end up in the right place even if you don't do TDD and pair programming, so clearly there's something else that's really the key element.
I believe the answer to that is a set of things called the SOLID design principles (Single responsibility, Open-closed, Liskov substitution, Interface segregation and Dependency inversion).
SOLID Design Principles
The acronym SOLID was coined by Michael Feathers as a mnemonic device to help remember the first five object-oriented design principles collected by Robert C. Martin (aka Uncle Bob) in the late 1990s. These five basic principles are intended to help you create a system that's more robust, maintainable and easier to extend over time.
Table 1 lists the SOLID design principles.
||Single Responsibility Principle
||Liskov Substitution Principle
||Interface Segregation Principle
||Dependency Inversion Principle
Table 1. The first five principles of object-oriented design.
I'll review each principle, what it means and why it matters.
- Single Responsibility Principle: This principle is all about cohesion, which is a measure of how closely related two things are, and consequently whether they belong together. I've always liked the way Uncle Bob described it: "A class should have one, and only one, reason to change." Achieving this principle is difficult, but the benefits are huge, because when you need to make a change to a behavior, you can always identify just the right place to make it. Also, because each class does only one thing, you can feel confident that you won't accidentally change some other behavior.
- Open/Closed Principle: This principle was originally described by Bertrand Meyer in his book "Object-Oriented Software Construction" (Prentice Hall PTR, 1997), and it can be confusing. I prefer to describe it in a simplified way: "You should be able to extend or change the behavior of a class without changing the class itself." While you probably don't want to apply a principle like this to every behavior in a class, you should be mindful of reasonable places that consuming code may want to change the behavior. To achieve this, you need to introduce abstractions and leverage polymorphism. The result will make it easier for derived classes to extend the behavior in response to new requirements.
- Liskov Substitution Principle: This principle is one I've seen many developers get wrong. In essence, it says that if you create a derived class, you should be able to provide it to anything that consumes the base class, and it should still work. This principle is closely related to the Open/Closed Principle, as in object-oriented languages the primary means of extending a class is by deriving from it. Without this rule, all clients who consume classes in an inheritance hierarchy have to know about every derived class, which is clearly bad from a maintenance perspective. When I talk to developers about this, I often say: "You don't inherit from another class to reuse some code it has. You inherit because it is the other class, but with some changes."
- Interface Segregation Principle: The essence of this principle is twofold: Many fine-grained interfaces are good, and interfaces should be designed for their clients. It's important to note that this isn't only applicable to Microsoft .NET Framework interfaces, but should be thought of more broadly and applied to the public interface of a class. Like the other SOLID principles, this one is related to the others in that it asserts each interface should have a single responsibility and not be overloaded. Also, you should be careful about inheritance for your interfaces: Just because two interfaces happen to share some methods, it doesn't follow that one inherits from another, or even from a common base. Think about it this way: "Clients shouldn't be forced to depend on interfaces they don't use."
- Dependency Inversion Principle: Dependency inversion isn't just the last one on the list, it's also the one that brings them all together for the win. It's very natural to create layers of components where higher layers depend on lower layers. This principle challenges that assumption, and holds that higher-level things should depend on abstractions, and not on the lower-level things themselves. Uncle Bob likes to say, "Depend on abstractions. Do not depend on concretions." Creating malleable, maintainable code requires finding the right abstractions and using them as the code upon which you depend. These abstractions become the "seams" where you can change the code in response to new or changing requirements.
Testability and SOLID Designs
Most developers these days understand that good unit testing is key to higher-quality software. But you're often faced with the seemingly insurmountable challenge that your code simply wasn't designed with testability in mind.
The Agile community loves to talk about testable designs. "If your code isn't testable, then you have a design problem," is a common refrain. But what goes unstated is how to enable testable designs. Refactoring is clearly an important technique, but refactoring should always have a goal, a target -- a place you're taking the code. Without knowing where you're going with a series of refactorings, you're just playing around.
It turns out that the answer lies in these five design principles. Solutions built with the SOLID design principles are inherently testable. Highly cohesive, loosely coupled designs are testable. When dependencies can be replaced or stubbed out, you can test classes and methods in isolation from one another. The Single Responsibility and Interface Segregation principles enable this. Being able to mock or stub a dependency is a powerful and important technique, and it ultimately requires the Liskov Substitution principle (and to a lesser extent, the Dependency Inversion principle).
The Agile community has long recommended TDD as an approach for the coding side of Agile development. What hasn't been as clear is why TDD matters. There's the obvious benefit of getting a good suite of unit tests for your code. It's also beneficial to put some thought into what you're going to code before coding it, which is why TDD puts writing the test before writing the code.
But another touted benefit is the impact that TDD, unit testing and testability have on your designs. Unfortunately, the discussion often doesn't go much further than that. Rarely do people get into why and how unit testing affects the design of your solutions.
The SOLID design principles are the explanation that has been missing from this discussion. Since the mid-1990s, these principles have been recognized as the foundation of good object-oriented design. But these principles and testability go hand-in-glove: when you do one, you get the other. If you take unit testing and testable code seriously, you'll see these principles in your code. If you follow these principles when coding, you'll find that your code is easy to test (and more maintainable and flexible).
About the Author
Peter Provost is a principal program manager lead at Microsoft, where he focuses on making Visual Studio the supreme development environment for enterprise and Agile customers. In his 20-plus years in the software industry he has worked as a program manager, developer, team Lead, architect and consultant. He first got started with Agile development more than 10 years ago when he read an article about Extreme Programming, and has never gone back.