-
Notifications
You must be signed in to change notification settings - Fork 0
SOLID Principles
A class should have only one reason to change.
Let's say we have a class, which has two responsibilities:
- First. It knows how to perform financial data calculation.
- Second. It knows how to communicate with database directly to fetch/update financial data.
And as it was said in the headline, it also has multiple reasons to change.
- For the first point - class will change if the calculation logic changes.
- For the second point - class will change if underlying DB schema changes, or if we want to migrate to another ORM and so on...
So why is that bad?
Changes made in the code for one responsibility will enforce us to test the code for another one. Because the code for both of responsibilities is located in the same place we can't guarantee that new changes will not break anything somewhere else in the class.
Database specific changes enforce us to test calculation logic and vice versa which is design smell.
Best way to avoid this is to separate these responsibilities into two different classes with their own responsibilities. Doing so will allow us to modify and test these responsibilities separately.
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
This princple encourages us to use polymorphism in our design. Our classes should mainly operate on interfaces/abstract classes to ensure that if we add new subtype of some basic class - we will not need to modify client's code. Such approach makes our system extensible (open for extension) for new functionality and it also protects client's code from being changed by such extensions (closed from modification).
Common example of OCP violation is careless switch-case usage.
Let's say we have a system, which operates with fruits.
It is not uncommon to see the following piece of code in your system:
public void doSomething(Fruit fruit)
switch (fruit) {
case APPLE: {
//todo do something for APPLE
break;
}
case ORANGE: {
//todo do something for ORANGE
break;
}
case CARROT: {
//todo do something for CARROT
break;
}
default: {
throw new IllegalArgumentException(String.format("Unrecognized fruit [%s]", fruit));
}
}
}
And if you are very unlucky - these switch cases might be literally everywhere.
So what is so bad about that situation?
It has rigid design. Simple scenario, in which we want to add new types of fruits into our system, will force us to look for all the switch cases spread across our system and modify every single one of them. If you forgot to modify at least one of them - your system will most likely work incorrectly. It is very hard to maintain such system.
Correct way of solving such problem would be to locate logic for each particular fruit into corresponding fruit class.
APPLE {
@Override
public void doSomething() {
//todo do something for APPLE
}
},
ORANGE {
@Override
public void doSomething() {
//todo do something for ORANGE
}
},
CARROT {
@Override
public void doSomething() {
//todo do something for CARROT
}
};
In that way client's code will only have to call this method from the fruit, which came as an argument.
public void doSomething(Fruit fruit) {
fruit.doSomething();
}
Subtypes must be substitutable for their base types.
Which means that anywhere in our code where we operate on some base types - we should be able to substitute any subclass of base type and it should not violate the logic of how code should work.
TODO