In Object Oriented Programming we have a lot of patterns and principles to help up design and build better code. Many of these were written or codified by Robert C. Martin also known as Uncle Bob. The goal of the principles is to make it easier to develop, maintain, and scale. They also make it easy to recognize and avoid code smells as well as improving the refactorablity of your code.
“I could define all the principles in about five minutes but spend the rest of my career understanding them.”
S.O.L.I.D. is a mnemonic for the first five principle Uncle Bob promotes. They can and should be applied to any object oriented design and serve as the core of certain methodologies built around OOP such as agile. He states that these are not laws nor rules but advice on how to design and built better code. Like with design patterns these are guidelines that in most circumstances will lead to the best possible code.
“There is no royal road to Geometry” ~ Euclid
Simply learning and knowing these principles will not in itself make a person a better programmer. To truly be able to understand them you have to apply them. If you can’t do that in the code base you have now then work through practice problems applying them there. As you use them you will see why they are important and where they may not be the best choice. This is something that comes with time because it takes many experiences to get a full picture or how they can be applied. Whether you are just starting your career and learning the best ways to write code or have been at it for decades there is much to learn from just applying simple principles to your the way you code.
10:00 Single Responsibility Principle
“There should never be more than one reason for a class to change.” ~ Uncle Bob
Change means anything you do to the code that changes functionality of that method or class. This may be from adding a new feature, correcting bugs or errors, or refactoring your code.
A responsibility here is what is being done by this particular part of your system. Software should be a conglomeration of highly specialized pieces of code working together toward a specific goal. Keeping the responsibility down in a single piece of code reduces the need for other areas to know too much about it or in other words it keeps the coupling loose. The work done in these areas of the code need to be done in isolation.
“You’ll see this all the way down to the method level and all the way up to the application level.”
It’s based on Tom DeMarco’s principle of cohesion. Cohesions measures how strongly elements in a module of code belong together or relate to one another. It’s a qualitative measurement using a rubric to determine “high cohesion” vs “low cohesion” and ranges from Coincidental (worst) cohesion to Perfect (atomic) cohesion. There are seven levels in between these two. Coincidental cohesion happens when parts of a module/class are arbitrarily grouped together. Perfect cohesion would be a module containing only a single, atomic element. Perfect cohesion is not practical, in practice cohesion is a balance of unit complexity and coupling. The goal is for Functional cohesion as that happens when parts of a module are grouped together based on accomplishing a single well-defined task.
There are some criticisms of this principle. When it comes to bugs, refactoring, performance improvement, etc. do they count as a “reason” for change? The definition of a reason to change is too ambiguous. Responsibilities could be too broad or too narrow. There has to be an agreement on what is a valid responsibility.
16:30 Open/Closed Principle
“Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.” ~ Bertrand Meyer
You need to balance between maintaining single responsibility and changes in scope as you develop. Keep your existing or legacy code as immutable as possible while allowing new code to extend the functionality adding in new features. This extends single responsibility by saying if you need to make changes to a class you have to extend the class not alter it.
Open for extension means that behavior can be added to existing modules/classes of code to make it act in new and different ways. This could be adding fields to data structures or new functions. Reasons for extending your code can come from changes in scope of a project or adding new features to existing (legacy) code. If code is not extendable then you either can’t make changes to it or run the risk of breaking existing functionality because your have to change the source.
Closed for modification ensures that the original source code for a module/class is not changed. When adding new features you don’t want to effect the existing functionality. You may not be the only one using the code that you are changing. Meyer’s idea was to avoid having to change all the areas that were calling that code or library.
“This was back in the days of DLL hell.”
Applying this principle requires the use of abstraction and inheritance. Meyer’s approach to applying the principle relied on the use of inheritance. It uses implementation inheritance to create a new implementation of the class. This implementation would then contain the existing properties and behaviors of the inherited class. It could then be added to for modification without effecting the existing code.
The polymorphic approach uses a single abstract interface that allows for different implementations. This involves inheriting from an abstract base class so that the original class doesn’t change. The interface can be reused but not the implementation. Existing interfaces are closed to modification but new implementations of the interface may themselves add to it.
24:10 Liskov Substitution Principle
“Let φ(x) be a property provable about objects x of type T. Then φ(y) should be true for objects y of type S where S is a subtype of T.” ~ Barbara Liskov
Basically you should be able to replace an object with any of it subtypes and it still work the same. This extends open/closed principle so that anything that uses a class must be able to use any extension of that class.
“Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it.” ~ Uncle Bob
The principle sets standards around the inputs and outputs of functions. Many of these have been adapted into newer languages that were built to be object oriented. Covariance of return types in the subtype. You can pass in the subtype to a function that requires the parent. It will return an object of the parent type. Contravariance of method arguments in the subtype. You can pass in the parent to a function that requires the subtype. This runs into trouble if you try to read the subtype from the parent, best with write only. No new exceptions should be thrown by methods of the subtype, except where those exceptions are themselves subtypes of exceptions thrown by the methods of the supertype.
“The idea here is you don’t surprise the other developer.”
Also to adhere to Liskov’s principle the subtype must meet certain behavioral conditions. These are based on design by contract programming as defined by Bertrand Meyer. Designers should define formal interface specifications for components that extend normal abstract data types with preconditions, postconditions and invariants. A precondition is something that must be true before code is executed. A postcondition is something that must be true just after code is executed. An invariant is a condition that can be relied on to be true while code is executed.
Preconditions cannot be strengthened in a subtype. Postconditions cannot be weakened in a subtype. Invariants of the supertype must be preserved in a subtype.
The constraint of history rule prevents state changes in the subtype that are not permissible in the parent type. Subtypes may introduce methods that are not in the parent type. These methods may allow for changes that would otherwise not be allowed in the parent type. The history rule prevents this from happening. However anything added in the subtype may be modified by that subtype.
30:18 Interface Segregation Principle
“A client should never be forced to implement an interface that it doesn’t use or clients shouldn’t be forced to depend on methods they do not use.” ~ Uncle Bob
Interfaces should be thin and fine grained so that you have many interfaces each doing specific work as opposed to one large interface for everything. This is intended to keep systems decoupled so they are easier to refactor. Relating back to the single responsibility this specifies that clients or those calling an interface should only see what they need and that should give them all they need.
Interfaces are designed to abstract the methods used from the code calling those methods. They don’t contain any data or code. Interfaces tell the client or caller what the method does and how it can be used. Classes that have all the data or code for an interface implements that interface.
Methods not used by the client should not be in the interface. This could be based around the business or domain logic for the application. Any given interface should only contain the methods for the business logic it uses. The interface for controlling the speed of your vehicle isn’t the same as the interface for determining if it needs more oil.
“You’re not going to be checking your oil while you are driving your car so you don’t need those interfaces to be the same.”
When a client depends upon a class with interfaces that the client does not use that client will be affected by the changes that other clients force upon the class. Interface pollution happens when one interface inherits from another for just a few of it’s methods or subclasses. Each time an interface is added to the parent class it has to be implemented in the subclass.
36:50 Dependency Inversion Principle
“Thus when working with abstractions you work on a high-level view of your system. You only care about the interactions you can do and not how to do them.” ~ Uncle Bob
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
According to Martin Fowler there are several ways to look at the dependency inversion principle. Code should depend on things that are at the same or higher level of abstraction. High level policy should not depend on low level details. Capture low-level dependencies in domain-relevant abstractions.
In traditional application architecture the lower level modules such as your service layer or data access layer are designed to be consumed by the higher level modules (controller, business/policy layer). Higher-level modules then depend directly upon lower-level modules. They directly call the methods in those lower modules. Higher levels are bound to a specific implementation. This limits the reusability of higher level modules to where the dependent lower layers are available.
“All it needs to know is I need this information and here’s the method I use to get it.”
Using dependency inversion, interactions between high and low level modules should be abstract. The high level modules should be independent of the implementation details of the low level. The low level modules should be designed with the interaction in mind as it may need to change interfaces.
However, the inversion of dependency does not mean that lower level layers depend on higher level layers. Both should depend on the abstract interface between them. This reduces coupling of components without adding more code or coding patterns.
Velco Wink Bar
This is a smart, connected handlebar for your bicycle that pairs with an app on your phone. It allows for GPS navigation when riding, integrated headlights, and even geolocation if your bike is lost or stolen. The app is available in the Apple Store or Google Play and lets you set your routes for the bike to guide you. You can even get metrics about your ride from the information the handlebars gather.
Tricks of the Trade
When a piece of a larger whole fails catastrophically without bringing down the rest, or that can fail catastrophically without being helped by the rest really never was part of the whole to begin with. When you see such a failure, learn to treat that piece as separate from the rest of the system. This way of approaching things informed the way that the solid principle developed, but they apply to the larger scope of your life as well.