Wednesday, May 10, 2023

Bridge Design Pattern in Kotlin comparative analysis with Java

 Bridge

Unlike some other design patterns we've met, Bridge is less about a smart way to compose objects, and more about guidelines on how not to abuse inheritance. The way it works is actually very simple. Let's go back to the strategy game we're building. We have an interface for all our infantry units:

 interface Infantry {

    fun move(x: Long, y: Long)

 fun attack(x: Long, y: Long)

We have the concrete implementations: 

open class Rifleman : Infantry {

    override fun attack(x: Long, y: Long) {

        // Shoot

    }

 override fun move(x: Long, y: Long) {

        // Move at its own pace

    }

}

 open class Grenadier : Infantry {

    override fun attack(x: Long, y: Long) {

        // Throw grenades

    }

 override fun move(x: Long, y: Long) {

        // Moves slowly, grenades are heavy

    }

}

What if we want to have the ability to upgrade our units? Upgraded units should have twice the damage, but move at the same pace: 

class UpgradedRifleman : Rifleman() {

    override fun attack(x: Long, y: Long) {

        // Shoot twice as much

    }

}

 class UpgradedGrenadier : Grenadier() {

    override fun attack(x: Long, y: Long) {

        // Throw pack of grenades

    }

Now, our game designer has decided that we also need a light version of those units. That is, they attack in the same way as regular units, but move at twice the speed: 

class LightRifleman : Rifleman() {

    override fun move(x: Long, y: Long) {

        // Running with rifle

    }

}

 class LightGrenadier : Grenadier() {

    override fun move(x: Long, y: Long) {

        // I've been to gym, pack of grenades is no problem

    }

Since design patterns are all about adapting to change, here comes our dear designer, and asks that all infantry units be able to shout, that is, to proclaim their unit name loud and clear: 

interface Infantry {

    // As before, move() and attack() functions

fun shout() // Here comes the change

What are we to do now? We go and change the implementation of six different classes, feeling lucky that there are only six and not sixteen.

Bridging changes 

Depending on the way you look at it, the Bridge design pattern may resemble Adapter, which we already discussed, or Strategy. The idea behind the Bridge design pattern is to flatten the class hierarchy, which is currently three levels deep: Infantry --> Rifleman  --> Upgraded Rifleman --> Light Rifleman            --> Grenadier --> Upgraded Grenadier --> Light Grenadier Why do we have this complex hierarchy? It's because we have three orthogonal properties: weapon type, weapon strength, and movement speed. Say instead, we were to pass those properties to the constructor of a class that implements the same interface we were using all along: 

class Soldier(private val weapon: Weapon,

     private val legs: Legs) : Infantry {

    override fun attack(x: Long, y: Long) {

        // Find target

        // Shoot

        weapon.causeDamage()

    }

 override fun move(x: Long, y: Long) {

        // Compute direction

        // Move at its own pace

        legs.move()

    }

}

The properties that Soldier receives should be interfaces, so we could choose their implementation later: interface Weapon {

    fun causeDamage(): PointsOfDamage

}

 interface Legs {

    fun move(): Meters

But what are Meters and PointsOfDamage? Are those classes or interfaces we declared somewhere? Let's take a short detour.

Type aliases 

First, we'll look at how they're declared: typealias PointsOfDamage = Long

typealias Meters = Int We use a new keyword here, typealias. From now on, we can use Meters instead of plain old Int to return from our move() method. They aren't new types. The Kotlin compiler will always translate PointsOfDamage to Long during compilation. Using them provides two advantages: Better semantics, as in our case. We can tell exactly what the meaning of the value we're returning is. One of the main goals of Kotlin is to be concise. Type aliases allow us to hide complex generic expressions. We'll expand on this in the following sections.

You're in the army now Back to our Soldier class. We want it to be as adaptable as possible, right? He knows he can move or use his weapon for greater good. But how exactly is he going to do that? We totally forgot to implement those parts! Let's start with our weapons: 

class Grenade : Weapon {

    override fun causeDamage() = GRENADE_DAMAGE

}

 class GrenadePack : Weapon {

    override fun causeDamage() = GRENADE_DAMAGE * 3

}

 class Rifle : Weapon {

    override fun causeDamage() = RIFLE_DAMAGE

}

 class MachineGun : Weapon {

    override fun causeDamage() = RIFLE_DAMAGE * 2

} Now, let's look at how we can move: class RegularLegs : Legs {

    override fun move() = REGULAR_SPEED

}

 class AthleticLegs : Legs {

    override fun move() = REGULAR_SPEED * 2

}

Constants We define all parameters as constants:

 const val GRENADE_DAMAGE : PointsOfDamage = 5L

const val RIFLE_DAMAGE = 3L

const val REGULAR_SPEED : Meters = 1 

Those values are very effective, since they are known during compilation. Unlike static final variables in Java, they cannot be placed inside a class. You should place them either at the top level of your package or nest them inside object. Note that although Kotlin has type inference, we can specify the types of our constants explicitly, and even use type aliases. How about having DEFAULT_TIMEOUT : Seconds = 60 instead of

DEFAULT_TIMEOUT_SECONDS = 60 in your code?

A lethal weapon What is left for us is to see that with the new hierarchy, we can still do the exact same things: val rifleman = Soldier(Rifle(), RegularLegs())

val grenadier = Soldier(Grenade(), RegularLegs())

val upgradedGrenadier = Soldier(GrenadePack(), RegularLegs())

val upgradedRifleman = Soldier(MachineGun(), RegularLegs())

val lightRifleman = Soldier(Rifle(), AthleticLegs())

val lightGrenadier = Soldier(Grenade(), AthleticLegs()) 

Now, our hierarchy looks like this: Infantry --> Soldier Weapon --> Rifle

       --> MachineGun

       --> Grenade

       --> GrenadePack 

 Legs --> RegularLegs

     --> AthleticLegs 

Much simpler to extend and also to comprehend. Unlike some other design patterns we discussed before, we didn't use any special language feature we didn't know about, just some engineering best practices.

Code Example :

fun main(args: Array<String>) {

    val rifleman = Soldier(Rifle(), RegularLegs())

    val grenadier = Soldier(Grenade(), RegularLegs())

    val upgradedGrenadier = Soldier(GrenadePack(), RegularLegs())

    val upgradedRifleman = Soldier(MachineGun(), RegularLegs())

    val lightRifleman = Soldier(Rifle(), AthleticLegs())

    val lightGrenadier = Soldier(Grenade(), AthleticLegs())

}

interface Infantry {

    fun move(x: Long, y: Long)

    fun attack(x: Long, y: Long)

}

typealias PointsOfDamage = Long

typealias Meters = Int

interface Weapon {

    fun causeDamage(): PointsOfDamage

}

const val GRENADE_DAMAGE : PointsOfDamage = 5L

const val RIFLE_DAMAGE = 3L

const val REGULAR_SPEED : Meters = 1


class Grenade : Weapon {

    override fun causeDamage() = GRENADE_DAMAGE

}


class GrenadePack : Weapon {

    override fun causeDamage() = GRENADE_DAMAGE * 3

}


class Rifle : Weapon {

    override fun causeDamage() = RIFLE_DAMAGE

}


class MachineGun : Weapon {

    override fun causeDamage() = RIFLE_DAMAGE * 2

}


class RegularLegs : Legs {

    override fun move() = REGULAR_SPEED

}


class AthleticLegs : Legs {

    override fun move() = REGULAR_SPEED * 2

}


interface Legs {

    fun move(): Meters

}


class Soldier(private val weapon: Weapon,

              private val legs: Legs) : Infantry {

    override fun attack(x: Long, y: Long) {

        // Find target

        // Shoot

        weapon.causeDamage()

    }

    override fun move(x: Long, y: Long) {

        // Compute direction

        // Move at its own pace

        legs.move()

    }

}







 

No comments: