Repository Pattern adalah pola yang mengabstraksi akses data di belakang interface collection-like. Domain atau application layer memanggil method simpan, cari, semua, hapus tanpa peduli apakah data disimpan di database relasional, document store, cache, atau API eksternal. Pada chapter ini kita akan membangun repository generik Repository<T, ID>, decorator cache di atas repository lain, specification pattern untuk query terstruktur, serta adapter API yang menyimulasikan akses REST.
Karakteristik Repository #
| Aspek | Penjelasan |
|---|---|
| Generic interface | Repository<T, ID> bekerja untuk semua entity |
| Sembunyikan detail storage | Tidak ada SQL, JSON, atau HTTP di kode pemanggil |
| Mendukung decorator | Cache, logging, transaction bisa ditumpuk |
| Berorientasi aggregate | Satu repository per aggregate root |
Repository adalah abstraction di atas data source, bukan tempat menulis SQL. Jika kode memanggil repo.cari(id), ia tidak peduli apakah cari akhirnya mengeksekusi SELECT di Postgres, GET ke REST API, atau lookup di cache.
Implementasi Repository dengan Beberapa Strategi #
// Demo Repository Pattern: abstract data access, generic Repository<T, ID>.
import java.time.LocalDate
// === Entity ===
data class Pengguna(val id: Int, val nama: String, val email: String, val daftar: LocalDate)
// === Generic Repository interface ===
interface Repository<T, ID> {
fun simpan(entity: T): T
fun cari(id: ID): T?
fun semua(): List<T>
fun hapus(id: ID): Boolean
}
// === InMemory implementation ===
class PenggunaRepositoryInMemory : Repository<Pengguna, Int> {
private val store = mutableMapOf<Int, Pengguna>()
override fun simpan(entity: Pengguna): Pengguna {
store[entity.id] = entity
return entity
}
override fun cari(id: Int): Pengguna? = store[id]
override fun semua(): List<Pengguna> = store.values.toList()
override fun hapus(id: Int): Boolean = store.remove(id) != null
}
// === Cache implementation (decorator over another repository) ===
class CachedPenggunaRepository(
private val inner: Repository<Pengguna, Int>,
private val cache: MutableMap<Int, Pengguna> = mutableMapOf()
) : Repository<Pengguna, Int> by inner {
override fun cari(id: Int): Pengguna? {
if (cache.containsKey(id)) {
println("[cache] hit untuk id=$id")
return cache[id]
}
println("[cache] miss untuk id=$id, delegasi ke inner")
val result = inner.cari(id)
if (result != null) cache[id] = result
return result
}
override fun simpan(entity: Pengguna): Pengguna {
cache[entity.id] = entity
return inner.simpan(entity)
}
override fun hapus(id: Int): Boolean {
cache.remove(id)
return inner.hapus(id)
}
}
// === Specification Pattern (companion to Repository) ===
interface Specification<T> {
fun dipenuhi(oleh: T): Boolean
}
class NamaMengandung(private val keyword: String) : Specification<Pengguna> {
override fun dipenuhi(oleh: Pengguna) = olele(oleh).contains(keyword, ignoreCase = true)
private fun olele(p: Pengguna) = p.nama
}
class EmailBerakhiranDomain(private val domain: String) : Specification<Pengguna> {
override fun dipenuhi(oleh: Pengguna) = oleh.email.endsWith(domain)
}
class CompositeSpec<T>(private val specs: List<Specification<T>>) : Specification<T> {
override fun dipenuhi(oleh: T) = specs.all { it.dipenuhi(oleh) }
}
// === API client adapter (alternative implementation) ===
class ApiPenggunaRepository(private val baseUrl: String) : Repository<Pengguna, Int> {
private val mock = mutableMapOf(
1 to Pengguna(1, "API User", "[email protected]", LocalDate.of(2026, 1, 15))
)
override fun simpan(entity: Pengguna): Pengguna {
println("[api] POST $baseUrl/pengguna ${entity.email}")
mock[entity.id] = entity
return entity
}
override fun cari(id: Int): Pengguna? {
println("[api] GET $baseUrl/pengguna/$id")
return mock[id]
}
override fun semua(): List<Pengguna> {
println("[api] GET $baseUrl/pengguna")
return mock.values.toList()
}
override fun hapus(id: Int): Boolean {
println("[api] DELETE $baseUrl/pengguna/$id")
return mock.remove(id) != null
}
}
fun main() {
println("=== Demo Repository Pattern ===")
println("--- 1. InMemory implementation ---")
val repo = PenggunaRepositoryInMemory()
val tanggal = LocalDate.of(2026, 1, 15)
repo.simpan(Pengguna(1, "Budi", "[email protected]", tanggal))
repo.simpan(Pengguna(2, "Siti", "[email protected]", tanggal))
println("Cari id=1: ${repo.cari(1)}")
println("Semua: ${repo.semua().size} pengguna")
println()
println("--- 2. Cached decorator ---")
val cachedRepo = CachedPenggunaRepository(PenggunaRepositoryInMemory().also {
it.simpan(Pengguna(1, "Budi", "[email protected]", tanggal))
})
println("Cari pertama (miss): ${cachedRepo.cari(1)?.nama}")
println("Cari kedua (hit): ${cachedRepo.cari(1)?.nama}")
println()
println("--- 3. Specification pattern ---")
val specNama = NamaMengandung("Budi")
val specDomain = EmailBerakhiranDomain("@example.com")
val specComposite = CompositeSpec(listOf(specNama, specDomain))
val p = Pengguna(1, "Budi", "[email protected]", tanggal)
println("Pengguna memenuhi spec nama? ${specNama.dipenuhi(p)}")
println("Pengguna memenuhi spec domain? ${specDomain.dipenuhi(p)}")
println("Pengguna memenuhi composite spec? ${specComposite.dipenuhi(p)}")
println()
println("--- 4. API implementation ---")
val apiRepo = ApiPenggunaRepository("https://api.example.com")
apiRepo.cari(1)
apiRepo.simpan(Pengguna(2, "Siti", "[email protected]", tanggal))
apiRepo.semua()
}Output yang Dihasilkan:
=== Demo Repository Pattern ===
--- 1. InMemory implementation ---
Cari id=1: Pengguna(id=1, nama=Budi, [email protected], daftar=2026-01-15)
Semua: 2 pengguna
--- 2. Cached decorator ---
[cache] miss untuk id=1, delegasi ke inner
Cari pertama (miss): Budi
[cache] hit untuk id=1
Cari kedua (hit): Budi
--- 3. Specification pattern ---
Pengguna memenuhi spec nama? true
Pengguna memenuhi spec domain? true
Pengguna memenuhi composite spec? true
--- 4. API implementation ---
[api] GET https://api.example.com/pengguna/1
[api] POST https://api.example.com/pengguna [email protected]
[api] GET https://api.example.com/penggunaBedah Kode Baris-per-Baris #
Mari kita amati bagian kunci:
- Baris 8-14 (
interface Repository<T, ID>): Generic interface dengan empat method dasar. TipeTadalah entity,IDadalah tipe identifier. Setiap aggregate root biasanya punya repository sendiri. - Baris 17-28 (
PenggunaRepositoryInMemory): Implementasi in-memory yang menyimpan data diMutableMap. Cocok untuk testing atau prototype. Bisa diganti keJdbcPenggunaRepositorytanpa mengubah kode pemanggil. - Baris 31-56 (
CachedPenggunaRepository): Decorator cache di atas repository lain.by innermendelegasikan method yang tidak di-override; cache hanya overridecari,simpan, danhapus. Pola ini memungkinkan cache ditambah tanpa mengubah repository inner. - Baris 59-73 (Specification): Pattern tambahan untuk query terstruktur.
Specification<T>adalah predikat yang dapat dikombinasikan viaCompositeSpec. Repository bisa menyediakan methodcari(spec: Specification<T>): List<T>untuk query dinamis. - Baris 76-97 (
ApiPenggunaRepository): Implementasi yang menyimulasikan akses REST API. Methodsimpan,cari,semua,hapusmencetak HTTP request yang akan dilakukan. Pada produksi, implementasi ini akan memakai Ktor Client atau OkHttp.
Repository vs DAO #
Repository dan Data Access Object (DAO) sering dianggap sama, tetapi ada perbedaan halus. Repository berorientasi aggregate root dan menyerupai collection; method-nya seperti simpan, cari, semua. DAO lebih dekat ke storage dan biasanya berorientasi tabel; method-nya bisa insert, update, findByEmail. Repository cocok untuk domain-driven design; DAO cocok untuk aplikasi CRUD yang langsung memetakan tabel.
Penutup #
Repository Pattern mengabstraksi akses data di belakang interface yang menyerupai collection. Generic interface, decorator cache, specification, dan adapter API adalah empat varian yang sering dipakai bersamaan. Pada chapter berikutnya kita akan membahas bagaimana memilih arsitektur aplikasi yang tepat untuk proyek dengan kompleksitas berbeda.
Referensi #
Referensi Lanjutan #
- Kotlin documentation home
— halaman utama dokumentasi Kotlin untuk
interfacesebagai kontrak repository yang dapat di-mock