← Yazılara dön

Üretim Seviyesinde Bir iOS SDK'sı için Tasarım İlkeleri

Üretime giden bir iOS SDK'sı tasarlarken: API yüzeyi, sürüm uyumluluğu, bağımlılık politikası ve gerçek-dünya kullanım pratiği.


1. Giriş: SDK, Uygulama Kodundan Farklıdır

Bir uygulama yazarken kodu istediğiniz zaman değiştirebilir, bir hatayı bir sonraki sürümde sessizce düzeltebilirsiniz. Bir SDK (yazılım geliştirme kiti) yazarken ise kodunuz başka geliştiricilerin elinde çalışır. Verdiğiniz her public API bir sözleşmedir, çökmeniz onların uygulamasını çökertir ve eklediğiniz her bağımlılık onların projesine bulaşır. Bu yüzden SDK tasarımı, uygulama geliştirmeye kıyasla farklı ve daha katı ilkeler gerektirir.

Bu makale, üretim ortamında binlerce uygulamada güvenle çalışacak bir iOS SDK’sının dayanması gereken temel tasarım ilkelerini ele alır.

2. Mümkün Olan En Küçük Public Yüzey

Bir SDK’nın en tehlikeli kısmı, sızdırdığı public detaylardır. Public yaptığınız her tip, her metot, ileride değiştirilemeyen bir taahhüttür. Bu yüzden varsayılan tutum her şeyi internal tutmak, yalnızca gerçekten gerekli olanı public açmaktır.

// İçeride kalması gereken detay
internal struct RequestSigner {
    func sign(_ request: URLRequest) -> URLRequest { ... }
}

// Dışarıya açılan tek giriş noktası
public final class AnalyticsClient {
    public static let shared = AnalyticsClient()

    private let signer = RequestSigner()

    private init() {}

    public func track(event name: String, properties: [String: String] = [:]) {
        // ...
    }
}

Burada RequestSigner bir implementasyon detayıdır ve dışarı sızmaz. Kullanıcı yalnızca AnalyticsClient.shared.track(...) görür. Yarın imzalama mantığını tamamen değiştirseniz bile public sözleşme bozulmaz.

3. Kararlı ve Evrilebilir API

Bir SDK’nın itibarı, sürüm yükseltmenin acısız olmasına bağlıdır. Geriye dönük uyumluluğu kırmak, müşterilerinizi sürüm yükseltmekten alıkoyar. Bir API’yi kaldırmanız gerektiğinde önce onu işaretleyin (deprecate), bir geçiş yolu sunun, ancak birkaç ana sürüm sonra silin:

public extension AnalyticsClient {
    @available(*, deprecated, message: "track(event:properties:) kullanın")
    func logEvent(_ name: String) {
        track(event: name)
    }
}

Yeni parametre eklerken varsayılan değer kullanın, böylece mevcut çağıranların kodu derlenmeye devam eder:

public func track(
    event name: String,
    properties: [String: String] = [:],
    timestamp: Date = Date()   // yeni eklendi, varsayılan değerli
) { ... }

Semantik sürümleme (semver) uygulamak da kritiktir: kırıcı değişiklikler yalnızca ana sürüm artışında yapılır.

4. Net Bir Yapılandırma ve Başlatma Modeli

SDK’lar genellikle bir API anahtarı veya ortam gibi yapılandırma gerektirir. Bunu zorunlu ve tek seferlik bir başlatma adımıyla netleştirmek, hataları erken yakalar. Sessizce çalışan ama yanlış yapılandırılmış bir SDK, hata vermeyen bir SDK’dan daha tehlikelidir.

public struct Configuration {
    public let apiKey: String
    public let environment: Environment
    public var loggingEnabled: Bool

    public init(apiKey: String,
                environment: Environment = .production,
                loggingEnabled: Bool = false) {
        self.apiKey = apiKey
        self.environment = environment
        self.loggingEnabled = loggingEnabled
    }

    public enum Environment {
        case production
        case staging
    }
}

public final class AnalyticsClient {
    public static let shared = AnalyticsClient()
    private var configuration: Configuration?

    public func configure(with configuration: Configuration) {
        self.configuration = configuration
    }

    public func track(event name: String) {
        guard configuration != nil else {
            assertionFailure("track çağrılmadan önce configure(with:) çağrılmalı")
            return
        }
        // ...
    }
}

Geliştirme sırasında assertionFailure erken uyarı verir, üretimde ise SDK sessizce ve güvenli şekilde davranır.

5. Asla Ana Uygulamayı Çökertmemek

Bir SDK’nın en temel kuralı budur: kendi hatanız asla ana uygulamayı çökertmemelidir. try!, as! ve zincirlenmemiş force unwrap SDK kodunda yasaktır. Hatalar tip güvenli şekilde dışarıya iletilir ve içeride asla yutulmaz:

