CodeBork | Tales from the Codeface

The coding blog of Alastair Smith, a software developer based in Cambridge, UK. Interested in DevOps, Azure, Kubernetes, .NET Core, and VueJS.


Project maintained by Hosted on GitHub Pages — Theme by mattgraham

Sadly for all you Whovians out there, I don’t mean those Ood. Instead, I want to talk about the SOLID Principles of Object-Oriented Design, as proposed by “Uncle” Bob Martin back in the mid-1990s. The SOLID Principles gained a level of notoriety in the .NET community a couple of months ago, after Jeff Atwood and Joel Spolsky made some, er, unguarded comments about them in one of their StackOverflow podcasts. This post is long overdue, so I’m glad to finally get it out there! :-)

[img_assist|nid=71|title=|desc=|link=node|align=center|width=400|height=233] The SOLID Principles of OOD are a bunch of guidelines for developing object-oriented systems. They can be interpreted rigidly, and indeed religiously, or they can be applied in a more relaxed fashion. The key, as with any of these principles and practices we learn, is to stick to them in general, and know them well enough to be able to correctly judge when to bend them. You can find the full definitions of the SOLID Principles in Uncle Bob’s original blog post; alternatively, you can hear it straight from the horse’s mouth in episode #145 of Hanselminutes. I’m just going to summarise the five main principles of class design; you may be surprised to see that they’re actually pretty simple, and that you’re probably already following a number of them unwittingly. Take a look:

Single Responsibility Principle
A class (or method) should only have one reason to change.
Open Closed Principle
Extending a class shouldn't require modification of that class.
Liskov Substitution Principle
Derived classes must be substitutable for their base classes.
Interface Segregation Principle
Make fine grained interfaces that are client specific.
Dependency Inversion Principle
Program to the interface, not the implementation.

The Single Responsibility Principle

The crux of this principle is that not only should a class encapsulate an idea, such as a blog post, but that encapsulation should be quite restricted to “real” properties and behaviours of that idea. This means that all ancillary operations related to the idea should be split out into separate classes. Using the blog post example, your blog post class might have a SaveToDatabase method and a RenderHtml method. A change in the database schema or display layout would result in a change in the class. This is in violation of the Single Responsibility Principle, because these two methods change for two different reasons. SaveToDatabase more properly belongs in a Repository class, and RenderHtml in the View part of a Model-View-Controller pattern.

The Open Closed Principle

Software modules should be open for extension and closed for modification. Stop and think about that for a moment. Open for extension and closed for modification. What does that even mean?!

Well, to my mind, it essentially means that you should treat your modules (classes, methods, assemblies, whatever) as black boxes unless you are working directly on that module. If you have a Foo, and you want to extend it to make a FooBar, you shouldn’t have to do anything to Foo to get your FooBar. This comes down to writing good, clean interfaces to your modules, and utilising polymorphism to its fullest extent. For example, you might have an Account base class; you could then use polymorphism to define, manipulate, and otherwise work with various accounts such as CurrentAccount (with an overdraft facility), CreditCardAccount (with a credit limit), etc. Account is marked as abstract, and defines the core functionality of a financial account: recording transactions. The subclasses modify the abstract base class’ behaviour by introducing a kind of limit on the balance: the overdraft or credit limit. You add new features by adding new code, not by changing existing, tested code. How’s that for robust?

The Liskov Substitution Principle

This follows on nicely from the Open-Closed Principle. Given an abstract class or interface, anything that refers to it should be able to handle one of its derived classes. It’s sort of another way of saying that “a rose is a rose is a rose is a rose”. That’s all a bit of a mouthful, so let’s try an example.

If you drive a Ford, you should also be able to drive a Toyota. Or a Hyundai. Or a Volkswagen, Mercedes or Ferrari. A car is a car is a car is a car: given the abstract class of “car”, you should be able to drive that car, no matter what make and model it is.

A good metaphor for the Liskov Substitution Principle not being followed is Microsoft Office. For many years, they got by with the menu-based system. With the release of Office 2007 they introduced a new Ribbon interface. Suddenly people didn’t know how to use Office 2007. A similar thing happens with new release of Windows: try finding anything in the Vista Control Panel if you’re used to XP (the same applies to the XP Control Panel if you’re used to Windows 98, 2000, etc.). In order for these new interfaces to be adopted, a form of the Liskov Substitution Principle has to be applied; hence Microsoft’s efforts to get third-party developers using the Ribbon and the similarity between Vista’s Control Panel and Windows 7’s.

How does this apply to your code? Well, is a square a rectangle? Your instinct might be to say “yes, it’s a special case of the rectangle”*. That’s not strictly true, though: what does it mean to talk of a square’s height and width as opposed to a rectangle’s height and width? When implementing squares and rectangles in code, the rectangle should rather be a generalisation of the square than the square a specialisation of the rectangle, otherwise your subclasses do not behave like their superclass.

The Interface Segregation Principle

This principle applies to very fat classes, i.e., with many tens or hundreds of methods, and draws on the theme of the Single Responsibility Priniciple. Very often, only subsets of the members of classes like these are used at any one time, but because they’re all defined in the same class, a client of that class may need to change because part of the fat class has changed (often even if it’s an unrelated part of the fat class). The idea here, then, is to define those subsets of methods as interfaces, and have the fat class implement each interface (or, ideally, split the subsets into smaller classes implementing those interfaces). You can then pass around references to these interfaces (see the Dependency Inversion Principle, below) rather than the full fat class, meaning that you only have available the methods that are relevant to that particular usage of the class. It’s a bit like partitioning the class according to function, with the option of easier re-factoring in the future.

The Dependency Inversion Principle

In Uncle Bob’s own words, this is like a restatement of the Open-Closed Principle with a 90-degree rotation. Put simply, this says “depend only on things which are abstract”, what I have called in the past, “interface programming” or “programming to the interface”. In essence, you should not rely on any concrete implementations of any classes, be they your own or framework objects. For example, in the .NET framework, there are a number of List classes: List, LinkedList, etc. If you declare a class member of type List, and then realise that a LinkedList would be better (performance reasons, whatever), you have to go through and change all List declarations that may handle your list. If instead you had referred to it as an IList throughout, all you’d need to do is change the line(s) that construct the list object. If you realise that a List is far too specific, an IEnumerable or even ICollection would be a better choice.

This is one of the best lessons I learnt during my internship at Citrix back in 2006, and it’s stayed with me ever since. Using interfaces greatly simplifies your code, because you end up dealing with a number of Foo-shaped boxes into which you can slot any form of Foo you like. If I wanted a Collection-shaped box, I could use any form of Collection I liked, be it List or not.

Conclusion

The sum of these principles is a set of guidelines that will help you write good, clean, testable code. Give them a whirl in your next coding task, and see what you make of them. Then leave me a comment!


* I made this mistake in an interview once. Oops.