Adapter Pattern adalah pola structural yang menyatukan dua interface yang tidak kompatibel. Adapter bekerja sebagai penerjemah: klien memanggil method yang familiar, adapter mengonversi panggilan tersebut menjadi method yang dipahami target. Pada chapter ini kita akan membangun beberapa adapter: class adapter yang membungkus logger lama, extension function sebagai adapter idiomatic, adapter untuk konversi List ke JSON, serta adapter untuk parsing JSON string menjadi data class.

Kapan Adapter Diperlukan #

Adapter berguna ketika kode sudah ada (legacy library, third-party service) yang interfacenya tidak sesuai dengan harapan kode baru. Mengubah kode legacy seringkali tidak mungkin (vendor) atau berisiko (sudah dipakai banyak tempat). Adapter menjadi jembatan tanpa menyentuh kode legacy.

SkenarioAdapter yang cocok
Library lama dengan interface berbedaClass adapter yang mengimplementasi target interface
Perlu beberapa method konversi pada tipe yang samaExtension function
Konversi struktur data (Map ↔ data class)Adapter class dengan method toX()
Parsing format string ke tipe domainAdapter + parser

Implementasi Adapter #

kotlin
// Demo Adapter Pattern: wraps incompatible interface; extension function as adapter.

// === Target interface yang diharapkan klien ===
interface LoggerModern {
    fun log(level: String, message: String)
    fun error(message: String) = log("ERROR", message)
    fun info(message: String) = log("INFO", message)
    fun warn(message: String) = log("WARN", message)
}

// === Adaptee: library lama dengan interface berbeda ===
class LegacyLogger {
    fun writeLog(priority: Int, message: String) {
        val level = when (priority) {
            1 -> "INFO"
            2 -> "WARN"
            3 -> "ERROR"
            else -> "UNKNOWN"
        }
        println("[legacy] $level: $message")
    }
}

// === Adapter: class yang mengimplementasi Target dan membungkus Adaptee ===
class LegacyLoggerAdapter(private val legacy: LegacyLogger) : LoggerModern {
    override fun log(level: String, message: String) {
        val priority = when (level.uppercase()) {
            "INFO" -> 1
            "WARN" -> 2
            "ERROR" -> 3
            else -> 0
        }
        legacy.writeLog(priority, message)
    }
}

// === Extension function as adapter (idiomatic Kotlin) ===
fun LegacyLogger.logModern(level: String, message: String) {
    val priority = when (level.uppercase()) {
        "INFO" -> 1
        "WARN" -> 2
        "ERROR" -> 3
        else -> 0
    }
    writeLog(priority, message)
}

// === Adapter untuk collection: List<Int> -> JSON array string ===
class ListToJsonAdapter(private val list: List<Int>) {
    fun toJson(): String = "[" + list.joinToString(",") + "]"
}

// === Adapter untuk Map<String, Any> -> data class instance ===
data class Pengguna(val id: Int, val nama: String, val email: String)

class MapToPenggunaAdapter(private val map: Map<String, Any>) {
    fun toPengguna(): Pengguna {
        @Suppress("UNCHECKED_CAST")
        return Pengguna(
            id = (map["id"] as Number).toInt(),
            nama = map["nama"] as String,
            email = map["email"] as String
        )
    }
}

// === Adapter untuk third-party JSON library (simulated) ===
// Misalkan kita punya library JSON yang mengembalikan String, padahal kita butuh Map
class JsonStringAdapter(private val json: String) {
    // Parser sangat sederhana untuk JSON seperti {"id":1,"nama":"Budi","email":"[email protected]"}
    fun toMap(): Map<String, Any> {
        val body = json.trim().removeSurrounding("{", "}")
        return body.split(",").associate { pair ->
            val (k, v) = pair.split(":", limit = 2)
            val key = k.trim().trim('"')
            val rawVal = v.trim()
            val value: Any = if (rawVal.startsWith("\"")) rawVal.trim('"')
            else rawVal.toIntOrNull() ?: rawVal
            key to value
        }
    }
}

