Single Responsibility
One job per class as much as possible, so changes in behavior are more granularly isolated.
Open Close
Open for extension, close for modification.
A class interface, or a parent class defines the base blueprint, extending it encapsulates differences in behavior between children of it
You find yourself writing a long series of “if” statements, or a big switch case, inside a parent class. Try to encapsulate the functionality in classes instead
if (something = x) { then do y}
if (something = z) { then do a}
etc…
better expressed as:
class x {
do y
}
class y {
do a
}
and make that a “spawn the right class” problem, the object contains its own different behavior
Liskov Substitution
Each class has a series of “contracts” attached it it. Which translates to methods should have pre conditions and post conditions.
For example, let’s say class A, has a pre-condition where it only takes arguments of the Z kind.
Along comes its subclass, and this one (wrongly, violating this principle) takes arguments of the Y kind as well.
This weakens the parent contract, and if you were to substitute the parent with the child anywhere in the code, now you are open to code failing, because you though object of the parent class will only take Z arguments, but this sub class is making it possible for them to take Y arguments.
The same can happen with the output: the parent class has a “contract” with the world out there, where it will only output float numbers, but now comes a subclass that allows int returns. The parent has been diminished, and results can break stuff because we can’t just plug and play the subclass with the parent contract anymore.
Interface Segregation
If you make your interfaces too broad, you will find yourself forced to implement methods you don’t need, for certain clients.
Abstract with interfaces, but don’t force clients of them to implement things they don’t need. Let the client dictate the segregation of the interfaces.
It is better to have a bunch of them, client specific, than generic ones that force extra code.
Dependency Inversion
Interdependency modules: at some point there will be a one to many relationship on them
Say, function A has three different ways to pull the data it needs internally. And say it is taking an argument to deal with that:
functionA(dataPuller){
if(dataPuller typeOf some){ pull the data this way}
if(dataPuller typeOf some){ pull the data that other way}
etc…
}
The above will be better off refactor as:
functionA(someSubclassThatImplementsAbstractClassThatDealsWithDataPulling){
someSubclassThatImplementsAbstractClassThatDealsWithDataPulling.pullData();
}
The advantage: when you add / substract / edit the ways you pull data, you do it on the abstraction, you don’t need to modify anything in functionA. So function A is not thighly coupled to this data pulling business, it just use it (whatever it is).