Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
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.
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.
As mentioned earlier, this pattern needs three components:
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.
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())
}
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.
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.