SOLID and Open Closed principle
Overview
In this series of articles, I will cover all the SOLID patterns and their use in examples.
The SOLID principles were introduced by Robert C. Martin in his 2000 paper “Design Principles and Design Patterns.”
The SOLID principles consist of the following five concepts:
Single Responsibility Principle Open/Closed Principle Liskov Substitution Principle Interface Segregation Principle Dependency Inversion Principle
Open Closed Principle
A software module (class or method) should be open for extension but closed for modification.
And I reuse code from previous article where I focus on Single Responsibility principle. Only change will be switch to Kotlin instead of Java.
Inheritance
In first exercise I have one interface Calculation
which is implemented in various scenarios.
import java.math.BigDecimal
interface Calculation {
fun calculate(): BigDecimal?
}
class ComissionedPayCalculation : Calculation {
override fun calculate(): BigDecimal? {
return null
}
}
class HourlyPayCalculation : Calculation {
override fun calculate(): BigDecimal? {
return null
}
}
class SalariedPayCalculation : Calculation {
override fun calculate(): BigDecimal? {
return null
}
}
Then I have CalculationFactory
where is decision for that scenarios made:
import java.math.BigDecimal
class PaymentCalculator {
@Throws(InvalidEmployeeType::class)
fun calculatePay(e: Employee): BigDecimal? {
return CalculationFactory.createCalculation(e.type).calculate()
}
}
object CalculationFactory {
fun createCalculation(type: EmployeeType?): Calculation {
return when (type) {
EmployeeType.COMMISSIONED -> ComissionedPayCalculation()
EmployeeType.HOURLY -> HourlyPayCalculation()
EmployeeType.SALARIED -> SalariedPayCalculation()
else -> throw InvalidEmployeeType(type)
}
}
}
And I want to add new calculation for SALARIED and HOURLY paid employees, where I want increase salary by 11% as a bonus.
What would be the most advisable solution with respect to the principle of an open closed system?
There are two ways how to implement that new functionality. Firstly we will look at inheritance solution.
In that solution I will create new class which extends from existing HourlyPayCalculation
and I call super constructor and multiple by bonus:
class BonusHourlyPayCalculation : HourlyPayCalculation() {
val BONUS = 1.11
override fun calculate(): BigDecimal? {
return super.calculate()?.multiply(BigDecimal(BONUS));
}
}
class BonusSalariedPayCalculation : SalariedPayCalculation() {
val BONUS = 1.11
override fun calculate(): BigDecimal? {
return super.calculate()?.multiply(BigDecimal(BONUS));
}
}
However, as you can see in this scenario, the code can grow considerably with each new feature, and this is one of the reasons why inheritance is not a highly recommended solution. A better approach to write less code and fewer classes.
Composition over inheritance
Let’s say I want to have all the decision logic (responsibility) in one place. And CalculationFactory
is the best for that.
Is there any way to refactor this bonus calculation decision to the factory class ?
Composition can help in that way. Firstly I create new class BonusPayCalculation
implements Calculation interface.
class BonusPayCalculation constructor(private val multiplication: BigDecimal,
private val baseCalculation: Calculation) : Calculation {
override fun calculate(): BigDecimal? {
return baseCalculation.calculate()?.multiply(multiplication);
}
}
As you can see there are variables which are consume by constructor, one is multiplication value (bonus in this case) and baseCalculation which can be any implementation what we want. And in factory I made this change:
object CalculationFactory {
val BONUS_VALUE = 1.11
fun createCalculation(type: EmployeeType?): Calculation {
return when (type) {
EmployeeType.COMMISSIONED -> ComissionedPayCalculation()
EmployeeType.HOURLY -> BonusPayCalculation(BigDecimal.valueOf(BONUS_VALUE), HourlyPayCalculation());
EmployeeType.SALARIED -> BonusPayCalculation(BigDecimal.valueOf(BONUS_VALUE), SalariedPayCalculation());
else -> throw InvalidEmployeeType(type)
}
}
}
So the CalculationFactory
is also responsible for deciding the value of the BONUS and which class to use for each payment calculation, and if I want to add some new functionality in the future (e.g. calculating payment for external), BonusPayCalculation will remain untouched and will be able to use my new functionality.