A principle is a fundamental rule that we want to follow because it produces better results. Robert Martin (Uncle Bob) identified five fundamental principles and grouped them under the acronym SOLID (Martin). They started out as SOLDI, but Michael Feathers pointed out to Bob that if they switched the last two principles they would spell a cool acronym (Hansel). Andy Hunt and Dave Thomas identified another important one: Don't Repeat Yourself (Hunt). And underlying all software development I insist on recognizing that there is a general, unspoken principle of organization, which all of these principles are related to.
My interest is to explore what is behind these six principles, why they are so important, and how they relate to each other. I've spoken about these topics frequently, this is my chance to put some of those thoughts down in writing. I will try to be agnostic about whether we are using a class-based (C++, Java, C#) or prototypal (JavaScript) environment.
Single Responsibility
Single Responsibility is the the "S" in SOLID. Uncle Bob defines this as "having a single reason to change" (Martin). If you have more than one reason for changing a class, then the class must support more than one responsibility! It's best to limit each class to one responsibility. That is not always achievable, but we are certainly going to try.Single responsibility and cohesion are not the same thing. Cohesion is a metric, a measurement of how closely the fields and methods are related to each other. If a class or object has a single responsibility then it must be cohesive. But it could still be cohesive even if it has two or more closely related responsibilities.
One definition of encapsulation is the grouping of cohesive elements together in a class or object. Grouping stuff together makes it easier to manage, so really encapsulation, cohesion, and single responsibility are all fundamentally about organization!
So a gross example could be found in a banking system. Of course there will be bank accounts . We expect an account to keep track of things like balances and interest rates. Now certainly each account is associated with a client. Where better to put the client details than in the account?
But this impulse fails on three counts. First off we have a problem with cohesion. Here is a test: when is the address used in a process with the balance? Only in the loosest terms, perhaps during the formatting of a report would the customer information be presented at the same time as the balance.
So if the class isn't cohesive then it certainly violates the single responsibility principle! This class has two responsibilities: supporting the balance and interest rate, and managing the client details.
When we put our design in front of our customer they asked how does this support one client having multiple accounts, or one account being shared by multiple client? To paraphrase Alan Shalloway: give up all hope that your requirements will not change. Not only will they change, but your customer will probably forget to give you many of them in the first place!
If the client details continue to be combined with the account information, what will happen? How many copies of the client information need to be added to each account? What happens when client information is duplicated across multiple accounts? If you update one copy and miss another, which one has the right information?
Don't Repeat Yourself
That second problem is a violation of the principle of Avoid Duplication, which is also known as Don't Repeat Yourself (DRY) from Andy Hunt and Dave Thomas (Hunt; Venners). Programming languages implement functions to support DRY. A function allows us to put commonly used code in one place and reference it wherever it is needed. If the code has to be modified then there is in one place where the work needs to be done. Again, it's all about organization!Well DRY is not just a coding issue, it applies to data as well. When you duplicate data like the client information, then it becomes harder to manage. DRY is not one of Bob's five principles, but it's so important that we almost always talk about them in the same breath. More importantly, both SOLID and DRY both raised big red flags that our Account class has problems. And when you have a problem in OO we usually solve it by creating another object: Account and Client:
Substitutability
The Liskov Substitution Principle is the "L" in SOLID, substitutability is the basic principle it builds on. Substitutability has been in the English language since the sixteenth century: it formally defines using a replacement for someone who wants to avoid military duty. We have adapted it as the foundation for polymorphism in object-oriented programming, replacing soldiers with objects.So substitutability defines that wherever an object of a particular type is expected, providing an object of that type or any other type that extends the expected type will work. It works because the class-driven OO languages enforce a contract when one class extends another: the subclass must provide at a minimum the same public interface as the super-class. It also works in prototypal languages, as long as the programmer makes sure the substituted object has the correct interface. But all of this is a functional definition: we know the program will not crash while sending messages to an object, but will the result of sending that message produce expected results?
Barbara Liskov's work goes beyond basic substitutability to address that the program should continue to function as expected when an object is substituted. To simplify it, the LSP says to support this an object doesn't just support an interface, the functionality behind the interface has to be consistent as well.
Consider a class A that has a method multiply that multiplies two values, x and y. If class B extends class A and overrides the multiply method to divide x by y, then class B violates the LSP. Why? Because while class B implements the interface correctly the overridden method does not produce the expected results. You can read more about the LSP in another article that I wrote: when a square is not a rectangle!
So unlike type, this isn't something that compilers can enforce. It is up to the programmer has to support this principle. And it's important to support it. A substituted class needs to do more than just provide the same interface, it needs to be functionally indistinguishable to the client. Only when the subclasses provide the same functionality can they be used indistinguishably from an instance of the super-class.
Tie it Back to the Other Principles
Substitutability leans on the other definition of encapsulation: the principle of information hiding. This principle says that everything in an object should only be as visible as it needs to be. If a client doesn't need it then it should remain hidden: what you can hide you can change. And substitutability is all about organization and DRY: extend a class or object to provide new functionality without repeating code.
Open/Closed
Open for Extension, Closed for Modification is the "O" in SOLID. Open/closed depends heavily on the Liskov Substitution Principle. It also depends on another principle: Design to the Abstraction, also known as Depend on Abstractions, and referred to as a programming technique: Code to the Interface.In a nutshell, design to the abstraction means that client code should be designed to use an interface, an abstraction, and not care about the actual implementation handed to it. I mean interface in the OO sense of how you send a message to an object, not the syntax of a particular language (Java and C# have interface language structures). Here is an adapter pattern combined with a factory to provide an object to perform credit card authorizations. We are just interested in the structure, many of the methods, their parameters and return types are not shown:
The factory provides a particular adapter based on configuration information that it reads. The client, the PointOfSaleTerminal, doesn't care which adapter it gets. It is designed to use the common interface that all of the adapters provide.
So what's the point? It's easy to add a new adapter to talk to another authorization service (the interface/superclass is open for extension). And you don't have to change the client to use the new adapter (closed for modification).
Just under 70% of the patterns in the Gang of Four design patterns book rely on open/closed and design to the abstraction (Gamma). Think about that. You should leverage this all the time to keep your clients decoupled from what they are using.
Tie it Back to the Other Principles
Open/closed is principally dependent on substitutability, and everything that substitutability is related to.
Dependency Inversion
The Spring Framework for both JAVA and .NET provides dependency injection where concrete objects are created from instructions in an XML context file and linked together using abstractions. That is an implementation of the principle of dependency inversion, the "D" in SOLID. And if that sounds a little complicated, it's OK because we have a simpler example.
Before we added the factory in the last example the PointOfSaleTerminal would have had to choose the correct adapter to instantiate. The structure, and the code would look something like this:
class PointOfSaleTerminal {
@tab;private PaymentAdapter paymentAdapter;
@tab;public PointOfSaleTerminal {
@tab;@tab;paymentAdapter = new IntuitPaymentAdapter();
@tab;}
@tab;...
}
@tab;private PaymentAdapter paymentAdapter;
@tab;public PointOfSaleTerminal {
@tab;@tab;paymentAdapter = new IntuitPaymentAdapter();
@tab;}
@tab;...
}
Well, there are two problems with this example: we violate both single responsibility and open/closed. Single responsibility because the class now is handling the sale and managing the choice of PaymentAdapter. And open/closed because if we change the adapter we have to modify the PointOfSaleTerminal. Any place that the new operator is used with a class not in a core library is a point where we are probably violating those two principles! At least we remembered to depend on the abstraction; the field type was a PaymentAdapter...
When the factory is added the dependency on the concrete adapter is moved from the PointOfSaleClass to the factory:
The responsibility of deciding which concrete adapter to instantiate is now with the factory, which then provides the selection to the PointOfSaleTerminal class. The factory design pattern implements dependency inversion: the selection of the concrete dependency has been moved outside of the PointOfSaleTerminal class!
Sure, if new adapters are created we might have to open up the factory. That depends on how it is implemented, how it reads and uses the configuration. But if it does have to be opened it only has one responsibility and nothing else is put at risk.
The responsibility of deciding which concrete adapter to instantiate is now with the factory, which then provides the selection to the PointOfSaleTerminal class. The factory design pattern implements dependency inversion: the selection of the concrete dependency has been moved outside of the PointOfSaleTerminal class!
Sure, if new adapters are created we might have to open up the factory. That depends on how it is implemented, how it reads and uses the configuration. But if it does have to be opened it only has one responsibility and nothing else is put at risk.
Tie it Back to the Other Principles
Dependency inversion supports open/closed and depends on design to the abstraction and the Liskov Substitution Principle.
Interface Segregation, the "I" in SOLID
To tie it to everything else, the Interface Segregation Principle is really just another facet of Single Responsibility, and is measured by cohesion. If you design a class that is highly cohesive, then the interface must be cohesive to what goes on in the class and should expose publicly the minimal functionality that the clients need.
So the only place we need to worry about this in the design is with actual interface structures in Java and C#. The methods that an interface defines must be cohesive with each other. If they are not, then it is time to break the interface apart into separate interfaces that are. Because any class can implement multiple interfaces, any class can choose the cohesive interfaces that it needs and ignore the others. Then a class can add just what it needs and nothing more.
Single Responsibility (Again)
Single Responsibility is so important that we will wrap up by visiting it again. I've always taught that the idea of single responsibility goes way beyond cohesion in a class, and I've been teaching that long before Bob created his first principles. Of course we didn't have this name for the principle. My associates and I noticed it as far back as the 1970s when we were focused on structured programming. And since formal languages have been around since the 1950's I know that others noticed it long before us.Single responsibility is applied at every level of software. Of course the focus is a little different at each level. Single Responsibility applies to an application: I wouldn't look to add rental cars to shipping software. It applies to each module, each class, each method, and each statement. And that's where I noticed it first: when functions were about one thing, when statements did one thing, then our programs were more robust, more reliable. I always teach that you should almost never use an increment or decrement in the middle of another expression, and this principle is why. The increment and decrement are one responsibility, whatever the rest of the statement is doing must be another responsibility.
I always felt somewhat uncomfortable stretching Uncle Bob's work to that philosophy when I taught SOLID. It's important for good design, but it wasn't what he was addressing with the principles. I meant to ask him but never got around to it. Because the really cool thing is that before I had the chance I stumbled across Bob saying almost exactly the same thing in an interview with Scott Hansel (Hansel)!
So that wraps up my foray into DRY, SOLID, and a few other important principles. The opinions expressed here are my own and not necessarily those of any organization I have worked for or the advertisers you may see alongside of this post :)
References
See the references page.
No comments:
Post a Comment