Barbara Liskov’un veri soyutlaması ilkelerini öğrenin: kararlı arayüzler tasarlayın, kırılmaları azaltın ve açık, güvenilir API'lerle sürdürülebilir sistemler kurun.

Barbara Liskov, modern yazılım ekiplerinin "dağılıp gitmeyen" şeyler inşa etme şeklini sessizce şekillendiren bir bilgisayar bilimcisidir. Onun veri soyutlaması, bilgi gizleme ve daha sonra ortaya çıkan Liskov Yerine Geçme İlkesi (LSP) üzerine çalışmaları, programlama dillerinden API'leri düşünme biçimimize kadar her şeyi etkiledi: net davranış tanımlayın, iç yapıyı koruyun ve başkalarının arayüzünüze güvenmesini güvenli kılın.
Güvenilir bir API sadece teorik olarak "doğru" değildir. Ürünün daha hızlı ilerlemesine yardımcı olan bir arayüzdür:
Bu güvenilirlik bir deneyimdir: API'yi çağıran geliştirici, onu sürdüren ekip ve dolaylı olarak ona bağımlı kullanıcılar için.
Veri soyutlaması, çağıranların bir konsept (hesap, kuyruk, abonelik) ile nasıl saklandığı veya hesaplandığı yerine küçük bir operasyon seti aracılığıyla etkileşmesi gerektiği fikridir.
Temsili ayrıntıları gizlediğinizde, belirli hata kategorilerini ortadan kaldırırsınız: kimse "kamuya açık olmayan" bir veritabanı alanına kazara güvenemez veya sistemi kaldıramayacak şekilde paylaşılan durumu mutasyona uğratamaz. Aynı derecede önemli olarak, soyutlama koordinasyon yükünü düşürür: halka açık davranış tutarlı kaldığı sürece ekiplerin iç yapıyı refactor etmesi için izin gerekmez.
Sonunda pratik yolları bileceksiniz:
Hızlı bir özet isterseniz, daha sonra /blog/a-practical-checklist-for-designing-reliable-apis bölümüne bakabilirsiniz.
Veri soyutlaması basit bir fikirdir: bir şeyle nasıl yapıldığıyla değil, ne yaptığıyla etkileşirsiniz.
Bir otomatı düşünün. Motorların nasıl döndüğünü veya paraların nasıl sayıldığını bilmenize gerek yok. Sadece kontrolleri bilirsiniz ("ürün seç", "öde", "ürünü al") ve kuralları ("yeterince öderseniz ürünü alırsınız; tükenmişse iade edilir"). İşte soyutlama budur.
Yazılımda arayüz "ne yaptığı"dır: operasyon isimleri, kabul ettiği girdiler, döndürdüğü çıktılar ve beklenen hatalar. Uygulama ise "nasıl çalıştığı"dır: veritabanı tabloları, caching stratejisi, iç sınıflar ve performans hileleri.
Bunları ayrı tutmak, sistem evrilirken arayüzlerin stabil kalmasını sağlar. İç yapıyı yeniden yazabilir, kütüphane değiştirebilir veya depolamayı optimize edebilirsiniz—kullanıcılar için arayüz aynı kalır.
Bir soyut veri tipi, belirli bir iç yapıya bağlı kalmadan tanımlanmış "kapsayıcı + izin verilen işlemler + kurallar"dır.
Örnek: Stack (son giren, ilk çıkar).
Önemli olan vaat: pop() en son push() edilen öğeyi döndürür. Stack'in arkasında dizi mi, bağlı liste mi olduğu gizlidir.
Aynı ayrım her yerde uygulanır:
POST /payments arayüzdür; fraud kontrolü, retry'ler ve veritabanı yazımları implementasyon.client.upload(file) arayüzdür; chunking, sıkıştırma ve paralel istekler implementasyon.Soyutlama ile tasarladığınızda, kullanıcıların güvendiği sözleşmeye odaklanırsınız—ve perde arkasını değiştirme özgürlüğünü kazanırsınız.
Invariant, bir soyutlama içinde her zaman doğru olması gereken kuraldır. Bir API tasarlıyorsanız, invariantlar verinizin imkansız durumlara kaymasını engelleyen koruyuculardır—örneğin bir bankanın iki para birimine aynı anda sahip olması veya "tamamlanmış" bir siparişin öğe içermemesi gibi.
Invariantı tipin “gerçekliği” olarak düşünün:
Cart negatif miktar içeremez.UserEmail her zaman geçerli bir e-posta adresidir ("daha sonra doğrulanacak" değil).Reservation için start < end ve her iki zaman da aynı zaman diliminde olmalıdır.Bu ifadeler doğru olmaktan çıktığında, sistem tahmin edilemez hale gelir çünkü her özellik artık "bozuk" verinin ne anlama geldiğini tahmin etmek zorunda kalır.
İyi API'ler invariantları sınırda uygular:
Bu, belirsiz hatalar yerine API'nin hangi kuralın ihlal edildiğini açıklamasını sağlar ("başlangıç, bitişten sonra olmalı" gibi).
Çağıranlar "önce normalize() çağrısı yapın" gibi iç kuralları ezberlememeli. Eğer bir invariant özel bir ritüle bağlıysa, o bir invariant değil—bir hata silahıdır.
Arayüzü şöyle tasarlayın:
Bir API tipi belgelendiğinde şunları yazın:
İyi bir API sadece fonksiyonlar seti değildir—o bir vaattir. Sözleşmeler bu vaadi açık hale getirir; böylece çağıranlar davranışa güvenebilir ve bakım yapanlar iç yapıyı değiştirse bile kimse şaşırmaz.
En azından şunları dokümante edin:
Bu netlik davranışı öngörülebilir kılar: çağıranlar hangi girdilerin güvenli olduğunu ve hangi sonuçları ele almaları gerektiğini bilir; testler de niyeti tahmin etmek yerine vaadi kontrol eder.
Sözleşme yoksa ekipler hafızaya ve gayri resmi normlara güvenir: "Oraya null geçirme", "O çağrı bazen retry yapıyor", "Hata olursa boş döner" gibi. Bu kurallar onboarding, refactor veya olaylar sırasında kaybolur.
Yazılı bir sözleşme bu gizli kuralları paylaşılan bilgi haline getirir. Ayrıca kod incelemelerinde stabil bir hedef sağlar: tartışma "Bu değişiklik hâlâ sözleşmeyi sağlıyor mu?" haline gelir.
Belirsiz: "Kullanıcı oluşturur."
Daha iyi: "Eşsiz bir e-postayla kullanıcı oluşturur.\n\n- Önkoşullar: email geçerli olmalı; çağıranın users:create izni olmalı.\n- Sonkoşullar: yeni userId döner; kullanıcı kalıcı hale gelir ve hemen alınabilir.\n- Hata modları: e-posta zaten varsa 409; alanlar geçersizse 400; kısmi kullanıcı oluşturulmaz."
Belirsiz: "Öğeleri hızlıca alır."
Daha iyi: "createdAt azalan biçimde sıralanmış, limit kadar öğe döndürür.\n\n- Yan etkiler: yok.\n- Tutarlılık: en fazla 60 saniye kadar eski olabilir.\n- Sayfalama: sonraki sayfa için nextCursor kullanın; cursor'lar 15 dakikada dolar."
Bilgi gizleme veri soyutlamasının pratik tarafıdır: çağıranlar API'nin ne yaptığına güvenmeli, nasıl yaptığına değil. İç yapı görünmezse, her sürüm bir kırılma haline gelmez.
İyi bir arayüz küçük bir operasyon seti (create, fetch, update, list, validate) yayımlar ve temsili—tablolar, cache'ler, kuyruklar, dosya düzenleri—gizli tutar.
Mesela “sepete öğe ekle” bir işlemdir. Veritabanı CartRowId'si iç bir detaytır. Detayı açığa çıkardığınızda kullanıcılar kendi mantıklarını buna göre kurar ve bu da değişiklik yapmanızı zorlaştırır.
İstemciler sadece stabil davranışa bağlıysa şunları yapabilirsiniz:
…ve API uyumlu kalır çünkü sözleşme yerinde durur. Gerçek kazanç budur: kullanıcılar için stabilite, bakım yapanlar için özgürlük.
İç yapı istemeden dışarı sızabilir:
status=3 gibi.Anlamı, mekanikleri değil, tarif eden cevapları tercih edin:
\"userId\": \"usr_…\").Bir detay değişebilir görünüyorsa yayımlamayın. Kullanıcılar buna ihtiyaç duyuyorsa, onu kasıtlı ve belgelenmiş bir parça yapın.
LSP bir cümlede: Bir kod bir arayüzle çalışıyorsa, o arayüzün herhangi geçerli bir implementasyonu yerine konulduğunda özel durumlar gerektirmeksizin çalışmaya devam etmelidir.
LSP miras ile ilgili olmaktan çok güven ile ilgilidir. Bir arayüz yayımladığınızda davranış hakkında bir vaat verirsiniz. LSP der ki her implementasyon bu vaadi tutmalı, çok farklı iç yaklaşımlar kullansa bile.
Çağıranlar API'nin ne dediğine güvenir—bugün ne yaptığına değil. Arayüz "geçerli herhangi bir kayıtla save() çağırabilirsiniz" diyorsa, her implementasyon o kayıtları kabul etmelidir. Arayüz "get() değer döndürür veya net bir 'bulunamadı' sonucu verir" diyorsa, implementasyonlar rastgele yeni hatalar fırlatmamalıdır.
Güvenli genişletme, yeni implementasyonlar eklediğinizde veya sağlayıcı değiştirdiğinizde kullanıcıların kodunu yeniden yazmasını gerektirmez. Bu LSP'nin pratik faydasıdır: arayüzler değiştirilip yerine konabilir kalır.
İki yaygın yol vardır:
Üçüncü, ince bir ihlal de hata davranışını değiştirmektir: bir implementasyon "bulunamadı" dönerken diğeri aynı durumda istisna fırlatıyorsa, çağıranlar bunların yerine geçmesini güvenle yapamaz.
"Plug-in" desteklemek için arayüzü bir sözleşme gibi yazın:
Eğer bir implementasyon gerçekten daha katı kurallar gerektiriyorsa, aynı arayüzün arkasına saklamayın: ayrı bir arayüz tanımlayın veya supportsNumericIds() gibi açık bir yetenekle bunu belgeleyin; böylece istemciler bilerek tercih eder.
İyi tasarlanmış bir arayüz, çağıranın sadece ihtiyacı olanı (ve fazlasını değil) sunar; bunun sonucu aracı kullanmak “açık” hissidir. Liskov’un veri soyutlaması görüşü sizi dar, stabil ve okunaklı arayüzlere yönlendirir, böylece kullanıcılar iç ayrıntıları öğrenmeden güvenebilir.
Büyük API'ler genellikle ilgisiz sorumlulukları karıştırır: konfigürasyon, durum değişiklikleri, raporlama ve hata ayıklama tek bir yerde toplanır. Bu ne zaman güvenli çağrı yapıldığını anlamayı zorlaştırır.
Uyumlu bir arayüz aynı soyutlamaya ait davranışları gruplayarak sadece kuyruk davranışlarına (enqueue/dequeue/peek/size) odaklanır. Daha az kavram, yanlış kullanım yollarını azaltır.
"Esneklik" genellikle "belirsiz" demektir. options: any, mode: string veya çok sayıda boolean (force, skipCache, silent) belirsiz kombinasyonlar yaratır.
Tercih edin:
publish() vs publishDraft()), veyaEğer bir parametre için kaynak kodu okumak gerekiyorsa, o parametre iyi bir soyutlamanın parçası değildir.
İsimler sözleşmeyi iletir. Gözlemlenebilir davranışı tarif eden fiiller seçin: reserve, release, validate, list, get. Mekaforik ve aşırı zekice isimlerden kaçının. İki metod benzer geliyorsa kullanıcılar benzer davranışı varsayar—o yüzden bunu doğru kılın.
Şu durumlarda ayırın:
Ayrı modüller iç yapıyı evrilirken temel vaadi korumanıza izin verir. Büyümeyi planlıyorsanız, ince bir “core” paketi ve eklentiler yaklaşımını düşünün.
API'ler nadiren sabit kalır. Yeni özellikler gelir, uç durumlar keşfedilir ve "küçük iyileştirmeler" gerçek uygulamaları sessizce kırabilir. Amaç arayüzü dondurmak değil—kullanıcıların zaten güvendiği vaatleri ihlal etmeden evrimleştirmektir.
SemVer bir iletişim aracıdır:
Sınırı: hâlâ yargı gerekir. Bir "bug fix" kullanıcıların dayandığı davranışı değiştiriyorsa pratikte kırıcıdır—eskiden beklenen yanlış davranışa güvenmiş olabilirler.
Birçok kırıcı değişiklik derleyicide görünmez:
Bunun yerine önkoşullar ve sonkoşullar bağlamında düşünün: çağıranların ne sağlaması gerektiği ve neler alabilecekleri.
Deprecation açık ve zamanlı olduğunda işe yarar:
Liskov tarzı veri soyutlaması, kullanıcıların neye güvenebileceğini daralttığı için evrimi kolaylaştırır. Çağıranlar sadece arayüz sözleşmesine bağlıysa, depolama formatını, algoritmaları ve optimizasyonları özgürce değiştirebilirsiniz.
Uygulamada güçlü araçlar da yardımcı olur. Örneğin bir React uygulaması veya Go + PostgreSQL backend üzerinde hızlı iterasyon yapıyorsanız, Koder.ai gibi chat odaklı bir iş akışı uygulamayı hızlandırabilir; ama temel disiplin değişmez: net sözleşmeler, stabil kimlikler ve geriye dönük uyumlu evrim isteğe bağlıdır.
Güvenilir bir API hiç başarısız olmayan değil—çağıranların anlayabileceği, ele alabileceği ve test edebileceği şekillerde başarısız olan bir API'dir. Hata yönetimi soyutlamanın bir parçasıdır: "doğru kullanım" ne anlama gelir ve dünya (ağlar, diskler, izinler, zaman) karşı çıktığında ne olur.
İlk olarak iki kategori ayırın:
Bu ayrım arayüzünüzü dürüst kılar: çağıranlar kodda neyi düzeltebileceklerini ve neyi çalışma zamanında ele almaları gerektiğini öğrenir.
Sözleşmeniz mekanizmayı önerir:
Ok | Error) beklenen hatalar için ve çağıranın açık ele almasını istediğiniz durumlarda.Ne seçerseniz seçin, API genelinde tutarlı olun ki kullanıcılar tahmin etmesin.
Operasyon başına olası hataları anlam bağlamında listeleyin: "versiyon eski olduğu için çakışma", "bulunamadı", "izin reddedildi", "rate limited" gibi. Stabil hata kodları ve yapılandırılmış alanlar sağlayın ki testler string eşlemeye bağımlı olmasın.
Bir işlemin yeniden denenebilir olup olmadığını, hangi koşullarda olduğunu ve idempotensi nasıl sağlanacağını (idempotency key'leri, doğal istek ID'leri) belgeleyin. Kısmi başarı mümkünse (batch işlemler), başarılar ve hatalar nasıl raporlanır ve zaman aşımından sonra istemcinin hangi durumda olduğunu varsayması gerektiği tanımlanmalı.
Bir soyutlama bir vaattir: "Bu işlemleri geçerli girdilerle çağırırsanız bu sonuçları alırsınız ve bu kurallar her zaman geçerlidir." Test, kod değiştikçe bu vaadi korumanın yoludur.
Sözleşmeyi otomatik kontrol edilebilecek kontrollere çevirerek başlayın.
Birim testleri her operasyonun sonkoşullarını ve uç durumlarını doğrulamalıdır: dönüş değerleri, durum değişiklikleri ve hata davranışı. Arayüz "olmayan öğeyi silmek false döndürür ve hiçbir şey değiştirmez" diyorsa tam olarak bunu test edin.
Entegrasyon testleri gerçek sınırlar boyunca sözleşmeyi doğrulamalıdır: veritabanı, ağ, serileştirme ve auth. Birçok "sözleşme ihlali" ancak tipler encode/decode edilirken ya da retry/zaman aşımı olduğunda ortaya çıkar.
Invariantlar, geçerli operasyonların herhangi bir dizisinde doğru kalması gereken kurallardır (örn. "bakiye negatif olamaz", "ID'ler benzersizdir", "list() ile dönen öğeler get(id) ile alınabilir").
Property-based testing, bu kuralları rastgele ama geçerli girdiler ve operasyon dizileri üretip test ederek sınar; insanın düşünmediği köşe durumlarını bulmada etkilidir.
Paylaşılan veya halka açık API'ler için tüketicilerin yaptıkları istek örneklerini ve güvendikleri yanıtları yayınlamalarına izin verin. Sağlayıcılar CI'da bu sözleşmeleri çalıştırarak değişikliklerin gerçek kullanımı kırmadığını doğrular.
Testler her şeyi kapsayamaz; bu yüzden sözleşmenin değiştiğini gösteren sinyalleri izleyin: yanıt şekli değişiklikleri, 4xx/5xx oranlarında artış, yeni hata kodları, gecikme artışları, bilinmeyen alan veya deseralize hataları. Bunları endpoint ve sürüm bazında izleyin ki sürüklenmeyi erken tespit edip güvenle geri alabilelim.
Eğer teslim hattınızda snapshot veya rollback destekliyorsa, bunlar bu zihniyetle doğal olarak eşleşir: sürüklenmeyi erken tespit edin, sonra istemcileri orta bir olaya zorlamadan geri alın. (Örneğin Koder.ai snapshot ve rollback iş akışının parçasıdır; bu "önce sözleşme, sonra değişiklik" yaklaşımına uyar.)
Soyutlamayı önemseyen takımlar bile anlık pratiklik uğruna zamanla API'yi özel durumlar yığınına dönüştürebilecek desenlere kayabilir. İşte tekrarlayan tuzaklar ve ne yapmalı:
Feature flagler rollout için iyidir ama flag'ler halka açık, uzun ömürlü parametrelere dönüştüğünde (?useNewPricing=true, mode=legacy, v2=true) sorun başlar. Zamanla çağıranlar bunları beklenmedik şekillerde birleştirir ve birden fazla davranışı sonsuza dek desteklemek zorunda kalırsınız.
Daha güvenli yaklaşım:
Tablo ID'leri, join anahtarları veya "SQL şeklinde" filtreler (where=...) isteyen API'ler istemcileri storage modelinizi öğrenmeye zorlar. Bu, refactor'ları kırıcı yapar.
Bunun yerine domain kavramları ve stabil identifierlar etrafında modelleyin. İstemcilerin "nasıl sakladığınızı" sormasına gerek kalmasın; ne istediklerini sorun.
Alan eklemek zararsız görünür ama tekrarlanan "bir alan daha" değişiklikleri sorumlulukları bulanıklaştırır ve invariantları zayıflatır. İstemciler kazara bu detaylara bağlı hale gelir.
Uzun vadeli maliyeti azaltın:
Aşırı soyutlama gerçek ihtiyaçları engelleyebilir—örneğin cursor desteklemeyen bir sayfalama veya "exact match" belirtmeyen bir arama. İstemciler bunu aşmak için workaround yapar; daha kötü performans ve daha fazla hata ortaya çıkar.
Çözüm kontrollü esnekliktir: açık uçlu kaçış yolları yerine (örn. desteklenen filtre operatorleri) küçük, iyi tanımlanmış uzatma noktaları sunun.
Sadeleştirme güçten almak anlamına gelmez. Kafa karıştırıcı seçenekleri deprecate edin ama altta yatan yeteneği daha temiz bir biçimle tutun: üst üste binen parametreleri tek yapılandırılmış istek objesiyle değiştirin veya "her şeyi yapan" uç noktayı iki uyumlu uç noktaya bölün. Sonra geçişi sürümlü dokümantasyon ve net deprecasyon takvimi ile yönlendirin.
Liskov’un veri soyutlaması fikirlerini basit bir tekrarlanabilir kontrol listesiyle uygulayabilirsiniz. Amaç mükemmellik değil—API vaatlerini açık, test edilebilir ve evrilmesi güvenli kılmaktır.
Kısa, tutarlı bloklar kullanın:
transfer(from, to, amount)amount > 0 ve hesaplar mevcutInsufficientFunds, AccountNotFound, TimeoutDerinleşmek isterseniz bakın: Abstract Data Types (ADTs), Design by Contract, ve Liskov Substitution Principle (LSP).
Eğer ekibiniz iç notlar tutuyorsa, bunları /docs/api-guidelines gibi bir sayfadan linkleyin ki inceleme iş akışı tekrar kullanılabilir kalsın—ve eğer yeni servisleri hızlıca oluşturuyorsanız (elle veya Koder.ai gibi sohbet destekli bir araçla), bu kılavuzları "hızlı yayınlama"nın vazgeçilmez bir parçası olarak görün. Güvenilir arayüzler hızın bileşik olarak fayda sağlamasını sağlar, tersine çevirmesini değil.
O, veri soyutlaması ve bilgi gizleme kavramlarını yaygınlaştırdı; bunlar modern API tasarımına doğrudan uyarlanır: küçük ve stabil bir sözleşme yayınlayın, implementasyonu esnek tutun. Kazanç pratiktir: daha az kırılma, güvenli refactor'lar ve tahmin edilebilir entegrasyonlar.
Bir güvenilir API, zaman içinde arayanların güvenebileceği bir şeydir:
Güvenilirlik "hiç başarısız olmamak" değil; tahmin edilebilir şekilde başarısız olmak ve sözleşmeye sadık kalmaktır.
Davranışı bir sözleşme olarak yazın:
Boş sonuçlar, çoğaltmalar, sıralama gibi uç durumları da ekleyin ki çağıranlar beklentilere göre kod yazıp test edebilsinler.
Invariant, bir soyutlama içinde her zaman doğru olması gereken kuraldır (ör. “miktar hiçbir zaman negatif olamaz”). API bunları sınırda uygulamalıdır:
Bu, downstream hatalarını azaltır çünkü sistem artık imkansız durumlarla uğraşmak zorunda kalmaz.
Bilgi gizleme, işlemleri ve anlamı açığa çıkarmak; iç temsili gizlemek demektir. Değiştirme ihtimali olan şeylere tüketicileri bağlamayın (tablolar, cache anahtarları, shard anahtarları).
Pratik taktikler:
usr_...).Çünkü istemcileriniz storage modelinize bağlandığında, bir schema değişikliği API kırılmasına dönüşür. Eğer filtreler, join anahtarları veya dahili ID'ler açıksa, refactor yapmak zorlaşır.
Bunun yerine alanı gizli tutun ve müşterilerin ne istediklerini sorun: “belirli tarih aralığındaki müşteri siparişleri” gibi domain odaklı sorgular sağlayın.
LSP demektir ki: bir arayüzle çalışan kod, o arayüzün herhangi geçerli bir uygulamasıyla da özel durumlara ihtiyaç duymadan çalışmaya devam etmelidir. API açısından bu, çağıranı şaşırtmama kuralıdır.
Yerine geçebilir uygulamalar desteklemek için standardize edin:
Dikkat edilmesi gerekenler:
Eğer bir implementasyon gerçekten ek kısıt gerektiriyorsa, aynı arayüzü gizlice değiştirmeyin: ayrı bir arayüz veya açık bir yetenek bayrağı sunun ki istemciler bilerek tercih etsin.
Arayüzleri küçük ve uyumlu tutun:
options: any veya çok sayıda boolean gibi belirsiz parametrelerden kaçının.Hataları sözleşmenin bir parçası olarak tasarlayın:
UYgulamada mekanizma ne olursa olsun (istisnalar vs sonuç tipleri) tutarlılık, kullanıcıların tahmin edebilmesi için daha önemlidir.
status=3reservereleaselistvalidateFarklı roller veya değişim hızları varsa modülleri ayırın; çekirdek ve eklenti yaklaşımı büyürken faydalıdır.