Kihagyás

Open-Closed principle

"software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification"

Sounds spooky doesn't it Well, if you need a new feature sometimes you cannot just notify everyone like:

"IDC if you your plane crashes, that function now needs and extra string as parameter"

A class can allow its behaviour to be extended without modifying the rest

Example class

class Footballer(private var name: String, private var age: Int, private var role: String) {

    fun getFootballerRole(): String {
        return when (role) {
            "goalkeeper" -> "The footballer, ${this.name} is a goalkeeper"
            "defender" -> "The footballer, ${this.name} is a defender"
            "midfielder" -> "The footballer, ${this.name} is a midfielder"
            "forward" -> "The footballer, ${this.name} plays in the forward line"
            else -> throw Exception("Unsupported (╯°□°)╯︵ ┻━┻");
        }
    }
}

I hope we can all see where this is goin'

When a new player role emerge, we must modify this part, which can break other's code, or this will not be changed, and the new role will cause trouble wherever it's used

Solution for the class

Ok, so the last time we tried to make it work, it was a momentary solution now plan for the future, 'cause there is no such thing as finished code

A useful and common way to ensure the open-closed principle is to use inheritance, and never modify the parent class

Here comes the boilerplate, but keep in mind that now every role can behave differently, and the other's functionality won't be affected at all...

abstract class PlayerRole {
    abstract fun getRole(): String
}

internal class GoalkeeperRole : PlayerRole() {
    override fun getRole(): String {
        return "goalkeeper"
    }
}


internal class DefenderRole : PlayerRole() {
    override fun getRole(): String {
        return "defender"
    }
}


internal class MidfieldRole : PlayerRole() {
    override fun getRole(): String {
        return "midfielder"
    }
}


internal class ForwardRole : PlayerRole() {
    override fun getRole(): String {
        return "forward"
    }
}

class FootballPlayer(private var name: String, private var age: Int, private var role: PlayerRole) {

    fun getRole(): String {
        return role.getRole()
    }
}

It looks kinda similar to the dependency injection, but it's just one example

Example function

Here's another example, but now with a function

fun calculatePrice(price: Int, discount: String): Int {
    if (discount == "Black Friday") {
        return (price * 1.1).toInt()
    } else if (discount == "Joe") {
        return price / 2
    } else if (discount == "outlet") {
        return price / 3
    }
    /*...*/
    return price
}

"Oh, so you want another kind of discount, and the if statement is already 10k lines long??!"

You might want to create a constant and extend that later

Solution for the function

In this case, each type is assigned to a discount value, and the maintenance will remain cheap

val discounts = arrayOf(
    Pair("Black Friday", 1.1),
    Pair("Joe", 0.5),
    Pair("outlet", 0.33),
    /* ... ,*/
    Pair("Családvédelmi akcióterv", 3.0)
)

Now the calculate function can remain the same one-liner we learned from Haskell, our beloved

fun calculate(price: Int, discount: String): Int {
    return (price * (discounts.find { p -> p.first == discount }?.second ?: 1.0)).toInt()
}