There's quite a bit of bad software design in the industry and that's surprising. After all, there hasn't been any lack of guidance about design over the years. From the earliest days of programming, there have been people who've shared their experience with creating software that is easy to change over time. As a result, we have an incredible number of concepts that we can fall back upon - Coupling and Cohesion, the Single Responsibility Principle, the Law of Demeter, the Dependency Inversion Principle, Postel's Law, RESTful-ness. The list is nearly endless. Surely, we should have good design by now. If only it were that simple.
The fact of the matter is that many organizations do excel at design. Not as many as I wish, but there are quite a few. But, the teams I find most interesting are the ones that miss the mark by being overly allegiant to design guidance. There's always that team that takes some piece of advice too seriously and ends up creating a mess. A classic example is over-doing the Dependency Inversion Principle (DIP). DIP tells us that it is better to depend on abstractions than it is to depend on implementation-esque entities. Some people, in statically typed OO languages, reduce it to sound-bite and write code where every single concrete class has an associated interface. You can't really fault them on following directions, but when you look at the resulting mess, it's obvious that they lack judgment. Or, that they don't feel the wiggle room that is necessary to apply judgment. Recently, I've arrived at the idea that we can fix that.
Most pieces of design guidance are presented in a context free style. Essentially, they are stated as ideals - "a good class in OO should look like this." What would happen if we focused on the problem we're trying to solve rather than the solution?
Imagine that you are working with someone and you see what would classically be called a Single Responsibility Principle violation. It's obvious to you that the class you are working on does several things, and the reason that you can see that is because you have wide experience with software. The person you are working with may not yet have that sort of tuned design sense. What can you do?
Well one thing that you can do is launch into a discussion of Single Responsibility and work through some examples. Your hope at the end of the process is that they've absorbed the idea and, having seen some examples, can apply the concept in new situations. Maybe they can, maybe they can't, but there is a more direct way of approaching the problem. We can simply say "if we were going change this class to do <some new thing> what would we have to change?" Notice that this is completely open-ended. You have a conversation, and in the process you deal with context. You may see that there is no need to split the class, or that in this particular situation you can, but it's not worth it. In any case, you are dealing with something real - a particular piece of code and thoughts about how it has and will change.
It turns out that nearly every design principle or piece of design guidance can be framed as a set of challenges to your code. To approach Liskov Substitution you can ask the question "will a caller be surprised if we replace every object of type A with an object of type B?" We can also approach bigger issues of Open/Closed Principle violations with hypothetical features - "Suppose the business changes in this way and we need to add this feature. What classes will change?" Again, it is all grounded.
I see the use of Design Challenges as a mental hack that is analogous to the move from Test-Driven Development to Behaviour-Driven Development. It's a re-placement of emphasis. I encourage you to try it out. The starting point is to learn how to take things you know about good design and frame them as questions.
Hi Mike,
I find it interesting that TDD helps to decide when to apply the various principles. For example a problem dependency may make the code you want to write untestable in a unit test. That could lead to applying dependency inversion. When a test case obviously is concerned unrelated ideas, maybe its time to split a class do achieve single responsibility.
Posted by: Jwgrenning | June 06, 2013 at 07:27 AM
Instead of guiding principles, what if we asked guiding questions? I like it.
Posted by: Jchyip | June 06, 2013 at 10:10 PM
Nice post. Asking questions is a classic approach when coaching to engage another person. Describing the challenge as the motivation of the design principle is neat too, especially as some design principles can be framed into multiple what-if challenges. Being open-ended, challenges should avoid problems with over-zealous application. However, a the biggest challenge to be faced when attempting this approach is many developers are opinionated and already steeped in design principles. I think they'd likely respond to a challenge with stock answers of what worked on their last project. Any thoughts on how to handle someone dismissing design challenges because they already know the answer?
Posted by: Rachel Davies | June 07, 2013 at 11:29 PM
I sure dealt with several codebases where "every single concrete class has an associated interface" made a mess. In particular because they had consistently had I and Iimpl.
However I'm not so sure that "every single concrete class has an associated interface" is a bad idea. Indeed Nat Pryce and Steeve Freeman in GOOS makes a point of using interfaces (for almost every interaction between two objects) as a way of naming the nature of the conversation taking place. I haven't had the opportunity to try it out full scale on a project, but I sure would like to. Do you believe it would work, given a reasonably mature team?
Posted by: Johan Martinsson | August 22, 2013 at 01:59 AM