Sunday, May 7, 2023

Adapter Design Pattern in Kotlin comparative analysis with Java

Adapter 

The main goal of an Adapter, or Wrapper, as it's sometimes called, is to convert one interface to another interface. In the physical world, the best example would be an electrical plug Adapter, or a USB Adapter. Imagine yourself in a hotel room in the late evening, with 7% battery left on your phone. Your phone charger was left in the office, at the other end of the city. You only have an EU plug charger with a USB mini cable. But your phone is USB type-C and near all your outlets are of  type-A. What do you do? So, now that we understand a bit better what adapters are for in the physical world, let's see how we can apply the same in code. Let's start with interfaces: 

interface UsbTypeC

interface UsbMini

interface EUPlug

interface USPlug Now we can declare a phone and a power outlet: // Power outlet exposes USPlug interface

fun powerOutlet() : USPlug {

    return object : USPlug {}

}

 fun cellPhone(chargeCable: UsbTypeC) {

 } 

 Our charger is wrong in every way, of course: // Charger accepts EUPlug interface and exposes UsbMini interface

fun charger(plug: EUPlug) : UsbMini {

    return object : UsbMini {}

Here we get the following errors: Type mismatch: required EUPlug, found USPlug: charger(powerOutlet())

Type mismatch: 

required UsbTypeC, found UsbMini: cellPhone(charger(powerOutlet()))

Different adapters So, we need two types of adapters. In Java, you would usually create a pair of classes for that purpose. In Kotlin, we can replace those with extension functions. We could adopt the US plug to work with the EU plug by using the following extension function:

 fun USPlug.toEUPlug() : EUPlug {

    return object : EUPlug {

        // Do something to convert 

    }

We can create a USB Adapter between mini USB and type-C USB in a similar way:

 fun UsbMini.toUsbTypeC() : UsbTypeC {

    return object : UsbTypeC {

        // Do something to convert

    }

And finally, we get back online by combining all those adapters together:

cellPhone(

    charger(

        powerOutlet().toEUPlug()

    ).toUsbTypeC()

)

 As you can see, we don't need to compose one object inside the other to adapt them. Nor, luckily, do we need to inherit both interface and implementation. With Kotlin, our code stays short and to the point.

 Adapters in the real world 

 You've probably encountered those adapters too. Mostly, they adapt between concepts and implementations. For example, let's take the concept of collection versus the concept of a stream: 

 val l = listOf("a", "b", "c")

 fun <T> streamProcessing(stream: Stream<T>) { 

    // Do something with stream

You cannot simply pass a collection to a function that receives a stream, even though it may make sense: streamProcessing(l) // Doesn't compile

Luckily, collections provide us with the .stream() method: 

streamProcessing(l.stream()) // Adapted successfully

Caveats of using adapters Did you ever plug a 110v appliance into a 220v socket through an Adapter, and fry it totally? That's something that may also happen to your code, if you're not careful. 

The following example, which uses another Adapter, compiles well:

 fun <T> collectionProcessing(c: Collection<T>) {

    for (e in c) {

        println(e)

    }

}

 val s = Stream.generate { 42 }

collectionProcessing(s.toList()) But it never completes, because Stream.generate() produces an infinite list of integers. 

So, be careful, and adapt this pattern wisely.

Code Example :

AdpaterDesignPattern.kt

import java.util.stream.Stream

fun <T> collectionProcessing(c: Collection<T>) {

    for (e in c) {

        println(e)

    }

}

fun <T> streamProcessing(stream: Stream<T>) {

    // Do something with stream

}

fun USPlug.toEUPlug(): EUPlug {

    return object : EUPlug {

        // Do something to convert

    }

}

fun UsbMini.toUsbTypeC(): UsbTypeC {

    return object : UsbTypeC {

        // Do something to convert

    }

}

// Power outlet exposes USPlug interface

fun powerOutlet(): USPlug {

    return object : USPlug {}

}

// Charger accepts EUPlug interface and exposes UsbMini interface

fun charger(plug: EUPlug): UsbMini {

    return object : UsbMini {}

}

fun cellPhone(chargeCable: UsbTypeC) {

}

interface UsbTypeC

interface UsbMini

interface EUPlug

interface USPlug

main.kt 

import java.util.stream.Stream

fun main(args: Array<String>) {

    // Adapter Design Pattern test

    cellPhone(charger(powerOutlet().toEUPlug()).toUsbTypeC())

    val l = listOf("a", "b", "c")

    streamProcessing(l.stream())

    val s = Stream.generate { 42 }

    println("Collecting elements")

    collectionProcessing(s.toList())

}


No comments: