The 5 principals which make this up were bundled together as the S.O.L.I.D. principals in the early 2000s. These describe basic concepts that are required to write good code. So it is very important for novice, junior and mid-level programmers to put real thought into these and get good at applying the concepts correctly and consistently. The better a programmer gets at these the less time they should need to spend thinking about them.
As I mentioned these are very important basic programming concepts that need to be learned, mastered and (eventually) internalized by software engineers. But I personally think the language often used to define and explain the underlying concepts is a bit cryptic or overly complex. Below is my attempt at conveying these concepts.
(S) Single Responsibility Principal
Don’t mix multiple (especially unrelated) capabilities / functionalities together in one class. Make separate, more narrowly defined classes for each distinct capability. Classes that have a more defined / narrow purpose are easier to learn, apply, maintain and also reuse. Note that this applies to any division of code (methods, classes, interfaces, modules, libraries, etc).
(O) Open Closed Principal
Great concept, not so great name.
Once functionality has been crated and is in use don’t modify it in a way that would break existing code! At the same time there should also be a way to extend or alter the behavior of the previously written code (without rewriting or copy/paste) that won’t break code that uses it.
A good way to do this is through the use of abstract base classes and inheritance. The core functionality for performing a task should be written into a base class. A concrete descendant class will inherit that functionality and allow it to be used. That concrete class should not be changed in a way that breaks existing once in use. But new concrete classes and possibly a new level of abstract class can be added that extends or changes the functionality without impacting existing concrete classes.
(L) Liskov Substitution Principal
A rather complicated way of defining a good concept.
Descendant classes should not break either the explicit nor implicit contract of the parent class. Descendant classes should be implemented in a manner that allows them to be dropped in as replacements to the parent class without requiring any code that expects the parent class to be changed. The key here is that it is more than just signature level compatibility (list of methods, parameters, and types). The expected behavior must also remain the same!
Note that this isn’t limited to classes. This concept should be applied anyplace where substitution is allowed by the programming language or system. Which means this also applies to interfaces. This would also apply to multiple DLLs that expose the same method signatures (which is sometimes used to implement plug-ins or extensions). Anyplace substitution is possible care should be taken to ensure the behaviors are consistent.
(I) Interface Segregation Principal
This is the same general concept that underlies the Single Responsibility Principal but applied specifically to interfaces. Its the same underlying concept, it doesn’t need 2 separate principals.
(D) Dependency Inversion Principal
This is the least obvious of the bunch, possibly because this way of thinking and the frameworks / technologies needed to support it are more recent. Or maybe because this principal is emergent and not stand alone; It becomes possible due to other principals.
The idea here is that classes which require instances of other classes (or interfaces) to perform their work should not instantiate specific concrete implementations within their code. This has the effect of embedding the decision as to which concrete implementation to use in a place where it is difficult to change. This also typically results in these choices being embedded in many different places throughout a software system.
The concept behind the Open/Close principal encourages inheritance and abstractions. The concept behind the Liskov principal states that all implementations of something that allows substitution (such as classes and interfaces) must be interchangeable. If we’re applying both of those concepts consistently its a shame to make substitutions difficult by hard-coding and embedding those decisions all over the place in the code.
Bingo! And that’s why Dependency Inversion sprung up. To standardize and centralize the ability to use substitution in a software system.
Currently the most common preference for implementing this capability is the Dependency Injection pattern where concrete instances are passed into a class via its constructor. But that is not the only option, any pattern that extracts and centralizes these decisions could be used. One such alternative is the Service Locator pattern which, in some cases, can be preferable to Dependency Injection.
It is worth noting that the Dependency Inversion principal is the only one of the bunch that isn’t universally applicable. Not all applications need DI. I would likely argue that all large or complex systems probably should use it. But small systems or one-offs that aren’t expecting to be maintained probably don’t.