public enum AnalyticsError: Error {
    case notConfigured
    case networkFailure(underlying: Error)
    case invalidPayload
}

public func track(
    event name: String,
    completion: @escaping (Result<Void, AnalyticsError>) -> Void
) {
    guard let configuration else {
        completion(.failure(.notConfigured))
        return
    }
    networkClient.send(name, key: configuration.apiKey) { result in
        switch result {
        case .success:
            completion(.success(()))
        case .failure(let error):
            completion(.failure(.networkFailure(underlying: error)))
        }
    }
}

Typed error, kullanıcının hatayı switch ile eksiksiz ele almasını sağlar ve gizli bir çökme bırakmaz.

6. Bağımlılıkları En Aza İndirmek

Bir SDK’ya eklenen her üçüncü taraf bağımlılık, müşterinin projesine de bulaşır. Eğer iki SDK aynı kütüphanenin farklı sürümlerine bağımlıysa, çakışma (diamond dependency) ortaya çıkar ve müşteri sıkışır. Bu yüzden üretim seviyesinde bir SDK ya hiç bağımlılık taşımaz ya da bağımlılığı içine gömerek (vendoring) dışarı sızdırmaz. Mümkün olan her yerde Foundation ve sistem framework’leri tercih edilir.

// Harici JSON kütüphanesi yerine Codable yeterlidir
struct EventPayload: Codable {
    let name: String
    let properties: [String: String]
    let timestamp: Date
}

let data = try JSONEncoder().encode(payload)

7. Eşzamanlılık ve Thread Güvenliği

SDK’nız hangi thread’den çağrılacağını bilemez. Bu yüzden iç durumunuzu thread güvenli hale getirmeli ve kullanıcıya geri dönüşlerin hangi thread’de geleceğini açıkça belgelemelisiniz. Swift Concurrency’de actor, paylaşılan durumu korumanın en temiz yoludur:

public actor EventQueue {
    private var pending: [EventPayload] = []

    public func enqueue(_ event: EventPayload) {
        pending.append(event)
    }

    public func flush() -> [EventPayload] {
        let copy = pending
        pending.removeAll()
        return copy
    }
}

UI ile ilgili geri dönüşler her zaman ana thread’e taşınmalıdır, aksi halde müşteri uygulamasında belirsiz çökmeler oluşur:

private func deliver(_ result: Result<Void, AnalyticsError>,
                     to completion: @escaping (Result<Void, AnalyticsError>) -> Void) {
    DispatchQueue.main.async {
        completion(result)
    }
}

8. Dağıtım: SPM ve XCFramework

Üretim seviyesinde bir SDK için en yaygın iki dağıtım kanalı vardır: kaynak kodlu Swift Package Manager dağıtımı ve önceden derlenmiş ikili (binary) XCFramework. İkili dağıtım, kaynak kodu gizlemek ve müşterinin derleme süresini kısaltmak istediğinizde tercih edilir.

// Package.swift, ikili hedef tanımı
// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "Analytics",
    products: [
        .library(name: "Analytics", targets: ["Analytics"])
    ],
    targets: [
        .binaryTarget(
            name: "Analytics",
            url: "https://cdn.example.com/Analytics-1.2.0.xcframework.zip",
            checksum: "a1b2c3..."
        )
    ]
)

İkili dağıtımda Swift’in modül stabilitesi (library evolution) etkinleştirilmelidir, aksi halde farklı Swift sürümleriyle derlenen müşteri projeleri SDK’yı bağlayamaz.

9. Gözlemlenebilirlik, Ama Kontrol Müşteride

SDK’nız loglama yapabilir, ancak bu logları müşteri kontrol edebilmelidir. Konsolu sessizce kirleten bir SDK, gürültü kaynağıdır. Loglamayı varsayılan olarak kapalı tutun ve müşteriye kendi log işleyicisini takabilme imkanı verin:

public protocol AnalyticsLogger {
    func log(_ message: String)
}

public final class AnalyticsClient {
    public var logger: AnalyticsLogger?

    private func debug(_ message: String) {
        logger?.log("[Analytics] \(message)")
    }
}

Müşteri kendi loglama sistemini (örneğin OSLog) bağlayabilir, ya da hiç bağlamayarak SDK’yı tamamen sessiz tutabilir.

10. Sonuç

Üretim seviyesinde bir iOS SDK’sı tasarlamak, kısıtlamayı bir erdem olarak görmeyi gerektirir. En küçük public yüzey, kararlı ve evrilebilir API, asla ana uygulamayı çökertmeyen hata yönetimi, minimum bağımlılık, thread güvenliği ve müşteri kontrolünde gözlemlenebilirlik. Bunların hepsi tek bir hedefe hizmet eder: SDK’nızı kullanan geliştiricinin sizi unutabilmesi. En iyi SDK, görünmez olandır.




← Yazılara dön