SOLID is an acronym for five best practice principles in Object Oriented Programming. The purpose is to make OOP more reusable and maintainable and easier to understand.
The SOLID Principles are as follows:
- A class does one thing and one thing well
- A class is open to extension but not to direct modification
- A super class can be replaced by a sub class without breaking the program
- It is better to have small specific interfaces than one big one
- A class should depend on abstraction over concretion
That's a bit of a mouthful though so I'm going to extend my car analogy to talk about each principle in kind.
A car is made of many discrete parts. Brakes, wheels, engine, etc. I'm going to focus on the engine. Now, most people think that the engine's responsibility is to make the car go, but this isn't true.
The engine's responsibility is to spin the crankshaft. That's it. Making the car go is a combined effort of many different parts each that do one single thing. The engine however doesn't care about the brakes or the axles or the wheels. It's only job is to turn the crankshaft. In fact, you can even take the engine out of the car and put it in something else and the engine will do its job.
Every part in a car behaves this way. The brakes only care about applying friction to the wheels. The spark plugs only care about firing in the "combustion" phase of the piston.
Now this might be a limitation of mechanical engineering, creating a part that does multiple things at once is not easy, but in programming, it's very easy and even seductive. Why not make one master class?
Because it turns into a mess, that's why. If you have a single object that is concerned with multiple parts of the application, changing one thing here might break something else, somewhere else, in a less predictable manner. Perhaps the code is old and you've forgotten that this value here is doing double duty over there.
This is a principle that can be expanded beyond OOP, it's also a good principle for programming in general, especially in functional programming. A function should do one thing, and do it well.
But what does it mean to "do one thing?" After all, it's pointless to have a function or an object that literally only has one line of instruction. This is a question of design, based on your own judgement. It's up to you and your team to divide up the software into small discrete parts with distinct roles and create classes, objects and functions to perform them.
As an example, imagine an object responsible for connecting to your database. That's all it should do. It should not also do CRUD operations or expose the public API. It should connect to the database and manage that connection. That's it. Like a car's engine, you'd be able to decouple such an object from your software and use it elsewhere. If you start building REST methods and CRUD methods into it, it becomes more complex and less reusable.
The principle of Open-Close says that an object should not be modified, but merely extended. This is akin to inheritance. When Subaru or Ford builds a car, they extend upon the original concept of a car. (We'll touch upon this further when I talk about the Dependency Inversion principle.) We have a base idea of a car that no one wants to change, and for good reason. If Subaru "changed" the idea of a car, they'd produce a car we wouldn't be able to easily operate.
Now, the analogy here fails because our concept of a car is abstract, but pretending it was concrete, if you change the base class, then all the derived classes also break.
Once an object or a module is created, one should be able to write code that extends it, adding fields and properties as needed to the extension, but not to the base object itself. The base object instead keeps its internals encapsulated and only exposes what it needs to expose.
Liskov-Substitution states that supertypes can be substituted by subtypes without breaking the code. If I write code with a base class object, I should be able to replace that base class object with any of its subtypes and it still works. Inheritance is typically the method that allows this to happen.
Most people know how to drive a car. If you consider Ford or Subaru or Chevrolet to be subclasses of type Car, you can replace any car with any other car and still know how to operate it. Each car make and model has different properties, but they're the same enough, because they derive from a base class of Car, that they can be substituted without modifications.
It is better to have many small specific interfaces than one big one. A large general purpose interface, much like a large object, is unmaintainable and unwieldy. Instead, the software should be broken up into smaller components that do single simple things.
For example, the wipers in my car aren't dependent on some other system (other than the basic electronics) to be active or inactive first. They don't depend on the state of other parts.
This has a further use when I can add or remove interfaces as necessary. Think of a computer for example. One interface is a keyboard. It does one thing, it sends keyboard instructions to the motherboard. It doesn't depend on any other piece (not even the motherboard, as it can plug into other motherboards and possibly other pieces of hardware.) You can remove it without affecting the other parts of the computer.
This is a downside to laptops, especially Macs. You can't easily swap parts, the interface is one huge chunk, and Mac users don't have the ability to add new RAM or internal ROM.
Break the software down into independent parts and keep them separate from each other. Try to think in terms of "plug and play."
The principle here is that a class should be based on abstraction, and not concrete implementation.
Again, we all have an idea of what a car is and how one is to be operated. The cars we drive, the concrete metal objects we drive, based on concrete schematics stored in the headquarters for the car companies, are based on this abstract idea. The exact details on how to implement a car are found in the schematics (the class) and constructed in the cars themselves (the instance objects.)
Java Servlets serve as a great example for object oriented programming. The hierarchy is as follows:
- Servlet is an interface.
- GenericServlet is an abstract class that implements Servlet
- HttpServlet is a concrete class that implements GenericServlet.
We start at the abstract idea of what a servlet is. The interface lets us know what the classes need to implement. The abstract class will provide more details on what needs to be implemented by extending classes and will provide some behavior and properties that are already set. HttpServlet is a concrete class based on those two higher templates.
We know that HttpServlet implements
service() because the interface at the top demands that it does. This is a top down system where the top lays out the behavior and state that the extending/implementing class needs to perform and the class at the bottom provides the implementation.
This is a best practice because the higher implementation details are, the more brittle the whole inheritance chain is. If I change something at the top, that effect can ripple downward and break everything beneath. Dependency inversion puts only a contract at the top, the derived objects decide for themselves what the implementation details are.
There's a basic idea of what a car is, a Subaru Outback is a specific concrete implementation of what a car is. A car is a vague definition with few details, a Subaru Outback is a specific definition with tangible details.
SOLID is a set of five principles to help a developer create software that is reusable, understandable and easier to maintain. Knowing and applying these principles will make you a better programmer over all.