Aplikasi yang dibangun tanpa pemisahan tanggung jawab biasanya tumbuh menjadi satu blok kode yang mengurus banyak hal sekaligus: membaca input pengguna, menjalankan aturan bisnis, menulis ke database, lalu merangkai tampilan. Perubahan kecil di satu sisi menuntut kita memahami seluruh blok karena semuanya saling terjalin. Separation of Concerns (SoC) adalah prinsip yang menyatakan: sistem harus dipecah menjadi bagian-bagian yang tiap bagian mengurus satu concern — satu kekhawatiran spesifik. Chapter ini membahas cara mengenali concern, memetakan concern ke modul, dan membuktikan bahwa pemisahan benar-benar memudahkan perubahan.

Concern adalah Alasan Berubah #

Concern adalah satu aspek fungsional yang dapat berubah secara independen — misalnya tampilan antarmuka, rumus bisnis, atau cara penyimpanan data. Uji praktisnya sederhana: bila dua bagian kode selalu berubah karena alasan yang sama, keduanya boleh menjadi satu modul. Bila keduanya berubah karena alasan berbeda, keduanya harus dipisah. Martin Fowler menyebut pemisahan presentasi–domain–data sebagai salah satu pemisahan paling mendasar dan paling sering terbukti berguna dalam arsitektur aplikasi.

Tiga concern klasik pada aplikasi bisnis dapat dipetakan seperti berikut:

ConcernPertanyaan yang dijawabContoh pemicu perubahan
PresentationBagaimana data ditampilkan?Ganti format, bahasa, atau kanal tampilan
Business logicApa aturan bisnisnya?Regulasi baru, rumus diskon berubah
PersistenceBagaimana data disimpan?Pindah database, ubah skema

Alur kerja ketiganya berjalan satu arah, dari pengguna menuju penyimpanan dan kembali:

Contoh Cicilan yang Tercampur #

Bentuk bermasalah paling umum adalah method yang sekaligus menghitung, menyimpan, dan mencetak. Potongan berikut memperlihatkan pola tersebut:

java
// Sebelum: satu method, tiga concern
void prosesPinjaman(long pokok, int tenor) {
    double i = 0.06 / 12;
    double angsuran = pokok * i / (1 - Math.pow(1 + i, -tenor));
    catatan.add("pinjaman|" + pokok + "|" + angsuran);   // persistence
    System.out.println("Angsuran: Rp" + angsuran);        // presentation
}

Method prosesPinjaman sulit diuji karena setiap pemanggilan mencetak ke layar dan menulis catatan; mengubah format cetak berisiko menyentuh rumus angsuran. Kita pecahkan menjadi tiga class, masing-masing satu concern:

import java.util.ArrayList;
import java.util.List;

public class KalkulatorCicilan {
    public static void main(String[] args) {
        CicilanService layanan = new CicilanService();
        RiwayatPinjaman riwayat = new RiwayatPinjaman();
        TampilanCicilan tampilan = new TampilanCicilan();

        Pinjaman pinjaman = new Pinjaman("Budi", 12_000_000, 12, 0.06);
        double angsuran = layanan.angsuranBulanan(pinjaman);
        riwayat.simpan(pinjaman, angsuran);

        System.out.println(tampilan.ringkasan(pinjaman, angsuran));
        System.out.println("Tersimpan: " + riwayat.jumlah() + " pinjaman");
    }
}

// Concern 1: aturan bisnis pinjaman
record Pinjaman(String nama, long pokok, int tenor, double bungaTahunan) {
}

class CicilanService {
    double angsuranBulanan(Pinjaman p) {
        double i = p.bungaTahunan() / 12;
        if (i == 0) {
            return (double) p.pokok() / p.tenor();
        }
        return p.pokok() * i / (1 - Math.pow(1 + i, -p.tenor()));
    }
}

// Concern 2: penyimpanan data
class RiwayatPinjaman {
    private final List<String> catatan = new ArrayList<>();

    void simpan(Pinjaman p, double angsuran) {
        catatan.add(p.nama() + "|" + p.pokok() + "|" + angsuran);
    }

    int jumlah() {
        return catatan.size();
    }
}

