IoC: Inversion of Control principle

This is the one principle that, chances are, you have applied even if you didn’t do it on purpose.
In essence, if you have written code that does not have any control over the execution flow, then you most probably have applied the IoC principle. How?

IoC through design patterns

If you have implemented the strategy or the template pattern then you have applied the IoC principle. For example, having a report generator and feeding it with the necessary filtering code.
All the code you write for filtering data, does not have any control over the execution’s flow. The generator is the one that decides when and if it will be invoked:

class Report(
val women: List<Person>,
val men: List<Person>
) {
fun isEmpty(): Boolean {
return women.isEmpty() && men.isEmpty()
}
}
class ReportGenerator(
private val repository: Repository
) {
fun create(filter: (Report) -> Report): Report {
val people = repository.fetchAllPeople()
val rawReport = splitByGender(people)
if (rawReport.isEmpty()) {
return rawReport
}
// the filter function does not have any control over its invokation
val filteredReport = filter(rawReport)
return sortByName(filteredReport)
}
//
}
// example:
val reportGenerator = ReportGenerator(repository)
val withPeopleOver21 = { rawReport: Report ->
val over21 = { person: Person -> person.age > 21 }
Report(
rawReport.women.filter(over21),
rawReport.men.filter(over21)
)
}
val reportWithPeopleOver21 = reportGenerator.create(withPeopleOver21)
Strategy pattern

The same goes with the template pattern too. The code you write in the template’s hooks is being controlled by the template and you don’t get to control it:

class Report(
val women: List<Person>,
val men: List<Person>
) {
fun isEmpty(): Boolean {
return women.isEmpty() && men.isEmpty()
}
}
abstract class ReportGenerator(
private val repository: Repository
) {
fun create(): Report {
val people = repository.fetchAllPeople()
val rawReport = splitByGender(people)
if (rawReport.isEmpty()) {
return rawReport
}
val filteredReport = filter(rawReport)
return sortByName(filteredReport)
}
abstract fun filter(report: Report): Report
//
}
class AdultsReportGenerator(repository: Repository) : ReportGenerator(repository) {
override fun filter(report: Report): Report {
val over21 = { person: Person -> person.age > 21 }
return Report(
report.women.filter(over21),
report.men.filter(over21)
)
}
}
// example:
val generator = AdultsReportGenerator(repository)
val reportWithPeopleOver21 = generator.create()
Template pattern

Both of these examples might seem weird since we are the ones that wrote both the filtering code and the generator so we feel that we control the flow. The thing is that we need to separate them in our heads and observe them individually. The generator is the one that controls the flow and dictates the actions that will take place. The filtering code is one of the actions. We just write them, provide them to the generator and that’s it.

IoC through frameworks

Another way of applying IoC is by using frameworks that have adopted it. Most popular are the IoC containers that are used to inject dependencies.
Another example is the android’s framework. In android you don’t have control over an activity’s lifecycle and you simply extend the Activity class and override hooks to run your code.

IoC vs DI

Because of the aforementioned IoC containers, many people assume that dependency injection and IoC are the same. They are not. DI is just a way to help in applying the IoC principle. For example:

class Report(
val women: List<Person>,
val men: List<Person>
) {
fun isEmpty(): Boolean {
return women.isEmpty() && men.isEmpty()
}
}
class ReportGenerator(
private val repository: Repository,
private val filter: (Report) -> Report
) {
fun create(): Report {
val people = repository.fetchAllPeople()
val rawReport = splitByGender(people)
if (rawReport.isEmpty()) {
return rawReport
}
val filteredReport = filter(rawReport)
return sortByName(filteredReport)
}
//
}
// Example:
val withPeopleOver21 = { report: Report ->
val over21 = { person: Person -> person.age > 21 }
Report(
report.women.filter(over21),
report.men.filter(over21)
)
}
val generator = ReportGenerator(repository, withPeopleOver21)
val reportWithPeopleOver21 = generator.create()

we can change the generator and have the filtering code being injected to it by constructor. The inversion is already happening, DI is simply used to provide the extra code.

A use case of using chain of responsibility pattern to scale strategy

Lets say we have a simple application that executes certain actions dictated by an external service.

More particular the external service sends a response that looks like this:

class Response(
val action: String,
val value: String
)

the supported actions are:

interface Action {
val name: String
fun execute(response: Response)
}
class Print : Action {
override val name: String = "print"
override fun execute(response: Response) {
println("printing ${response.value}")
}
}
class Log : Action {
override val name: String = "log"
override fun execute(response: Response) {
println("logging ${response.value}")
}
}
class WriteToFile : Action {
override val name: String = "write_to_file"
override fun execute(response: Response) {
println("writing to file ${response.value}")
}
}

