Aplikasi modern melakukan banyak hal sekaligus: menerima input dari user, menjalankan aturan bisnis, menyimpan ke database, dan mengembalikan respons. Bila semua ini ditumpuk dalam satu class, perubahan kecil di satu sisi dapat merusak sisi lain. Separation of concerns adalah prinsip memecah aplikasi menjadi modul dengan tanggung jawab tunggal. Pada chapter ini kita akan membandingkan kode yang mencampur UI, business, dan data dalam satu class dengan versi yang memisahkannya menjadi tiga layer.

Tiga Layer Utama #

  • Presentation layer (UI): berinteraksi dengan user. Menampilkan data, menerima input. Bisa berupa CLI, web, atau mobile.
  • Business layer: aturan bisnis. Menghitung diskon, memvalidasi pesanan, koordinasi antar domain.
  • Data layer: persistensi. Menyimpan dan mengambil data dari DB, file, atau API eksternal.

Kode Sebelum dan Sesudah #

Program berikut membandingkan class AplikasiBuruk yang mencampur tiga concern dengan versi yang memisahkannya menjadi KonsolPesanan, LayananPesanan, dan PesananRepository.

kotlin
// Demo separation of concerns: UI vs business vs data layer.

// === BURUK: semua bercampur dalam satu class ===
class AplikasiBuruk {
    fun prosesPesanan(id: Int, nama: String, total: Double) {
        // UI logic: menampilkan ke user
        println("=== Memproses Pesanan ===")
        println("Pesanan #$id oleh $nama")
        // Business logic: hitung diskon
        val diskon = if (total > 100_000) total * 0.10 else 0.0
        val final = total - diskon
        println("Diskon: Rp$diskon, Bayar: Rp$final")
        // Data logic: simpan ke "database" (mock)
        println("[db] INSERT INTO pesanan VALUES ($id, '$nama', $final)")
        // UI logic: konfirmasi
        println("Pesanan tersimpan. Terima kasih!")
    }
}

// === BAIK: pemisahan tiga concern ===

// 1. Data layer: hanya urus persistensi
class PesananRepository {
    private val penyimpanan = mutableMapOf<Int, String>()

    fun simpan(id: Int, data: String) {
        penyimpanan[id] = data
        println("[repo] Pesanan $id disimpan")
    }

    fun ambil(id: Int): String? = penyimpanan[id]
}

// 2. Business layer: hanya urus aturan bisnis
class LayananPesanan(private val repo: PesananRepository) {
    fun hitungDiskon(total: Double): Double = if (total > 100_000) total * 0.10 else 0.0

    fun proses(id: Int, nama: String, total: Double): Double {
        val diskon = hitungDiskon(total)
        val final = total - diskon
        repo.simpan(id, "nama=$nama,total=$final")
        return final
    }
}

// 3. Presentation layer: hanya urus tampilan/user interaction
class KonsolPesanan(private val layanan: LayananPesanan) {
    fun tampilkanForm(id: Int, nama: String, total: Double) {
        println("=== Memproses Pesanan ===")
        println("Pesanan #$id oleh $nama")
        val final = layanan.proses(id, nama, total)
        val diskon = total - final
        println("Diskon: Rp$diskon")
        println("Bayar: Rp$final")
        println("Pesanan tersimpan. Terima kasih!")
    }
}

data class Pesanan(val id: Int, val nama: String, val total: Double)

fun main() {
    println("=== Demo Separation of Concerns ===")

    println("--- Versi buruk (campur aduk) ---")
    AplikasiBuruk().prosesPesanan(1, "Budi", 150_000.0)

    println()
    println("--- Versi baik (terpisah) ---")
    val repo = PesananRepository()
    val layanan = LayananPesanan(repo)
    val ui = KonsolPesanan(layanan)
    ui.tampilkanForm(2, "Siti", 150_000.0)

    println()
    println("--- Test ulang dengan data berbeda ---")
    ui.tampilkanForm(3, "Dewi", 50_000.0)

    println()
    println("--- Verifikasi persistence ---")
    println("Pesanan #2 di repo: ${repo.ambil(2)}")
    println("Pesanan #3 di repo: ${repo.ambil(3)}")
}

Output yang Dihasilkan:

text
=== Demo Separation of Concerns ===
--- Versi buruk (campur aduk) ---
=== Memproses Pesanan ===
Pesanan #1 oleh Budi
Diskon: Rp15000.0, Bayar: Rp135000.0
[db] INSERT INTO pesanan VALUES (1, 'Budi', 135000.0)
Pesanan tersimpan. Terima kasih!

--- Versi baik (terpisah) ---
=== Memproses Pesanan ===
Pesanan #2 oleh Siti
[repo] Pesanan 2 disimpan
Diskon: Rp15000.0
Bayar: Rp135000.0
Pesanan tersimpan. Terima kasih!

--- Test ulang dengan data berbeda ---
=== Memproses Pesanan ===
Pesanan #3 oleh Dewi
[repo] Pesanan 3 disimpan
Diskon: Rp0.0
Bayar: Rp50000.0
Pesanan tersimpan. Terima kasih!

--- Verifikasi persistence ---
Pesanan #2 di repo: nama=Siti,total=135000.0
Pesanan #3 di repo: nama=Dewi,total=50000.0

Manfaat Pemisahan #

Pada versi baik, setiap layer dapat berubah tanpa memengaruhi yang lain:

  • Ganti UI: tambah web UI tanpa ubah business atau data.
  • Ganti DB: migrasi dari in-memory ke Postgres, hanya PesananRepository yang berubah.
  • Test bisnis: LayananPesanan.hitungDiskon dapat di-test terpisah dengan mock repository.

Anti-Pola yang Sering Dijumpai #

  • Business logic di controller: kode diskon ditulis di handler HTTP, sehingga sulit dipakai ulang di job CLI.
  • SQL query di UI: kode presentation layer langsung memanggil JDBC, mencampur SQL dengan HTML.
  • Kalkulasi di SQL: stored procedure melakukan perhitungan bisnis kompleks, sehingga sulit diuji dan dipindahkan.

Referensi Lanjutan #

  • Kotlin documentation home — halaman utama dokumentasi Kotlin sebagai rujukan fitur package, interface, dan internal untuk pemisahan tanggung jawab

Penutup #

Separation of concerns adalah prinsip yang akan kita lihat berulang dalam berbagai pola arsitektur: layered, clean, hexagonal. Pada chapter berikutnya kita akan masuk ke Dependency Inversion, prinsip SOLID yang paling berpengaruh pada arsitektur, yang memungkinkan high-level module tetap bersih dari ketergantungan implementasi konkret.