Python kütüphaneleri geliştirirken karşılaşılan en büyük zorluklardan biri, API’lerinizi zamanla nasıl esnek tutacağınız ve olası uyumluluk kırılmalarını nasıl önleyeceğinizdir. Özellikle de operasyonlar için ‘seçenekler’ veya ‘yapılandırma’ gibi sürekli değişebilecek durumları temsil eden karmaşık veri koleksiyonları söz konusu olduğunda bu durum daha da kritikleşir. Bu tür nesneler, minimal bir uyumluluk yüzeyine ve çok dikkatli seçilmiş bir genel arayüze sahip olmalıdır.
Esnek API Tasarımı Neden Önemli?
Bir kütüphane yazdığınızı ve fiziksel paket gönderimi yapan bir servisi sardığınızı düşünelim. Paket göndermenin pek çok yolu vardır: farklı taşıyıcılar, hava, kara veya deniz taşımacılığı, ertesi gün teslimat, imza gereksinimi, takip veya taahhütlü posta gibi sayısız seçenek. Bu karmaşıklığı yönetmek için başlangıçta ‘ShippingOptions’ gibi bir nesneye ihtiyacınız olabilir.
Kütüphanenizin çekirdeğinde aşağıdaki gibi bir fonksiyon bulunabilir:
async def shipPackage(<br> how: ShippingOptions,<br> where: Address,<br> ) -> ShippingStatus:<br> ...Böyle bir kütüphaneyi ilk uygulamaya başladığınızda, ‘ShippingOptions’ yapısının ilk halinin eksik olacağını veya tamamen doğru olmayacağını bilirsiniz. Sorun alanını iyice anlamadan, tonlarca farklı özniteliğe sahip geniş bir genel API’ye taahhüt vermek istemezsiniz.
Karşılaşılan Zorluklar
- ‘ShippingOptions’ kütüphanenin geri kalanı için hayati önem taşır; ancak sürekli gelişen bir yapıya sahip olması, çok fazla karmaşıklık ve değişimle karşılaşmak istemezsiniz.
- Bu nesne, farklı gönderim hizmetleriyle ilgili çok karmaşık dahili öznitelikler içerebilecek tonlarca durumu barındırmak zorundadır.
- Bugün ‘acil değil’, ‘standart’ ve ‘hızlı’ gibi seçenekler eklemeniz gerekebilir, ancak mükemmel yapıyı bulana kadar bu uygulamayı süresiz olarak erteleyemezsiniz.
Python’da Opaque Veri Tipleri: typing.NewType ile Çözüm
Bu gibi durumlarda başvurmanız gereken araç, opak veri tipi tasarım desenidir. C gibi dillerde bu tür yapılar yaygındır (örneğin ‘FILE’, ‘pthread_*_t’, ‘fd_set’). Ancak Python’da, bir ‘dataclass’ veya herhangi bir sınıfı ifşa ettiğinizde, tüm alanları özel tutsanız bile kurucusu doğası gereği halka açık kalır. ‘typing.NewType’ ise bu sorunu çözmek için Python’ın sunduğu güçlü bir araçtır.
Gereksinimler ve Çözümler
Opak tipler için temel gereksinimler ve ‘typing.NewType’ ile nasıl karşılandıkları:
- Gereksinim 1: İstemci kodunun tip anotasyonlarında kullanabileceği genel bir tipe ihtiyaç duyulması.
- Çözüm 1: Genel bir ‘NewType’ kullanmak, bize genel bir isim sağlar.
- Gereksinim 2: İstemcilerin, niteliklerini veya dahili kurucu argümanlarını göremese bile tipi bir şekilde oluşturabilmesi.
- Çözüm 2: Tamamen özel özniteliklere sahip özel bir sınıfı sarmalamak, bize gerçek bir veri yapısı sağlarken kurucuyu ifşa etmez.
- Gereksinim 3: Gelecekte daha incelikli ve karmaşık yapılandırmalar eklendiğinde bile desteklenecek yüksek seviyeli kavramları (örneğin ‘hızlı gönderim’) ifade edebilmek.
- Çözüm 3: ‘NewType’imizi döndüren bir dizi genel kurucu fonksiyon kullanmak.
Uygulama Örnekleri: Esnekliği Korumak
Bu yaklaşımla, başlangıçta basit bir gönderim hızı tanımıyla başlayabiliriz:
from dataclasses import dataclass<br>from typing import Literal, NewType<br><br>@dataclass<br>class _RealShipOpts:<br> _speed: Literal['fast', 'normal', 'slow']<br><br>ShippingOptions = NewType('ShippingOptions', _RealShipOpts)<br><br>def shipFast() -> ShippingOptions:<br> return ShippingOptions(_RealShipOpts('fast'))<br><br>def shipNormal() -> ShippingOptions:<br> return ShippingOptions(_RealShipOpts('normal'))<br><br>def shipSlow() -> ShippingOptions:<br> return ShippingOptions(_RealShipOpts('slow'))Bu ilk haliyle çok ilgi çekici görünmese de, asıl amaç API’mizin gelecekteki evrimine esneklik katmaktır. Örneğin, gönderim seçeneklerine belirli bir taşıyıcı ve navlun yöntemi eklemeye karar verdiğimizde ne olacağına bakalım:
from dataclasses import dataclass<br>from enum import Enum, auto<br>from typing import NewType<br><br>class Carrier(Enum):<br> FedEx = auto()<br> USPS = auto()<br> DHL = auto()<br> UPS = auto()<br><br>class Conveyance(Enum):<br> air = auto()<br> truck = auto()<br> train = auto()<br><br>@dataclass<br>class _RealShipOpts:<br> _carrier: Carrier<br> _freight: Conveyance<br><br>ShippingOptions = NewType('ShippingOptions', _RealShipOpts)<br><br>def shipFast() -> ShippingOptions:<br> return ShippingOptions(_RealShipOpts(Carrier.FedEx, Conveyance.air))<br><br>def shipNormal() -> ShippingOptions:<br> return ShippingOptions(_RealShipOpts(Carrier.UPS, Conveyance.truck))<br><br>def shipSlow() -> ShippingOptions:<br> return ShippingOptions(_RealShipOpts(Carrier.USPS, Conveyance.train))<br><br>def shippingDetailed(<br> carrier: Carrier, conveyance: Conveyance<br>) -> ShippingOptions:<br> return ShippingOptions(_RealShipOpts(carrier, conveyance))Opak Tiplerin Avantajları
- Genel ‘ShippingOptions’ tipimiz bir kurucuya sahip değildir, böylece istemcilerin dahili yapıyı doğrudan manipüle etmesi engellenir.
- ‘_RealShipOpts’ özel olduğu ve tüm nitelikleri özel tutulduğu için, eski sürümlerin uyumluluk sorunları yaratmadan tamamen kaldırılması mümkündür.
- Kütüphanemiz içindeki kod, ‘ShippingOptions’ üzerindeki özel değişkenlere hala erişebilir, çünkü çalışma zamanında taban tipiyle aynıdır ve minimal ek yük sunar.
- Kütüphane dışındaki istemciler, ‘shipFast’, ‘shipNormal’ ve ‘shipSlow’ gibi genel kurucu fonksiyonlarımızı aynı imza ve davranışla çağırmaya devam edebilirler, dahili değişikliklerden etkilenmezler.
Genel API’niz içinde bir durumu oluşturmanız ve iletmeniz gerekiyorsa, ancak uyumluluk sorunlarıyla ilişkili kopmaları önlemek istiyorsanız, bu teknik size yardımcı olabilir!