and there is an executor to help as translate the response to an actual action execution:

class ActionExecutor {
private val allActions = mutableMapOf<String, Action>()
fun addAction(action: Action) {
allActions[action.name] = action
}
fun executeByResponse(response: Response) {
val action = allActions[response.action]
action?.execute(response)
}
}

This is an implementation of the Strategy Pattern where every strategy is a certain action allowing us to add as many actions as we want as long as there is a one-to-one relation with what the response sends:

fun main() {
val executor = createActionExecutor()
executor.executeByResponse(Response("print", "leonidas"))
executor.executeByResponse(Response("log", "kotlin"))
executor.executeByResponse(Response("write_to_file", "leonidas loves kotlin"))
}
private fun createActionExecutor(): ActionExecutor {
return ActionExecutor().apply {
addAction(Print())
addAction(Log())
addAction(WriteToFile())
}
}

The problem

One day the external service decides to add a print error action but it does not do it by having a new action value. Instead it decides to have an is error flag which must be taken under consideration when the action value is print!

So the response is now this:

class Response(
val action: String,
val value: String,
val isError: Boolean = false
)

leading us into having an if in the print action’s code

class Print : Action {
override val name: String = "print"
override fun execute(response: Response) {
if (response.isError) {
System.err.println("printing (in error stream) ${response.value}")
return
}
println("printing ${response.value}")
}
}

which defeats the purpose of the strategy pattern and violates the SRP. Each strategy should do one thing and one thing only. By having an if in the strategy we allow it to implement two actions thus having two reasons to change.

Adding a chain

What we need is a way to allow the representation of a single action value from multiple strategies where each one implements a unique action depending on the values of the response.

This is where the Chain of Responsibility Pattern comes in helping as into having multiple actions tied together, passing the response to each other until one of them can execute it.

There are two ways to implement the pattern depending on the size and state of your project.

Using inheritance

If you have a small project with just a few actions and you don’t mind touching many files you can use inheritance and have each action containing the next in the chain:

abstract class BaseAction(
private val nextAction: Action?
) : Action {
override fun execute(response: Response) {
nextAction?.execute(response)
}
}
class Print : Action {
override val name: String = "print"
override fun execute(response: Response) {
println("printing ${response.value}")
}
}
class PrintInErrorStream(
nextAction: Action?
) : BaseAction(nextAction) {
override val name: String = "print"
override fun execute(response: Response) {
if (response.isError) {
System.err.println("printing (in error stream) ${response.value}")
return
}
super.execute(response)
}
}

which can be used as such:

fun main() {
val executor = createActionExecutor()
executor.executeByResponse(Response("print", "leonidas"))
executor.executeByResponse(Response("log", "kotlin"))
executor.executeByResponse(Response("write_to_file", "leonidas loves kotlin"))
executor.executeByResponse(Response("print", "$&#$@#", isError = true))
}
private fun createActionExecutor(): ActionExecutor {
return ActionExecutor().apply {
addAction(PrintInErrorStream(Print())) // first try to print in error and the in simple
addAction(Log())
addAction(WriteToFile())
}
}
Using composition

If we do not wish to change any of the existing actions then composition is the only way.

What we need is a way to wrap an existing action in an object that will (a) hold the next action in the chain and (b) decide which action will be executed, the wrapped or the next one:

class ChainAction(
private val action: Action,
private val canExecute: (Response) -> Boolean
) : Action {
private var nextAction: Action? = null
override val name: String = action.name
override fun execute(response: Response) {
if (canExecute(response)) {
action.execute(response)
return
}
nextAction?.execute(response)
}
fun or(action: ChainAction): ChainAction {
nextAction = action
return this
}
}
fun Action.asChain(canExecute: (Response) -> Boolean): ChainAction =
ChainAction(this, canExecute)
view raw chain_action.kt hosted with ❤ by GitHub

Using ChainAction results in this:

fun main() {
val executor = createActionExecutor()
executor.executeByResponse(Response("print", "leonidas"))
executor.executeByResponse(Response("log", "kotlin"))
executor.executeByResponse(Response("write_to_file", "leonidas loves kotlin"))
executor.executeByResponse(Response("print", "$&#$@#", isError = true))
}
private fun createActionExecutor(): ActionExecutor {
val printInErrorStream = PrintInErrorStream().asChain { response -> response.isError }
val justPrint = Print().asChain { true }
return ActionExecutor().apply {
addAction(printInErrorStream.or(justPrint))
addAction(Log())
addAction(WriteToFile())
}
}

This way we can scale the strategy pattern both vertically, one strategy per action value, and horizontally, one strategy per action value sub cases.