fun main() {
    println("=== Demo Adapter Pattern ===")

    println("--- 1. Class Adapter (LegacyLogger -> LoggerModern) ---")
    val modern: LoggerModern = LegacyLoggerAdapter(LegacyLogger())
    modern.info("Aplikasi dimulai")
    modern.error("Koneksi gagal")
    modern.warn("Stok menipis")

    println()
    println("--- 2. Extension function as adapter ---")
    val legacy = LegacyLogger()
    legacy.logModern("INFO", "via extension function")
    legacy.logModern("ERROR", "via extension function")

    println()
    println("--- 3. List -> JSON ---")
    val daftar = listOf(1, 2, 3, 4, 5)
    val json = ListToJsonAdapter(daftar).toJson()
    println("List: $daftar -> JSON: $json")

    println()
    println("--- 4. Map -> data class ---")
    val map = mapOf("id" to 1, "nama" to "Budi", "email" to "[email protected]")
    val pengguna = MapToPenggunaAdapter(map).toPengguna()
    println("Map: $map -> Pengguna: $pengguna")

    println()
    println("--- 5. JSON string -> Map -> data class ---")
    val jsonString = """{"id":42,"nama":"Siti","email":"[email protected]"}"""
    val parsed = JsonStringAdapter(jsonString).toMap()
    println("JSON: $jsonString")
    println("Map: $parsed")
    val p = MapToPenggunaAdapter(parsed).toPengguna()
    println("Pengguna: $p")
}

Output yang Dihasilkan:

text
=== Demo Adapter Pattern ===
--- 1. Class Adapter (LegacyLogger -> LoggerModern) ---
[legacy] INFO: Aplikasi dimulai
[legacy] ERROR: Koneksi gagal
[legacy] WARN: Stok menipis

--- 2. Extension function as adapter ---
[legacy] INFO: via extension function
[legacy] ERROR: via extension function

--- 3. List -> JSON ---
List: [1, 2, 3, 4, 5] -> JSON: [1,2,3,4,5]

--- 4. Map -> data class ---
Map: {id=1, nama=Budi, [email protected]} -> Pengguna: Pengguna(id=1, nama=Budi, [email protected])

--- 5. JSON string -> Map -> data class ---
JSON: {"id":42,"nama":"Siti","email":"[email protected]"}
Map: {id=42, nama=Siti, [email protected]}
Pengguna: Pengguna(id=42, nama=Siti, [email protected])

Bedah Kode Baris-per-Baris #

Mari kita perhatikan bagian kunci:

  1. Baris 4-9 (LoggerModern): Interface modern yang dipakai klien baru. Method default error, info, warn mendelegasikan ke log sehingga adapter hanya perlu mengimplementasi satu method.
  2. Baris 12-22 (LegacyLogger): Adaptee dengan interface berbeda. Method writeLog menerima priority integer, bukan string level. Mengubah class ini berisiko karena mungkin sudah dipakai banyak tempat.
  3. Baris 25-35 (LegacyLoggerAdapter): Adapter yang mengimplementasi LoggerModern dan membungkus LegacyLogger. Method log menerjemahkan string level menjadi priority integer, lalu memanggil writeLog.
  4. Baris 38-46 (LegacyLogger.logModern): Extension function sebagai adapter. Tidak membuat class baru; kita hanya menambah method ke LegacyLogger dari luar. Cocok bila konversi cukup sederhana dan tidak perlu instance adapter terpisah.
  5. Baris 69-82 (JsonStringAdapter): Adapter untuk parsing JSON string menjadi Map<String, Any>. Parser-nya sangat sederhana dan hanya mendukung JSON flat dengan tipe dasar. Untuk produksi, gunakan kotlinx.serialization atau Gson.

Memilih Class Adapter vs Extension Function #

PertanyaanJika ya, pilih
Apakah perlu mengimplementasi interface target?Class adapter
Apakah perlu akses private member?Class adapter
Apakah konversi hanya menambah beberapa method publik?Extension function
Apakah klien sudah memakai tipe adaptee secara langsung?Extension function

Penutup #

Adapter Pattern menyatukan interface yang tidak kompatibel tanpa mengubah kode legacy. Kotlin menyediakan dua pendekatan: class adapter untuk konversi kompleks dan extension function untuk konversi sederhana. Pada chapter berikutnya kita akan membahas Decorator Pattern: bagaimana menambah behavior ke object tanpa mengubah kode aslinya, memanfaatkan by delegation.

Referensi #

Referensi Lanjutan #

  • Kotlin documentation home — halaman utama dokumentasi Kotlin untuk interface dan extension function sebagai sarana adapter pattern