Ketika aplikasi melebihi beberapa ratus class, penataan menjadi soal hidup atau matinya kecepatan tim. Pola penataan tertua dan paling banyak dipakai adalah layered architecture: aplikasi disusun menjadi lapisan-lapisan yang tersusun vertikal — presentation menerima permintaan, service menjalankan aturan bisnis, persistence menyimpan data. Setiap lapisan hanya mengenal lapisan tepat di bawahnya, seperti lantai gedung yang menumpang pada lantai bawah. Chapter ini membahas aturan ketergantungannya, contoh mini yang dapat dijalankan, serta kritik klasik terhadap kelemahan pola ini: anemic domain model.
Tiga Lapis Klasik #
| Lapisan | Tanggung jawab | Contoh teknologi |
|---|---|---|
| Presentation | Menerima masukan, menyajikan hasil | REST controller, halaman web, CLI |
| Service (business) | Aturan bisnis, orkestrasi, transaksi | LayananProduk, use case |
| Persistence (repository) | Menyimpan dan mengambil data | JDBC, JPA, berkas |
Alur satu permintaan berjalan turun lapis demi lapis, lalu hasilnya naik kembali:
Pembagian ini mengulang prinsip separation of concerns pada skala aplikasi: lapisan presentasi boleh berubah total (dari CLI ke REST) tanpa service ikut berubah, dan database boleh diganti tanpa aturan bisnis tersentuh.
Aturan Ketergantungan Turun #
Satu aturan sederhana menjaga seluruh struktur: dependensi hanya boleh mengalir ke bawah. Presentation mengenal service; service mengenal persistence; persistence tidak mengenal siapa pun di atasnya. Pelanggaran paling umum berupa service yang mengembalikan objek ResultSet JDBC ke controller — detail penyimpanan naik menembus dua lapisan, dan mengganti database memaksa kode presentasi ikut berubah. Pada aplikasi ketat, batas antarlapisan dipertegas dengan interface: service bergantung pada interface PenyimpanProduk, bukan pada class persistence konkret — persis prinsip dependency inversion yang sudah kita bahas.
Contoh Aplikasi Berlapis #
Program berikut memindahkan toko kopi ke dalam tiga lapisan; main berperan sebagai perakit yang menyusun lapisan paling bawah lebih dulu:
import java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
public class AplikasiBerlapis {
public static void main(String[] args) {
PenyimpanProduk repository = new PenyimpanProduk();
LayananProduk service = new LayananProduk(repository);
MenuKonsol presentation = new MenuKonsol(service);
presentation.tampilkanKatalog();
presentation.pesanProduk("P-001", 2);
presentation.pesanProduk("P-002", 1);
presentation.pesanProduk("P-002", 5); // stok tidak cukup
presentation.tampilkanKatalog();
}
}
// --- Lapisan presentation: menerima permintaan dan menyajikan hasil ---
class MenuKonsol {
private final LayananProduk layanan;
MenuKonsol(LayananProduk layanan) {
this.layanan = layanan;
}
void tampilkanKatalog() {
System.out.println("== Katalog ==");
layanan.daftarProduk().forEach(p -> System.out.println(
p.kode() + " | " + p.nama() + " | Rp" + p.harga() + " | sisa " + p.stok()));
}
void pesanProduk(String kode, int jumlah) {
System.out.println(layanan.pesan(kode, jumlah));
}
}
// --- Lapisan service: aturan bisnis ---
class LayananProduk {
private final PenyimpanProduk penyimpan;
LayananProduk(PenyimpanProduk penyimpan) {
this.penyimpan = penyimpan;
}
List<Produk> daftarProduk() {
return penyimpan.semua();
}
String pesan(String kode, int jumlah) {
Optional<Produk> hasil = penyimpan.cari(kode);
if (hasil.isEmpty()) {
return "Produk " + kode + " tidak ditemukan";
}
Produk produk = hasil.get();
if (produk.stok() < jumlah) {
return "Gagal memesan " + produk.nama() + ": stok hanya " + produk.stok();
}
penyimpan.simpan(produk.withStok(produk.stok() - jumlah));
return "Berhasil memesan " + jumlah + " " + produk.nama();
}
}
// --- Lapisan persistence: penyimpanan data ---
class PenyimpanProduk {
private final Map<String, Produk> data = new LinkedHashMap<>();
PenyimpanProduk() {
data.put("P-001", new Produk("P-001", "Kopi Gayo 250g", 75_000, 5));
data.put("P-002", new Produk("P-002", "Teh Melati 100g", 40_000, 3));
}
Optional<Produk> cari(String kode) {
return Optional.ofNullable(data.get(kode));
}
List<Produk> semua() {
return List.copyOf(data.values());
}
void simpan(Produk produk) {
data.put(produk.kode(), produk);
}
}
record Produk(String kode, String nama, long harga, int stok) {
Produk withStok(int stokBaru) {
return new Produk(kode, nama, harga, stokBaru);
}
}Output yang Dihasilkan:
== Katalog ==
P-001 | Kopi Gayo 250g | Rp75000 | sisa 5
P-002 | Teh Melati 100g | Rp40000 | sisa 3
Berhasil memesan 2 Kopi Gayo 250g
Berhasil memesan 1 Teh Melati 100g
Gagal memesan Teh Melati 100g: stok hanya 2
== Katalog ==
P-001 | Kopi Gayo 250g | Rp75000 | sisa 3
P-002 | Teh Melati 100g | Rp40000 | sisa 2Mari kita bedah arah ketergantungan pada program tersebut:
- Baris 8-10 (
mainsebagai perakit): urutan pembuatan meniru arsitektur — persistence lebih dulu, service menerima persistence, presentation menerima service. Tidak ada lapisan yang menciptakan lapisan di atasnya. - Baris 24-26 (
MenuKonsolhanya mengenalLayananProduk): presentation tidak mengimporPenyimpanProduksama sekali. Mengganti tampilan menjadi REST controller hanya menulis ulang class ini; lapisan lain diam. - Baris 74-76 (
caridi persistence): service berbicara bahasa domain —cari,simpan,semua— bukan bahasa SQL. Pada aplikasi nyata, method-method ini menjadiinterface PenyimpanProdukdan implementasi JDBC mengikuti kontraknya. - Baris 51-62 (
pesandi service): seluruh aturan bisnis — produk harus ada, stok harus cukup, stok dikurangi — terkumpul di satu method. Pemanggilanpenyimpan.simpan(produk.withStok(...))membuatProdukrecord yang tidak dapat diubah, lalu lapisan persistence menyimpannya.
Kritik Model Anemia #
Perhatikan Produk pada program di atas: record tersebut hanya menyimpan data — seluruh perilaku (validasi stok, pengurangan) tinggal di LayananProduk. Martin Fowler menyebut gaya ini anemic domain model: objek domain yang “kurus”, dengan logika bisnis berpindah ke service yang menjadi prosedur besar. Untuk aplikasi CRUD sederhana, gaya ini justru jujur dan mudah dibaca. Masalah muncul ketika domain menjadi kaya aturan: invarian stok tidak lagi dijaga objeknya sendiri, setiap service baru harus mengingat aturan yang sama, dan duplikasi logika menyebar. Kritik inilah yang mendorong pola berikutnya — meletakkan domain di pusat dan membalik seluruh arah dependensi. Chapter selanjutnya membahas Hexagonal Architecture.
Referensi #
- Martin Fowler: Service Layer (PoEAA)
- Martin Fowler: AnemicDomainModel
- Martin Fowler: PresentationDomainDataLayering
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
AplikasiBerlapis.java
Unduh berkasimport java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
public class AplikasiBerlapis {
public static void main(String[] args) {
PenyimpanProduk repository = new PenyimpanProduk();
LayananProduk service = new LayananProduk(repository);
MenuKonsol presentation = new MenuKonsol(service);
presentation.tampilkanKatalog();
presentation.pesanProduk("P-001", 2);
presentation.pesanProduk("P-002", 1);
presentation.pesanProduk("P-002", 5); // stok tidak cukup
presentation.tampilkanKatalog();
}
}
// --- Lapisan presentation: menerima permintaan dan menyajikan hasil ---
class MenuKonsol {
private final LayananProduk layanan;
MenuKonsol(LayananProduk layanan) {
this.layanan = layanan;
}
void tampilkanKatalog() {
System.out.println("== Katalog ==");
layanan.daftarProduk().forEach(p -> System.out.println(
p.kode() + " | " + p.nama() + " | Rp" + p.harga() + " | sisa " + p.stok()));
}
void pesanProduk(String kode, int jumlah) {
System.out.println(layanan.pesan(kode, jumlah));
}
}
// --- Lapisan service: aturan bisnis ---
class LayananProduk {
private final PenyimpanProduk penyimpan;
LayananProduk(PenyimpanProduk penyimpan) {
this.penyimpan = penyimpan;
}
List<Produk> daftarProduk() {
return penyimpan.semua();
}
String pesan(String kode, int jumlah) {
Optional<Produk> hasil = penyimpan.cari(kode);
if (hasil.isEmpty()) {
return "Produk " + kode + " tidak ditemukan";
}
Produk produk = hasil.get();
if (produk.stok() < jumlah) {
return "Gagal memesan " + produk.nama() + ": stok hanya " + produk.stok();
}
penyimpan.simpan(produk.withStok(produk.stok() - jumlah));
return "Berhasil memesan " + jumlah + " " + produk.nama();
}
}
// --- Lapisan persistence: penyimpanan data ---
class PenyimpanProduk {
private final Map<String, Produk> data = new LinkedHashMap<>();
PenyimpanProduk() {
data.put("P-001", new Produk("P-001", "Kopi Gayo 250g", 75_000, 5));
data.put("P-002", new Produk("P-002", "Teh Melati 100g", 40_000, 3));
}
Optional<Produk> cari(String kode) {
return Optional.ofNullable(data.get(kode));
}
List<Produk> semua() {
return List.copyOf(data.values());
}
void simpan(Produk produk) {
data.put(produk.kode(), produk);
}
}
record Produk(String kode, String nama, long harga, int stok) {
Produk withStok(int stokBaru) {
return new Produk(kode, nama, harga, stokBaru);
}
}