Adapter Design Pattern in Kotlin

1. Definition

The Adapter Design Pattern allows objects with incompatible interfaces to collaborate. It acts as a bridge between two incompatible interfaces by providing a compatible interface, usually by either extending an existing class or implementing an interface.

2. Problem Statement

Imagine you have a legacy code or a third-party library that provides the functionality you need, but its interface doesn't match the one expected by your application. How can you integrate such a mismatched piece without rewriting a lot of code?

3. Solution

The Adapter Pattern offers a solution by allowing you to create a wrapper class that maps the expected interface to the legacy or third-party interface. This wrapper is the "adapter" and allows you to use the existing functionality without changing the interface.

4. Real-World Use Cases

1. Translating data from one format to another (e.g., from XML to JSON).

2. Using legacy code in newer applications without changing the original code.

3. Integrating third-party libraries that have different interfaces than expected by your application.

5. Implementation Steps

1. Identify the target interface you want the Adapter to match.

2. Create the adapter class that implements the target interface.

3. Inside the adapter, reference the adaptee (the class you want to adapt).

4. Implement the methods of the target interface in the adapter, translating them to the appropriate calls on the adaptee.

6. Implementation in Kotlin

// Step 1: Target Interface
interface ModernMediaPlayer {
    fun playAudio(fileType: String, fileName: String)
}

// Step 2: Adaptee (the existing different interface)
class OldMediaPlayer {
    fun play(fileName: String) {
        println("Playing audio file $fileName with Old Media Player")
    }
}

// Step 3: Adapter Class
class MediaAdapter(private val oldPlayer: OldMediaPlayer) : ModernMediaPlayer {
    override fun playAudio(fileType: String, fileName: String) {
        // Using old player to play the file
        oldPlayer.play(fileName)
    }
}

fun main() {
    val oldPlayer = OldMediaPlayer()
    val adapter = MediaAdapter(oldPlayer)

    adapter.playAudio("mp3", "song.mp3")
}

Output:

Playing audio file song.mp3 with Old Media Player

Explanation:

1. We've defined a ModernMediaPlayer interface that represents the target interface we want our Adapter to match.

2. We then have an OldMediaPlayer class (adaptee) which has a different method signature and represents the legacy or third-party code.

3. The MediaAdapter class acts as our adapter, implementing the ModernMediaPlayer interface and containing a reference to the OldMediaPlayer class.

4. Inside the MediaAdapter, we override the playAudio method to call the play method of OldMediaPlayer.

5. Finally, in the main function, we demonstrate how we can use the adapter to play an audio file with the modern interface, but the actual functionality is carried out by the old media player.

7. When to use?

Use the Adapter Pattern when:

1. You want to use a class that has an interface that doesn't match the one you need.

2. You want to create a reusable class that cooperates with unrelated classes with incompatible interfaces.

3. You want to integrate and utilize legacy code or third-party libraries without altering their source code.

The Adapter pattern allows for great flexibility and can save a lot of effort when integrating mismatched interfaces.


Comments