1. Introduction
As Kotlin developers, chances are we’ve heard of the adapter pattern. It’s a structural design pattern commonly used in software development to help classes work together that otherwise wouldn’t be able to.
In this tutorial, we’ll delve deeper into the adapter pattern, its pros and cons, and how it can be implemented in Kotlin.
2. What Is the Adapter Pattern in Kotlin
The adapter pattern is a structural design pattern that permits two somewhat incompatible interfaces to work together. It acts as a bridge between two classes with different interfaces, allowing them to work together seamlessly. It comprises three main components: the client, the adapter, and the adaptee.
- The client is the class that needs to use the adaptee, but its interface is incompatible with the adaptee’s interface.
- The adapter is the class that bridges the gap between the client and the adaptee. It implements the interface the client expects to use and delegates the calls to the adaptee.
- The adaptee is the actual class interface that the client needs to implement functionalities, but its interface is incompatible with the client’s interface.
3. Implementation: Building an Audio Player
3.1. Participants
As mentioned earlier, this pattern needs three components:
- AudioPlayer: This is the client, as it is the class that uses the MediaPlayer interface to play audio files.
- MediaAdapter: This is the adapter, as it adapts the VlcPlayer and Mp4Player classes. It represents the bridge between the AdvancedMediaPlayer interface and the MediaPlayer interface, making it usable by the AudioPlayer.
- VlcPlayer and Mp4Player: These are the adaptees, as they are responsible for implementing the incompatible AdvancedMediaPlayer interface.
Now, let’s look at what each of these classes looks like.
Firstly, we need our incompatible interfaces; MediaPlayer and AdvancedMediaPlayer:
interface MediaPlayer {
fun play(audioType: String, fileName: String)
}
interface AdvancedMediaPlayer {
fun playVlc(fileName: String)
fun playMp4(fileName: String)
}
The MediaPlayer doesn’t know what kind of file it has to play and normally should be able to play whatever is given to it, unlike the AdvancedMediaPlayer interface, which can play only .vls and .mp4 files.
Our VlcPlayer and Mp4Player adapters will look like this:
class VlcPlayer : AdvancedMediaPlayer {
override fun playVlc(fileName: String) {
println("Playing vlc file. Name: $fileName")
}
override fun playMp4(fileName: String) {
}
}
class Mp4Player : AdvancedMediaPlayer {
override fun playVlc(fileName: String) {
}
override fun playMp4(fileName: String) {
println("Playing mp4 file. Name: $fileName")
}
}
Each adapter is only concerned with the type of files they can play, disregarding any other formats.
To make both VlcPlayer and Mp4Player adaptees work together, we need the bridge – MediaAdapter:
class MediaAdapter(audioType: String) : MediaPlayer {
private var advancedMediaPlayer: AdvancedMediaPlayer? = null
init {
if (audioType.equals("vlc", true)) {
advancedMediaPlayer = VlcPlayer()
} else if (audioType.equals("mp4", true)) {
advancedMediaPlayer = Mp4Player()
}
}
override fun play(audioType: String, fileName: String) {
if (audioType.equals("vlc", true)) {
advancedMediaPlayer?.playVlc(fileName)
} else if (audioType.equals("mp4", true)) {
advancedMediaPlayer?.playMp4(fileName)
}
}
}
The MediaAdapter implements the MediaPlayer interface, and its constructor takes an audioType parameter to determine which AdvancedMediaPlayer to use. The play() method decides the correct playVlc() or playMp4() method of the AdvancedMediaPlayer.
Lastly, we have the AudioPlayer client:
class AudioPlayer : MediaPlayer {
private var mediaAdapter: MediaAdapter? = null
override fun play(audioType: String, fileName: String) {
if (audioType.equals("mp3", true)) {
println("Playing mp3 file. Name: $fileName")
} else if (audioType.equals("vlc", true) || audioType.equals("mp4", true)) {
mediaAdapter = MediaAdapter(audioType)
mediaAdapter?.play(audioType, fileName)
} else {
println("Invalid media. $audioType format not supported")
}
}
}
It implements the MediaPlayer interface. Its play() method first checks if the audioType is mp3, in which case it can play the file directly. If the audioType is “vlc” of “mp4“, it creates a MediaAdapter with the appropriate audioType and finally makes a call to the play() method with the audioType and fileName values.
3.2. Testing
Now, let’s test our client, the AudioPlayer class. All we need to ensure is that this class correctly plays various audio files. We are going to set the System.out output to a ByteArrayOutputStream to capture the output. Subsequently, we just call the play function on different file types and names:
@Test
fun testAudioPlayer() {
val outContent = ByteArrayOutputStream()
System.setOut(PrintStream(outContent))
val audioPlayer = AudioPlayer()
audioPlayer.play("mp3", "fetch_water.mp3")
assertEquals("Playing mp3 file. Name: fetch_water.mp3\n", outContent.toString())
outContent.reset()
audioPlayer.play("mp4", "get_lost.mp4")
assertEquals("Playing mp4 file. Name: get_lost.mp4\n", outContent.toString())
outContent.reset()
audioPlayer.play("vlc", "life_lessons.vlc")
assertEquals("Playing vlc file. Name: life_lessons.vlc\n", outContent.toString())
outContent.reset()
audioPlayer.play("avi", "still_waters.avi")
assertEquals("Invalid media. avi format not supported\n", outContent.toString())
}
4. Pros and Cons of the Adapter Pattern
One of the biggest advantages of the adapter pattern is that it allows classes with incompatible interfaces to work together. This can save time and effort, eliminating the need to modify existing code to make it compatible with new code. The adapter pattern also promotes code reusability, as it allows developers to reuse existing code without having to rewrite it. Additionally, the adapter pattern can help to simplify complex code by breaking it down into smaller, more manageable pieces.
However, the adapter pattern does introduce some drawbacks. One of the biggest disadvantages is that it can introduce additional complexity to the code. This is because the adapter class must be able to translate between two different interfaces, which can be challenging depending on the complexity of the codebase. The adapter pattern can also lead to additional overhead, as the adapter class must be instantiated and maintained.
5. Conclusion
In this article, we have discussed the importance of the adapter pattern in software development and demonstrated the use of this pattern in Kotlin to create a player empowered to play a couple of audio files. Also, we discuss some merits and demerits of this pattern.
As always, the code samples and relevant test cases about this article can be found over on GitHub.