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.
// 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:
=== 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.0Manfaat 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
PesananRepositoryyang berubah. - Test bisnis:
LayananPesanan.hitungDiskondapat 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, daninternaluntuk 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.