Pelajari prinsip abstraksi data Barbara Liskov untuk merancang antarmuka yang stabil, mengurangi kerusakan, dan membangun sistem yang mudah dipelihara dengan API yang jelas dan andal.

Barbara Liskov adalah seorang ilmuwan komputer yang karyanya diam‑diam membentuk cara tim perangkat lunak modern membangun sesuatu yang tidak runtuh. Penelitiannya tentang abstraksi data, penyembunyian informasi, dan kemudian Prinsip Substitusi Liskov (LSP) memengaruhi segala hal mulai dari bahasa pemrograman hingga cara kita sehari‑hari memikirkan API: definisikan perilaku yang jelas, lindungi internals, dan buat aman bagi orang lain untuk bergantung pada antarmuka Anda.
API yang andal bukan sekadar “benar” secara teoretis. Itu adalah antarmuka yang membantu sebuah produk bergerak lebih cepat:
Keandalan itu adalah sebuah pengalaman: bagi pengembang yang memanggil API Anda, bagi tim yang memeliharanya, dan bagi pengguna yang bergantung pada API itu secara tidak langsung.
Abstraksi data adalah gagasan bahwa pemanggil harus berinteraksi dengan sebuah konsep (sebuah akun, antrean, langganan) melalui sekumpulan operasi kecil—bukan melalui rincian berantakan bagaimana data disimpan atau dihitung.
Saat Anda menyembunyikan detail representasi, Anda menghilangkan seluruh kategori kesalahan: tidak ada yang bisa “secara tidak sengaja” bergantung pada field database yang seharusnya bukan publik, atau memodifikasi state bersama dengan cara yang tidak bisa ditangani sistem. Yang tak kalah penting, abstraksi menurunkan overhead koordinasi: tim tidak perlu izin untuk merombak internals selama perilaku publik tetap konsisten.
Di akhir artikel ini, Anda akan memiliki cara praktis untuk:
Jika Anda ingin ringkasan cepat nanti, lompat ke /blog/a-practical-checklist-for-designing-reliable-apis.
Abstraksi data adalah ide sederhana: Anda berinteraksi dengan sesuatu berdasarkan apa yang dilakukannya, bukan bagaimana itu dibangun.
Bayangkan mesin penjual otomatis. Anda tidak perlu tahu bagaimana motor berputar atau bagaimana koin dihitung. Anda hanya butuh kontrol (“pilih produk”, “bayar”, “terima produk”) dan aturan (“jika Anda membayar cukup, Anda mendapat produk; jika stok habis, uang dikembalikan”). Itu abstraksi.
Dalam perangkat lunak, antarmuka adalah “apa yang dilakukan”: nama operasi, input yang diterima, output yang dihasilkan, dan error yang diharapkan. Implementasi adalah “bagaimana kerjanya”: tabel database, strategi caching, kelas internal, dan trik performa.
Memisahkan keduanya adalah cara mendapatkan API yang tetap stabil walau sistem berkembang. Anda bisa menulis ulang internals, mengganti pustaka, atau mengoptimalkan penyimpanan—sementara antarmuka tetap sama bagi pengguna.
Sebuah tipe data abstrak adalah “wadah + operasi yang diizinkan + aturan,” dijelaskan tanpa berkomitmen pada struktur internal tertentu.
Contoh: sebuah Stack (last in, first out).
push(item): menambah itempop(): menghapus dan mengembalikan item yang terakhir ditambahkanpeek(): melihat item teratas tanpa menghapusnyaIntinya adalah janji: pop() mengembalikan push() yang terakhir. Apakah stack memakai array, linked list, atau sesuatu yang lain adalah privat.
Pemisahan yang sama berlaku di mana‑mana:
POST /payments adalah antarmuka; pemeriksaan fraud, retry, dan penulisan database adalah implementasi.client.upload(file) adalah antarmuka; chunking, kompresi, dan permintaan paralel adalah implementasi.Saat Anda mendesain dengan abstraksi, Anda fokus pada kontrak yang diandalkan pengguna—dan memberi diri Anda kebebasan untuk mengubah segalanya di balik tirai tanpa memecahkan mereka.
Sebuah invariant adalah aturan yang harus selalu benar di dalam sebuah abstraksi. Jika Anda merancang API, invariant adalah pagar pengaman yang menjaga data dari bergeser ke keadaan yang tidak mungkin—mis. akun bank dengan dua mata uang sekaligus, atau pesanan “selesai” tanpa item.
Anggap invariant sebagai “bentuk realitas” untuk tipe Anda:
Cart tidak boleh berisi kuantitas negatif.UserEmail selalu merupakan email valid (bukan “divalidasi nanti”).Reservation punya start < end, dan kedua waktu berada dalam zona waktu yang sama.Jika pernyataan‑pernyataan itu tidak lagi benar, sistem Anda menjadi tak dapat diprediksi, karena setiap fitur sekarang harus menebak apa arti data “rusak”.
API yang baik menegakkan invariant di batas:
Ini meningkatkan penanganan error: alih‑alih kegagalan samar di kemudian hari (“ada yang salah”), API bisa menjelaskan aturan mana yang dilanggar (“end harus setelah start”).
Pemanggil tidak harus menghafal aturan internal seperti “metode ini hanya bekerja setelah memanggil normalize().” Jika invariant bergantung pada ritual khusus, itu bukan invariant—itu jebakan.
Rancang antarmuka sehingga:
Saat mendokumentasikan sebuah tipe API, tuliskan:
API yang baik bukan sekadar sekumpulan fungsi—itu adalah sebuah janji. Kontrak membuat janji itu eksplisit, sehingga pemanggil bisa bergantung pada perilaku dan pemelihara bisa mengubah internals tanpa mengejutkan siapa pun.
Minimal, dokumentasikan:
Kejelasan ini membuat perilaku dapat diprediksi: pemanggil tahu input apa yang aman dan hasil apa yang harus ditangani, dan tes dapat memeriksa janji itu daripada menebak maksud.
Tanpa kontrak, tim bergantung pada memori dan norma informal: “Jangan kirim null di situ,” “Panggilan itu kadang‑kadang retry,” “Ini mengembalikan kosong saat error.” Aturan‑aturan itu hilang saat onboarding, refaktor, atau insiden.
Kontrak tertulis mengubah aturan tersembunyi itu menjadi pengetahuan bersama. Ia juga menciptakan target stabil untuk review kode: diskusi menjadi “Apakah perubahan ini masih memenuhi kontrak?” daripada “Ini bekerja di mesin saya.”
Samar: “Membuat user.”
Lebih baik: “Membuat user dengan email unik.
email harus alamat valid; pemanggil harus memiliki izin users:create.userId baru; user dipersist dan langsung dapat diambil.409 jika email sudah ada; 400 untuk field tidak valid; tidak ada user parsial yang dibuat.”Samar: “Mengambil item dengan cepat.”
Lebih baik: “Mengembalikan hingga limit item diurutkan menurut createdAt descending.
nextCursor untuk halaman berikut; cursor kedaluwarsa setelah 15 menit.”Penyembunyian informasi adalah sisi praktis dari abstraksi data: pemanggil harus bergantung pada apa yang dilakukan API, bukan bagaimana caranya. Jika pengguna tidak bisa melihat internals Anda, Anda bisa mengubahnya tanpa membuat setiap rilis menjadi perubahan yang memecah.
Antarmuka yang baik mempublikasikan sekumpulan operasi kecil (create, fetch, update, list, validate) dan menyembunyikan representasi—tabel, cache, antrean, layout file, batasan layanan—sebagai privat.
Misalnya, “tambahkan item ke cart” adalah operasi. “CartRowId” dari database Anda adalah detail implementasi. Saat Anda mengekspos detail itu, Anda mengundang pengguna untuk membangun logika mereka sendiri di atasnya, yang membekukan kemampuan Anda untuk berubah.
Saat klien hanya bergantung pada perilaku stabil, Anda bisa:
...dan API tetap kompatibel karena kontrak tidak bergeser. Itu manfaat nyata: stabilitas untuk pengguna, kebebasan untuk pemelihara.
Beberapa cara internals secara tak sengaja bocor:
status=3 bukannya nama jelas atau operasi khusus.Lebih suka respons yang menggambarkan makna, bukan mekanik:
"userId": "usr_…") ketimbang nomor baris database.Jika sebuah detail mungkin berubah, jangan publikasikan. Jika pengguna membutuhkannya, promosikan menjadi bagian antarmuka yang disengaja dan terdokumentasi.
Prinsip Substitusi Liskov (LSP) dalam satu kalimat: jika sebuah kode bekerja dengan sebuah antarmuka, itu harus tetap bekerja saat Anda mengganti dengan implementasi valid lain dari antarmuka itu—tanpa perlu kasus khusus.
LSP kurang soal inheritance dan lebih soal kepercayaan. Saat Anda mempublikasikan antarmuka, Anda membuat janji tentang perilaku. LSP mengatakan setiap implementasi harus menjaga janji itu, meski menggunakan pendekatan internal yang berbeda.
Pemanggil bergantung pada apa yang API katakan—bukan pada apa yang terjadi hari ini. Jika sebuah antarmuka mengatakan “Anda bisa memanggil save() dengan record valid apapun,” maka setiap implementasi harus menerima record tersebut. Jika antarmuka mengatakan “get() mengembalikan nilai atau hasil ‘not found’ yang jelas,” maka implementasi tidak boleh tiba‑tiba melempar error baru atau mengembalikan data parsial.
Ekstensi yang aman berarti Anda bisa menambahkan implementasi baru (atau mengganti provider) tanpa memaksa pengguna menulis ulang kode. Itu imbalan praktis LSP: menjaga antarmuka bisa dipertukarkan.
Dua cara umum API memecah janji:
Input lebih sempit (prekondisi lebih ketat): implementasi baru menolak input yang antarmuka izinkan. Contoh: antarmuka menerima string UTF‑8 sebagai ID, tapi satu implementasi hanya menerima ID numerik.
Output lebih lemah (postkondisi longgar): implementasi baru mengembalikan lebih sedikit daripada yang dijanjikan. Contoh: antarmuka mengatakan hasil terurut, unik, lengkap—tetapi satu implementasi mengembalikan data tak terurut, duplikat, atau menghapus item tanpa pemberitahuan.
Pelanggaran lain yang halus adalah mengubah perilaku kegagalan: satu implementasi mengembalikan “not found” sementara yang lain melempar exception untuk situasi yang sama—pemanggil jadi tak bisa mengganti implementasi dengan aman.
Untuk mendukung “plug‑in” (banyak implementasi), tulis antarmuka seperti kontrak:
Jika sebuah implementasi benar‑benar membutuhkan aturan lebih ketat, jangan sembunyikan itu di balik antarmuka yang sama. Atau (1) definisikan antarmuka terpisah, atau (2) buat constraint itu eksplisit sebagai capability (mis. supportsNumericIds()), sehingga klien ikut serta dengan sadar—bukannya terkejut oleh “substitute” yang sebenarnya tidak bisa disubstitusi.
Antarmuka yang dirancang baik terasa “jelas” dipakai karena hanya mengekspos apa yang pemanggil butuhkan—dan tidak lebih. Pandangan Liskov tentang abstraksi data mendorong Anda ke arah antarmuka yang sempit, stabil, dan mudah dibaca, sehingga pengguna bisa bergantung padanya tanpa mempelajari detail internal.
API besar cenderung mencampur tanggung jawab tak terkait: konfigurasi, perubahan state, pelaporan, dan troubleshooting dalam satu tempat. Itu membuat sulit memahami apa yang aman dipanggil dan kapan.
Antarmuka kohesif mengelompokkan operasi yang milik abstraksi sama. Jika API Anda merepresentasikan antrean, fokuslah pada perilaku antrean (enqueue/dequeue/peek/size), bukan utilitas umum. Lebih sedikit konsep berarti lebih sedikit jalan untuk salah pakai.
“Fleksibel” sering berarti “tidak jelas.” Parameter seperti options: any, mode: string, atau banyak boolean (mis. force, skipCache, silent) menciptakan kombinasi yang tak terdefinisi dengan baik.
Lebih baik:
publish() vs publishDraft()), atauJika parameter mengharuskan pemanggil membaca sumber agar tahu apa yang terjadi, itu bukan bagian dari abstraksi yang baik.
Nama menyampaikan kontrak. Pilih kata kerja yang menjelaskan perilaku yang teramati: reserve, release, validate, list, get. Hindari metafora cerdas dan istilah yang tumpang tindih. Jika dua metode terdengar mirip, pemanggil akan mengasumsikan perilakunya serupa—jadi pastikan memang begitu.
Pisahkan API saat Anda melihat:
Modul terpisah memungkinkan Anda mengembangkan internals sambil menjaga janji inti tetap teguh. Jika Anda merencanakan pertumbuhan, pertimbangkan paket “inti” ramping plus add‑on; lihat juga /blog/evolving-apis-without-breaking-users.
API jarang tetap diam. Fitur baru muncul, kasus tepi ditemukan, dan “penyempurnaan kecil” bisa diam‑diam memecah aplikasi nyata. Tujuannya bukan membekukan antarmuka—melainkan mengembangkannya tanpa melanggar janji yang sudah diandalkan pengguna.
Versioning semantik adalah alat komunikasi:
Batasnya: Anda tetap membutuhkan penilaian. Jika sebuah “perbaikan bug” mengubah perilaku yang diandalkan pemanggil, itu praktis memecah—meski perilaku lama sebenarnya sebuah kecelakaan.
Banyak perubahan yang memecah tidak terlihat di compiler:
Pikirkan dalam istilah prekondisi dan postkondisi: apa yang harus disediakan pemanggil, dan apa yang bisa mereka harapkan kembali.
Deprecation berhasil bila eksplisit dan berbatas waktu:
Abstraksi gaya Liskov membantu karena mempersempit apa yang bisa diandalkan pengguna. Jika pemanggil hanya bergantung pada kontrak antarmuka—bukan struktur internal—Anda bisa mengubah format penyimpanan, algoritme, dan optimisasi dengan bebas.
Dalam praktiknya, tooling yang kuat membantu. Misalnya, jika Anda iterasi cepat pada API internal sambil membangun app React atau backend Go + PostgreSQL, workflow cepat seperti Koder.ai dapat mempercepat implementasi tanpa mengubah disiplin inti: Anda tetap menginginkan kontrak yang jelas, identifier stabil, dan evolusi kompatibel mundur. Kecepatan adalah pengali—jadi baiknya menggandakan kebiasaan antarmuka yang tepat.
API yang andal bukan yang tak pernah gagal—melainkan yang gagal dengan cara yang bisa dipahami, ditangani, dan diuji oleh pemanggil. Penanganan error adalah bagian dari abstraksi: ia mendefinisikan apa arti “penggunaan yang benar”, dan apa yang terjadi ketika dunia (jaringan, disk, izin, waktu) tidak bersepakat.
Mulailah dengan memisahkan dua kategori:
Pembedaan ini membuat antarmuka jujur: pemanggil tahu apa yang bisa mereka perbaiki di kode vs apa yang harus mereka tangani saat runtime.
Kontrak Anda harus mengisyaratkan mekanisme:
Ok | Error) saat kegagalan diharapkan dan Anda ingin pemanggil menanganinya eksplisit.Apa pun yang Anda pilih, konsisten di seluruh API agar pengguna tidak menebak.
Daftar kegagalan kemungkinan per operasi dalam istilah makna, bukan detail implementasi: “conflict karena versi kadaluwarsa”, “not found”, “permission denied”, “rate limited.” Sediakan kode error stabil dan field terstruktur sehingga tes bisa menegaskan perilaku tanpa mencocokkan string.
Dokumentasikan apakah operasi aman untuk di‑retry, dalam kondisi apa, dan bagaimana mencapai idempoten (kunci idempoten, ID permintaan alami). Jika sukses parsial mungkin terjadi (operasi batch), definisikan bagaimana keberhasilan dan kegagalan dilaporkan, dan state apa yang harus diasumsikan pemanggil setelah timeout.
Abstraksi adalah sebuah janji: “Jika Anda memanggil operasi ini dengan input valid, Anda akan mendapatkan hasil ini, dan aturan ini akan selalu terpenuhi.” Pengujian adalah cara menjaga janji itu tetap jujur saat kode berubah.
Mulailah dengan menerjemahkan kontrak menjadi pengecekan otomatis.
Unit test harus memverifikasi postkondisi setiap operasi dan kasus tepi: nilai kembali, perubahan state, dan perilaku error. Jika antarmuka Anda mengatakan “menghapus item yang tidak ada mengembalikan false dan tidak mengubah apa pun,” tulis tes persis itu.
Integration test harus memvalidasi kontrak melintasi batas nyata: database, jaringan, serialisasi, dan auth. Banyak “pelanggaran kontrak” muncul hanya saat tipe dikodekan/didekodekan atau saat retry/timeout terjadi.
Invariant adalah aturan yang harus tetap benar di seluruh urutan operasi valid (mis. “saldo tidak pernah negatif”, “ID unik”, “item yang dikembalikan oleh list() bisa di‑get lewat get(id)).
Property‑based testing memeriksa aturan ini dengan menghasilkan banyak input dan urutan operasi random‑tapi‑valid, mencari contoh kontra. Secara konseptual, Anda berkata: “Tidak peduli urutan pemanggilan, invariant harus terpenuhi.” Ini sangat baik untuk menemukan kasus sudut aneh yang manusia sering lewatkan.
Untuk API publik atau berbagi, biarkan konsumen mempublikasikan contoh permintaan yang mereka buat dan respons yang mereka andalkan. Provider kemudian menjalankan kontrak ini di CI untuk memastikan perubahan tidak memecahkan penggunaan nyata—bahkan ketika tim provider tidak mengantisipasi penggunaan itu.
Tes tidak bisa menutupi semua hal, jadi pantau sinyal yang menunjukkan kontrak berubah: perubahan bentuk respons, kenaikan tingkat 4xx/5xx, kode error baru, lonjakan latensi, dan kegagalan deserialisasi. Lacak ini per endpoint dan versi sehingga Anda bisa mendeteksi drift lebih awal dan rollback dengan aman.
Jika Anda mendukung snapshot atau rollback dalam pipeline delivery, itu berpadu alami dengan pola ini: deteksi drift cepat, lalu kembalikan tanpa memaksa klien menyesuaikan di tengah insiden. (Koder.ai, misalnya, menyertakan snapshot dan rollback sebagai bagian workflow‑nya, yang sejalan dengan pendekatan “kontrak dulu, perubahan kemudian”.)
Bahkan tim yang menghargai abstraksi bisa masuk pola yang terasa “praktis” saat itu juga tetapi lama‑lama mengubah API menjadi kumpulan kasus khusus. Berikut beberapa jebakan berulang—dan apa yang harus dilakukan sebagai gantinya.
Feature flag bagus untuk rollout, tapi bermasalah ketika flag jadi parameter publik jangka panjang: ?useNewPricing=true, mode=legacy, v2=true. Lama‑lama pemanggil menggabungkannya secara tak terduga, dan Anda mendukung banyak perilaku selamanya.
Pendekatan lebih aman:
API yang mengekspos table ID, join key, atau filter “bertumpu SQL” memaksa klien mempelajari model penyimpanan Anda. Itu membuat refaktor menyakitkan: perubahan skema menjadi perubahan API yang memecah.
Modelkan antarmuka berdasarkan konsep domain dan identifier stabil. Biarkan klien meminta apa yang mereka maksud (“orders untuk customer dalam rentang tanggal”), bukan bagaimana Anda menyimpannya.
Menambah field terlihat tak berbahaya, tapi perubahan “satu field lagi” yang berulang bisa mengaburkan tanggung jawab dan melemahkan invariant. Klien mulai bergantung pada detail kebetulan, dan tipe menjadi kantong serba guna.
Hindari biaya jangka panjang dengan:
Over‑abstraksi bisa menghalangi kebutuhan nyata—mis. pagination yang tak bisa menyatakan “mulai setelah cursor ini”, atau endpoint pencarian yang tak mendukung “exact match.” Klien lalu mencari jalan memutar (panggilan multiple, filter lokal), menyebabkan performa dan error lebih buruk.
Solusinya adalah fleksibilitas terkontrol: sediakan sedikit titik ekstensi yang terdefinisi baik (mis. operator filter yang didukung), bukan celah lebar tanpa batas.
Penyederhanaan tak harus mengurangi kekuatan. Deprecate opsi yang membingungkan, tapi tetap sediakan kapabilitas lewat bentuk yang lebih jelas: ganti banyak parameter tumpang tindih dengan satu objek permintaan terstruktur, atau pecah satu endpoint “lakukan semua” menjadi dua endpoint kohesif. Lalu pandu migrasi dengan docs ber‑versi dan timeline deprecate (lihat /blog/evolving-apis-without-breaking-users).
Anda bisa menerapkan ide abstraksi data ala Liskov dengan daftar periksa sederhana dan bisa diulang. Tujuannya bukan kesempurnaan—melainkan membuat janji API eksplisit, bisa dites, dan aman untuk berkembang.
Gunakan blok singkat dan konsisten:
transfer(from, to, amount)amount > 0 dan akun adaInsufficientFunds, AccountNotFound, TimeoutJika ingin lebih dalam, cari: Abstract Data Types (ADT), Design by Contract, dan Prinsip Substitusi Liskov (LSP).
Jika tim Anda menyimpan catatan internal, tautkan dari halaman seperti /docs/api-guidelines agar alur review mudah digunakan kembali—dan bila Anda membangun layanan baru dengan cepat (apakah secara manual atau dengan builder berbasis chat seperti Koder.ai), perlakukan pedoman itu sebagai bagian yang tidak bisa dinegosiasikan dari “shipping fast.” Antarmuka yang andal adalah bagaimana kecepatan berlipat ganda alih‑alih berbalik melawan Anda.
Dia memopulerkan abstraksi data dan penyembunyian informasi, yang langsung berhubungan dengan desain API modern: publikasikan kontrak kecil dan stabil, dan biarkan implementasi tetap fleksibel. Imbasnya praktis: lebih sedikit perubahan yang memecah, refaktor lebih aman, dan integrasi lebih dapat diprediksi.
Sebuah API yang andal adalah yang bisa diandalkan pemanggilnya sepanjang waktu:
Keandalan kurang soal “tidak pernah gagal” dan lebih soal gagal dengan cara yang dapat diprediksi dan tetap menghormati kontrak.
Tulis perilaku sebagai kontrak:
Sertakan kasus tepi (hasil kosong, duplikat, pengurutan) agar pemanggil dapat mengimplementasikan dan menguji terhadap janji itu.
Invariant adalah aturan yang selalu harus berlaku di dalam sebuah abstraksi (mis. “kuantitas tidak pernah negatif”). Terapkan invariant di batas:
normalize() dulu” yang harus diingat caller.Ini mengurangi bug hulu karena sistem lain tidak perlu menangani keadaan yang mustahil.
Penyembunyian informasi berarti mengekspos operasi dan makna, bukan representasi internal. Hindari mengikat konsumen ke hal yang mungkin berubah nanti (tabel, cache, shard key, status internal).
Taktik praktis:
usr_...) daripada ID baris database.Karena mereka membekukan implementasi Anda. Jika klien bergantung pada filter berbentuk tabel, join key, atau ID internal, refaktor skema menjadi perubahan API yang memecah.
Lebih baik ajukan pertanyaan domain daripada pertanyaan penyimpanan, mis. “pesanan untuk pelanggan dalam rentang tanggal”, dan jaga model penyimpanan tetap privat di balik kontrak.
LSP berarti: jika kode bekerja dengan sebuah interface, ia harus terus bekerja dengan implementasi apapun dari interface itu tanpa kasus khusus. Dalam istilah API, ini aturan “jangan mengejutkan pemanggil”.
Untuk mendukung implementasi yang bisa dipertukarkan, standarkan:
Perhatikan:
Jika sebuah implementasi membutuhkan batasan ekstra, publikasikan interface terpisah atau capability eksplisit agar pemanggil memilih secara sadar.
Jaga antarmuka tetap kecil dan kohesif:
options: any dan tumpukan boolean yang menciptakan kombinasi ambigu.Rancang error sebagai bagian dari kontrak:
Konsistensi lebih penting daripada mekanisme pasti (exceptions vs result types) selama pemanggil dapat memprediksi dan menangani hasil.
status=3reserve, release, list, validate).Jika ada peran pengguna atau laju perubahan berbeda, pisahkan modul/sumber daya agar masing‑masing bisa berkembang sesuai kebutuhan.