// Concern 3: penyajian ke pengguna
class TampilanCicilan {
    String ringkasan(Pinjaman p, double angsuran) {
        return String.format("%s meminjam Rp%d selama %d bulan%nAngsuran: Rp%.0f per bulan",
                p.nama(), p.pokok(), p.tenor(), angsuran);
    }
}

Output yang Dihasilkan:

text
Budi meminjam Rp12000000 selama 12 bulan
Angsuran: Rp1032797 per bulan
Tersimpan: 1 pinjaman

Mari kita bedah hasil pemisahan tersebut:

  1. Baris 6-8 (tiga new ... di main): method main berperan sebagai perakit — ia memilih komponen mana yang bekerja sama, tanpa menjalankan logika concern mana pun. Mengganti RiwayatPinjaman dengan versi berbasis file nanti tidak menyentuh class lain.
  2. Baris 15 (record Pinjaman): data pinjaman berdiri sendiri tanpa method teknis. Record memberi constructor, aksesor, dan equals berbasis nilai secara otomatis.
  3. Baris 19-28 (class CicilanService): class ini hanya menghitung. Tidak ada System.out, tidak ada penyimpanan — akibatnya method angsuranBulanan dapat diuji dengan memanggil angka langsung tanpa efek samping.
  4. Baris 31-42 (class RiwayatPinjaman dan TampilanCicilan): penyimpanan dan penyajian berdiri sebagai class terpisah. Mengubah format keluaran dari teks ke JSON hanya menyentuh TampilanCicilan; mengganti media simpan hanya menyentuh RiwayatPinjaman.

Batas Modul sebagai Kontrak #

Setelah concern terpisah, batas antarmodul menjadi kontrak: CicilanService menerima dan mengembalikan data, tidak peduli siapa pemanggilnya. Kita bisa memanggil class yang sama dari aplikasi web, aplikasi desktop, maupun pengujian otomatis. Pemisahan juga menekan biaya perubahan — concern yang berubah cepat (tampilan) tidak menyebarkan dampaknya ke concern yang stabil (aturan bisnis). Dalam praktik industri, pemisahan inilah yang kemudian dibakukan menjadi layered architecture, lalu disempurnakan menjadi hexagonal dan clean architecture.

Pemisahan concern menjelaskan apa yang dipisah; prinsip berikutnya menjawab ke arah mana hubungan antarbagi diputar agar domain tidak terikat detail teknis. Chapter selanjutnya, Dependency Inversion, membahas pembalikan arah dependensi tersebut.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

KalkulatorCicilan.java

Unduh berkas
java
import java.util.ArrayList;
import java.util.List;

public class KalkulatorCicilan {
    public static void main(String[] args) {
        CicilanService layanan = new CicilanService();
        RiwayatPinjaman riwayat = new RiwayatPinjaman();
        TampilanCicilan tampilan = new TampilanCicilan();

        Pinjaman pinjaman = new Pinjaman("Budi", 12_000_000, 12, 0.06);
        double angsuran = layanan.angsuranBulanan(pinjaman);
        riwayat.simpan(pinjaman, angsuran);

        System.out.println(tampilan.ringkasan(pinjaman, angsuran));
        System.out.println("Tersimpan: " + riwayat.jumlah() + " pinjaman");
    }
}

// Concern 1: aturan bisnis pinjaman
record Pinjaman(String nama, long pokok, int tenor, double bungaTahunan) {
}

class CicilanService {
    double angsuranBulanan(Pinjaman p) {
        double i = p.bungaTahunan() / 12;
        if (i == 0) {
            return (double) p.pokok() / p.tenor();
        }
        return p.pokok() * i / (1 - Math.pow(1 + i, -p.tenor()));
    }
}

// Concern 2: penyimpanan data
class RiwayatPinjaman {
    private final List<String> catatan = new ArrayList<>();

    void simpan(Pinjaman p, double angsuran) {
        catatan.add(p.nama() + "|" + p.pokok() + "|" + angsuran);
    }

    int jumlah() {
        return catatan.size();
    }
}

// Concern 3: penyajian ke pengguna
class TampilanCicilan {
    String ringkasan(Pinjaman p, double angsuran) {
        return String.format("%s meminjam Rp%d selama %d bulan%nAngsuran: Rp%.0f per bulan",
                p.nama(), p.pokok(), p.tenor(), angsuran);
    }
}