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:
| Concern | Pertanyaan yang dijawab | Contoh pemicu perubahan |
|---|---|---|
| Presentation | Bagaimana data ditampilkan? | Ganti format, bahasa, atau kanal tampilan |
| Business logic | Apa aturan bisnisnya? | Regulasi baru, rumus diskon berubah |
| Persistence | Bagaimana 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:
// 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:
Budi meminjam Rp12000000 selama 12 bulan
Angsuran: Rp1032797 per bulan
Tersimpan: 1 pinjamanMari kita bedah hasil pemisahan tersebut:
- Baris 6-8 (tiga
new ...dimain): methodmainberperan sebagai perakit — ia memilih komponen mana yang bekerja sama, tanpa menjalankan logika concern mana pun. MenggantiRiwayatPinjamandengan versi berbasis file nanti tidak menyentuh class lain. - Baris 15 (
record Pinjaman): data pinjaman berdiri sendiri tanpa method teknis. Record memberi constructor, aksesor, danequalsberbasis nilai secara otomatis. - Baris 19-28 (
class CicilanService): class ini hanya menghitung. Tidak adaSystem.out, tidak ada penyimpanan — akibatnya methodangsuranBulanandapat diuji dengan memanggil angka langsung tanpa efek samping. - Baris 31-42 (
class RiwayatPinjamandanTampilanCicilan): penyimpanan dan penyajian berdiri sebagai class terpisah. Mengubah format keluaran dari teks ke JSON hanya menyentuhTampilanCicilan; mengganti media simpan hanya menyentuhRiwayatPinjaman.
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 berkasimport 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);
}
}