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 #

LapisanTanggung jawabContoh teknologi
PresentationMenerima masukan, menyajikan hasilREST controller, halaman web, CLI
Service (business)Aturan bisnis, orkestrasi, transaksiLayananProduk, use case
Persistence (repository)Menyimpan dan mengambil dataJDBC, 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:

text
== 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 2

Mari kita bedah arah ketergantungan pada program tersebut:

  1. Baris 8-10 (main sebagai perakit): urutan pembuatan meniru arsitektur — persistence lebih dulu, service menerima persistence, presentation menerima service. Tidak ada lapisan yang menciptakan lapisan di atasnya.
  2. Baris 24-26 (MenuKonsol hanya mengenal LayananProduk): presentation tidak mengimpor PenyimpanProduk sama sekali. Mengganti tampilan menjadi REST controller hanya menulis ulang class ini; lapisan lain diam.
  3. Baris 74-76 (cari di persistence): service berbicara bahasa domain — cari, simpan, semua — bukan bahasa SQL. Pada aplikasi nyata, method-method ini menjadi interface PenyimpanProduk dan implementasi JDBC mengikuti kontraknya.
  4. Baris 51-62 (pesan di service): seluruh aturan bisnis — produk harus ada, stok harus cukup, stok dikurangi — terkumpul di satu method. Pemanggilan penyimpan.simpan(produk.withStok(...)) membuat Produk record 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 #

Berkas Sumber

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

AplikasiBerlapis.java

Unduh berkas
java
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);
    }
}