Circular dependency büyük iOS projelerinde nasıl önlenir?

SwiftPM ve Xcode module cycle'ını zaten reddeder; asıl sorun, cycle'ın şişmiş bir Common module'ü ya da bölünemeyen bir monolit olarak geri dönmesidir. Beş tekniği, hangi nedene hangisinin uyduğunu ve yasak kenarı CI'da yakalayan script'i anlatıyoruz.

Module seviyesindeki circular dependency, iOS dünyasında garip bir hatadır: araçlar onu derlemeyi reddettiği için repoda neredeyse hiç göremezsiniz, ama etkisini her gün yaşarsınız. Booking ile Profile birbirine ihtiyaç duyduğunda cycle ortadan kalkmaz, biçim değiştirir. İki feature'ın ortak ihtiyacı Common adlı bir module'e taşınır, ya da iki feature hiç ayrılmayıp tek bir dev target olarak kalır. Bu yazının konusu target ve module seviyesindeki cycle'lar. Retain cycle ayrı bir dert, burada ona girmiyoruz.

Örnek olarak varsayımsal bir seyahat uygulaması kullanacağız: Booking, Profile, Analytics ve birkaç ortak module. Aşağıdaki hata mesajlarını ve komut çıktılarını Swift 6.4 ve Xcode 27.1 ile kendi makinemizde ürettik; sizin toolchain sürümünüzde ifadeler küçük farklarla çıkabilir.

Cycle'lar iyi niyetle yazılmış kodda doğar

Kimse sabah kalkıp "bugün bir cycle kurayım" demez. Büyük projelerde sık gördüğümüz dört doğum şekli var.

Birincisi karşılıklı navigasyon. Rezervasyon detayında yolcuya dokununca profil açılır, profilde "son rezervasyonlar" listesinden rezervasyon detayına gidilir. İkisi de meşru ürün gereksinimi ve ikisi de karşı tarafın ekranını yaratmak ister:

// Profile module'ü
import UIKit
import Booking   // "sadece son rezervasyonları göstermek için"

final class ProfileViewController: UIViewController {
    private let repository = BookingRepository.shared

    func didSelect(_ booking: BookingSummary) {
        let detail = BookingDetailViewController(bookingID: booking.id)
        navigationController?.pushViewController(detail, animated: true)
    }
}

// Booking module'ü
import UIKit
import Profile   // yolcu profilini açmak için

final class BookingDetailViewController: UIViewController {
    @objc private func passengerTapped() {
        let profile = ProfileViewController(userID: passengerID)
        navigationController?.pushViewController(profile, animated: true)
    }
}

İkincisi, sessizce bir feature'ı import eden "ortak" yardımcı. Aşağıdaki kod zararsız görünür, ama en alttaki module'ü en üsttekilere bağlar ve Analytics kullanan her feature artık dolaylı olarak diğer bütün feature'lara bağımlıdır:

// Analytics module'ü
import UIKit
import Booking
import Profile

public enum ScreenTracker {
    public static func name(for viewController: UIViewController) -> String {
        switch viewController {
        case is BookingDetailViewController: "booking_detail"
        case is ProfileViewController: "profile"
        default: String(describing: type(of: viewController))
        }
    }
}

Üçüncüsü yanlış module'de yaşayan modeller. BookingSummary tipi Booking içinde tanımlıdır, çünkü ilk orada lazım olmuştur. Sonra profil ekranı da onu göstermek ister ve Profile → Booking kenarı yalnızca bir struct yüzünden doğar.

Dördüncüsü her feature'ı tanıyan merkezî parçalar: deep link router'ı, analytics, feature flag kayıt defteri. Bunlar doğası gereği her şeyi bilir. Sorun, feature'ların da onları çağırmak istemesiyle başlar.

Niyet edilen                 Kodun gerçekte istediği

App                          App
 ├── Booking                  ├── Booking ⇄ Profile
 ├── Profile                  │      ↓         ↓
 └── Analytics                └── Analytics ───┘
                                     └──→ Booking, Profile

Araçlar cycle'ı görünce ne yapar?

SwiftPM: manifest aşamasında durur

Aynı package içinde iki target'ı birbirine bağlayın:

// swift-tools-version: 6.0
import PackageDescription

let package = Package(
    name: "Travel",
    targets: [
        .target(name: "Booking", dependencies: ["Profile"]),
        .target(name: "Profile", dependencies: ["Booking"]),
    ]
)

swift build tek satır kaynak derlemeden durur:

error: 'travel': cyclic dependency declaration found: Booking -> Profile -> Booking

Zincir uzunsa mesaj bütün yolu yazar; bizim denememizde App -> Booking -> Shared -> Profile -> Booking çıktı. Aynı hata swift package describe çalıştırdığınızda da gelir, yani cycle'lı bir manifest'i incelemeniz bile mümkün olmaz.

Package'lar arası durumda bir incelik var. SwiftPM 6.0 CHANGELOG'una göre tools-version 6.0'dan itibaren iki package, target seviyesinde cycle oluşmadığı sürece birbirine bağımlı olabilir. Denedik: PA ve PB birbirini dependency olarak yazdığında ve target'lar cycle kurmadığında build geçti. Aynı manifest'leri tools-version 5.9'a çektiğimizde şu hata çıktı:

error: cyclic dependency between packages PA -> PB -> PA requires tools-version 6.0 or later

Bu serbestliği bir tasarım aracı olarak kullanmanızı önermiyoruz. Package seviyesinde iki yönlü ok, sahipliğin karıştığının işaretidir ve target cycle'ına bir commit uzaklıktadır.

Xcode target'ları: build başlamadan hata

İki framework target'ını birbirinin target dependency listesine eklediğimizde xcodebuild şunu yazdı:

error: Cycle in dependencies between targets 'Booking' and 'Profile'; building could produce unreliable results.
Cycle path: Booking → Profile → Booking
Cycle details:
→ Target 'Booking' has an explicit dependency on Target 'Profile'
→ Target 'Profile' has an explicit dependency on Target 'Booking'

Xcode Help'teki ilgili sayfa, bağımlılığın açıkça yazılmasının şart olmadığını da hatırlatır: bir target başka bir target'ın ürününe link ediyorsa bağımlılık örtük olarak kurulur, cycle da böyle doğabilir. Ters yöndeki bağımlılığı hiç yazmayıp yalnızca import Booking satırını eklediğimizde ise temiz build'de Unable to resolve module dependency: 'Booking' hatasını aldık. Her iki durumda da sonuç aynı: target seviyesinde cycle derlenmiyor.

Tuist de graph'ı yüklerken aynı kontrolü yapar ve kaynak kodundaki mesaj Found circular dependency between targets: ... biçimindedir.

Tek module içindeki cycle'ı kimse görmez

Araçların reddettiği şey target'lar arası cycle. Bir module'ün içindeki tipler ise birbirine istediği kadar dolanabilir, çünkü Swift bir module'ün bütün dosyalarını tek birim olarak derler. BookingDetailViewController ile ProfileViewController aynı app target'ında yaşadığı sürece birbirini serbestçe yaratır ve hiçbir araç şikâyet etmez.

Fatura, o target'ı bölmeye kalktığınız gün gelir. Profile klasörünü yeni bir target'a taşırsınız, derleyici size yüzlerce Cannot find type ... in scope hatası verir ve bunların yarısı geri yöndeki kenarlardır. "Modülerleşme tahminimiz iki sprint'ti" cümlesi genellikle burada biter. Bölmeden önce ucuz bir ölçüm yapabilirsiniz: klasörü geçici bir dalda yeni target'a taşıyın, derleyin ve geri yöndeki hataları sayın. O sayı, kırmanız gereken kenar sayısıdır.

Cycle'ı kıran beş teknik

1. Interface ve implementation module'ü ayırın

En çok işe yarayan teknik bu. Her feature'ın başkalarına verdiği sözü küçük bir module'e koyarsınız: ProfileInterface yalnızca protocol'leri ve basit değer tiplerini içerir, Profile ise gerçek ekranları. Diğer feature'lar yalnızca interface'e bağımlı olur.

// ProfileInterface module'ü
import UIKit
import TravelModels

@MainActor
public protocol ProfileScreenBuilding {
    func makeProfileScreen(for user: UserID) -> UIViewController
}

// BookingInterface module'ü
import UIKit
import TravelModels

@MainActor
public protocol BookingScreenBuilding {
    func makeBookingDetail(for id: BookingID) -> UIViewController
}

public protocol BookingHistoryProviding: Sendable {
    func recentBookings(for user: UserID, limit: Int) async throws -> [BookingSummary]
}

Booking artık Profile'ı tanımaz, yalnızca verilen sözü tanır:

// Booking module'ü
import UIKit
import BookingInterface
import ProfileInterface
import TravelModels

final class BookingDetailViewController: UIViewController {
    private let bookingID: BookingID
    private let profile: any ProfileScreenBuilding
    var passengerID = UserID("u-1")

    init(bookingID: BookingID, profile: any ProfileScreenBuilding) {
        self.bookingID = bookingID
        self.profile = profile
        super.init(nibName: nil, bundle: nil)
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) { fatalError("init(coder:) kullanılmıyor") }

    @objc private func passengerTapped() {
        let screen = profile.makeProfileScreen(for: passengerID)
        navigationController?.pushViewController(screen, animated: true)
    }
}
Önce                         Sonra

Booking ⇄ Profile            Booking ──→ ProfileInterface ←── Profile
                             Profile ──→ BookingInterface ←── Booking
                             (interface'ler yalnızca TravelModels'e bağlı)

Yan kazanç: Profile içindeki bir view değiştiğinde Booking yeniden derlenmez, çünkü bağımlı olduğu module değişmemiştir.

2. Kablolamayı composition root yapsın

Protocol'ler tanımlandı, ama birinin ProfileFeature'ı Booking'e vermesi gerekiyor. Bu iş, her şeyi görmeye yetkili tek yer olan uygulama katmanına aittir. Biz app target'ını ince tutup kablolamayı AppComposition adlı bir module'e koyuyoruz.

Burada dürüst olmamız gereken bir nokta var. Module cycle'ını çözdünüz, ama nesne grafiğindeki cycle duruyor: BookingFeature kurulurken ProfileScreenBuilding ister, ProfileFeature kurulurken BookingScreenBuilding. İkisini aynı anda init edemezsiniz. Çözüm, bağımlılığı hazır bir değer yerine ilk kullanımda çözülen bir closure olarak vermek:

// Booking module'ü
@MainActor
public final class BookingFeature: BookingScreenBuilding {
    private let profile: @MainActor () -> any ProfileScreenBuilding
    private let analytics: any AnalyticsTracking

    public init(
        profile: @escaping @MainActor () -> any ProfileScreenBuilding,
        analytics: any AnalyticsTracking
    ) {
        self.profile = profile
        self.analytics = analytics
    }

    public func makeBookingDetail(for id: BookingID) -> UIViewController {
        analytics.track(BookingOpened(bookingID: id))
        return BookingDetailViewController(bookingID: id, profile: profile())
    }
}

// AppComposition module'ü
@MainActor
public final class AppContainer {
    private let analytics: any AnalyticsTracking = ConsoleAnalytics()

    public init() {}

    private(set) lazy var booking: any BookingScreenBuilding = BookingFeature(
        profile: { [unowned self] in self.profile },
        analytics: analytics
    )

    private(set) lazy var profile: any ProfileScreenBuilding = ProfileFeature(
        bookings: { [unowned self] in self.booking },
        history: BookingHistoryService()
    )
}

lazy var ile closure birlikte çalışır: hangi feature önce istenirse o kurulur, diğeri ilk ekran açılışında çözülür. Container uygulama boyunca yaşadığı için unowned burada güvenlidir. Container'ın 400 satıra çıkmasını nasıl önleyeceğiniz ise ayrı bir yazının konusu.

3. Navigasyonu route tiplerine ve app katmanındaki router'a taşıyın

İki feature birbirini iki üç noktadan çağırıyorsa interface module'ü yeter. Onlarca ekran birbirine gidiyorsa her çift için protocol yazmak yorucu olur. O noktada feature'lara "nereye gitmek istediğini söyle, nasıl gidileceğini bilme" diyoruz. Route tipi ve router protocol'ü altta küçük bir Navigation module'ünde durur, uygulaması app katmanındadır:

// Navigation module'ü (altta, yalnızca TravelModels'e bağlı)
import TravelModels

public enum AppRoute: Hashable, Sendable {
    case bookingDetail(BookingID)
    case profile(UserID)
}

@MainActor
public protocol AppRouting: AnyObject {
    func show(_ route: AppRoute)
}

// AppComposition module'ü
@MainActor
final class AppRouter: AppRouting {
    private let navigationController: UINavigationController
    private let container: AppContainer

    init(navigationController: UINavigationController, container: AppContainer) {
        self.navigationController = navigationController
        self.container = container
    }

    func show(_ route: AppRoute) {
        let screen: UIViewController = switch route {
        case .bookingDetail(let id): container.booking.makeBookingDetail(for: id)
        case .profile(let user): container.profile.makeProfileScreen(for: user)
        }
        navigationController.pushViewController(screen, animated: true)
    }
}

Deep link aynı kalıba oturur. Her feature kendi URL'lerini AppRoute'a çeviren küçük bir handler verir, app katmanı bunları bir dizide toplar ve eşleşen ilk route'u router'a iletir. Böylece "her feature'ı tanıyan" parça app katmanında kalır, feature'lar onu tanımaz.

Önce                              Sonra

DeepLinkRouter ──→ Booking        AppRouter (app) ──→ Booking, Profile
      ↑    └─────→ Profile        Booking, Profile ──→ Navigation
Booking, Profile (çağırıyor)      (AppRoute + AppRouting)

Bedeli açık: AppRoute her ekibin dokunduğu bir dosya olur ve derleyici switch'i eksiksiz istediği için yeni route eklemek app katmanında da değişiklik gerektirir. Biz bunu kabul edilebilir buluyoruz, çünkü çakışma tek satırlık case eklemelerinden ibarettir.

4. Ortak tipleri aşağı indirin

Kenarın tek sebebi bir model ise protocol'e gerek yok. BookingSummary'yi Booking'den çıkarıp her iki feature'ın altındaki TravelModels'e taşıyın.

// TravelModels module'ü
import Foundation

public struct BookingSummary: Identifiable, Hashable, Sendable {
    public let id: BookingID
    public let title: String
    public let startDate: Date

    public init(id: BookingID, title: String, startDate: Date) {
        self.id = id
        self.title = title
        self.startDate = startDate
    }
}
Önce                         Sonra

Profile ──→ Booking          Profile ──→ TravelModels ←── Booking
(yalnızca BookingSummary
 için)

Tek kuralımız: aşağı inen şey davranışsız veri olmalı. Model, kimlik tipi, hata enum'u iner. Ağ çağrısı yapan, cache tutan ya da UI bilen bir tip indiğinde TravelModels birkaç ay içinde yeni Common olur.

5. Tek yönlü bildirimler için closure ya da event kullanın

Bazı kenarlar bir soru sormaz, yalnızca haber verir. Oturum kapanınca rezervasyon cache'i temizlenmelidir ve bunun en kısa yolu Profile içinden BookingCache.shared.clear() çağırmaktır. Oysa Profile'ın bir cevaba ihtiyacı yok. Olayı yayınlasın, kimin dinlediğini bilmesin:

// Profile module'ü
public final class SessionStore: SessionEvents {
    private let stream: AsyncStream<UserID>
    private let continuation: AsyncStream<UserID>.Continuation

    public init() {
        (stream, continuation) = AsyncStream.makeStream(of: UserID.self)
    }

    public func logouts() -> AsyncStream<UserID> { stream }

    public func logOut(_ user: UserID) {
        // Token silme, sunucuya haber verme...
        continuation.yield(user)
    }
}

// AppComposition module'ü, AppContainer içinde
public func start() {
    let logouts = session.logouts()
    let cache = bookingCache
    Task {
        for await user in logouts {
            await cache.clear(for: user)
        }
    }
}

Tek dinleyici varsa AsyncStream yeterli; birden fazla dinleyici gerekiyorsa her biri için ayrı stream üretin ya da basit bir closure listesi tutun. Aynı ters çevirme Analytics örneğini de çözer: Analytics module'ü bir AnalyticsEvent protocol'ü tanımlar, her feature kendi event tipini yazar ve Analytics hiçbir feature'ı import etmez.

NotificationCenter ile string anahtarlı userInfo taşımak da teknik olarak aynı kenarı kırar, ama bağımlılığı derleyicinin göremediği bir yere saklar. Onu çözüm saymıyoruz.

Hangi neden için hangi teknik?

Cycle'ın nedeni Teknik Kararımız
İki feature birbirinin ekranını açıyor, birkaç temas noktası var Interface module'ü + composition root Varsayılan tercihimiz
Çok sayıda feature birbirine navigasyon yapıyor, deep link var Route tipi + app katmanında router 8-10 feature'dan sonra buna geçiyoruz
Kenarın tek sebebi bir model ya da kimlik tipi Tipi aşağı indirin En ucuz çözüm, önce bunu kontrol edin
Bir feature diğerine yalnızca haber veriyor Closure ya da AsyncStream, kablolama app'te Cevap beklenmiyorsa protocol yazmayın
"Ortak" module bir feature'ı import ediyor Protocol'ü ortak module'e koyun, feature uygulasın Alt katman yukarıyı asla import etmez, istisnası yok
İki feature onlarca noktadan birbirine dolanmış Birleştirin Muhtemelen tek domain; sınırı yeniden çizin

Son satır en az sevilen cevaptır. İki module arasında yirmi protocol gerekiyorsa çizdiğiniz sınır yanlıştır ve domain sınırlarını yeniden düşünmek interface yazmaktan ucuzdur.

Tespit edin ve geri gelmesini engelleyin

SwiftPM'in kendi komutları

swift package show-dependencies kulağa doğru araç gibi gelir, ama package seviyesinde çalışır. Bütün module'leri tek package içinde target olarak tutuyorsanız size yalnızca dış bağımlılıkları gösterir:

.
└── swift-collections<https://github.com/apple/swift-collections.git@1.7.1>

Target seviyesindeki kenarlar swift package describe çıktısındadır. --type seçeneği text, json ve mermaid değerlerini alır. JSON çıktısından bize lazım olan alanlar şunlar:

swift package describe --type json \
  | jq -c '.targets[] | {name, target_dependencies}'
{"name":"TravelModels","target_dependencies":null}
{"name":"ProfileInterface","target_dependencies":["TravelModels"]}
{"name":"Profile","target_dependencies":["ProfileInterface","BookingInterface","TravelModels","Analytics"]}
{"name":"BookingInterface","target_dependencies":["TravelModels"]}
{"name":"Booking","target_dependencies":["BookingInterface","ProfileInterface","TravelModels","Analytics"]}
{"name":"AppComposition","target_dependencies":["Booking","Profile","BookingInterface","ProfileInterface","Analytics"]}
{"name":"Analytics","target_dependencies":null}

--type mermaid aynı graph'ı bir flowchart olarak verir; PR açıklamasına yapıştırmak için kullanışlı. Tuist kullanıyorsanız tuist graph aynı işi proje seviyesinde yapar: --format seçeneği dot, json, png ve svg kabul eder, --skip-test-targets ve --skip-external-dependencies ile gürültüyü azaltırsınız.

Yasak kenarı CI'da kıran script

Cycle'ı araçlar zaten reddediyor. Bizim yakalamak istediğimiz, cycle'dan bir adım önceki kenar: Profile → Booking tek başına cycle değildir ve SwiftPM onu kabul eder, ta ki biri ters yönü ekleyene kadar. Kural basit: feature'lar birbirine bağımlı olamaz, interface'ler yalnızca core'a bağımlı olabilir, core yukarıyı göremez.

#!/usr/bin/env python3
"""Module graph'ında yasak kenar varsa build'i kırar.
Kullanım: swift package describe --type json | Scripts/check-module-graph.py
"""
import json
import re
import sys

# Aşağıdan yukarıya katmanlar. Sıra önemli: ilk eşleşen kazanır.
LAYERS = [
    ("core",      re.compile(r"^(TravelModels|Navigation|Analytics|Networking|DesignSystem)$")),
    ("interface", re.compile(r"Interface$")),
    ("app",       re.compile(r"^AppComposition$")),
    ("feature",   re.compile(r".*")),
]

# Hangi katman hangisine bağımlı olabilir.
ALLOWED = {
    "core":      {"core"},
    "interface": {"core"},
    "feature":   {"core", "interface"},
    "app":       {"core", "interface", "feature"},
}

# Bilinen ve bileti açılmış istisnalar: (kimden, kime). Liste yalnızca kısalabilir.
BASELINE = set()


def layer_of(name):
    return next(layer for layer, pattern in LAYERS if pattern.search(name))


package = json.load(sys.stdin)
targets = [t for t in package["targets"] if t["type"] != "test"]
violations = []

for target in targets:
    source = target["name"]
    for dependency in target.get("target_dependencies") or []:
        if (source, dependency) in BASELINE:
            continue
        if layer_of(dependency) not in ALLOWED[layer_of(source)]:
            violations.append(
                f"{source} ({layer_of(source)}) -> {dependency} ({layer_of(dependency)})"
            )

if violations:
    print("Yasak module bağımlılıkları:", file=sys.stderr)
    for violation in violations:
        print(f"  {violation}", file=sys.stderr)
    sys.exit(1)

print(f"Module graph temiz: {len(targets)} target kontrol edildi.")

Örnek package'da Profile'a Booking, Analytics'e BookingInterface bağımlılığı eklediğimizde çıktı şöyle oldu ve script 1 ile çıktı:

Yasak module bağımlılıkları:
  Profile (feature) -> Booking (feature)
  Analytics (core) -> BookingInterface (interface)

Script yalnızca aynı package içindeki target kenarlarına bakar. Module'leriniz birden fazla package'a yayılmışsa her package için çalıştırıp product_dependencies alanını da kurala katın. Xcode target'larıyla çalışıyorsanız aynı tabloyu tuist graph --format json çıktısıyla besleyebilirsiniz; o JSON'un şemasını kendi Tuist sürümünüzde kontrol edin. Mimari kurallarını araçlarla zorlamanın diğer yollarına ayrı bir yazıda değiniyoruz.

Canlı projede mevcut cycle'ı güvenli adımlarla kırmak

Canlı projede cycle nadiren ilan edilmiş bir cycle olarak durur, çünkü araçlar buna izin vermez. Üç kılıktan birinde karşınıza çıkar: bölünemeyen tek bir target, iki feature'ın ortak ihtiyaçlarının döküldüğü şişkin bir Common, ya da NSClassFromString ve string anahtarlı service locator gibi çalışma zamanı hileleri. Common module'ü, çözülmemiş cycle'ların mezarlığıdır.

Her adımı ayrı PR olarak yayınlanabilir tutuyoruz:

  1. Gerçek kenarları sayın. Kırmak istediğiniz yön için temas noktalarını listeleyin: grep -rn "BookingDetailViewController\|BookingRepository" Sources/Profile. Sayı küçük olan yön, kıracağınız yöndür.
  2. Graph'ı dondurun. Yukarıdaki script'i CI'a ekleyin, mevcut ihlalleri BASELINE'a yazın. O günden sonra yeni yasak kenar giremez.
  3. Önce modelleri indirin. Yalnızca tip yüzünden var olan kenarlar davranış değiştirmeden kalkar; en az riskli PR budur.
  4. Interface module'ünü ekleyin, mevcut tipi ona uydurun. BookingFeature protocol'ü uygular, ama henüz kimse protocol'ü kullanmaz. Davranış değişmez.
  5. Çağıranları tek tek çevirin. Her ekran BookingDetailViewController(...) yerine inject edilen BookingScreenBuilding'i kullanır. Ekran başına bir PR, her biri test edilebilir.
  6. Import'u ve manifest'teki bağımlılığı silin. Son import Booking gittiğinde Package.swift'ten kenarı kaldırın ve BASELINE'dan satırı çıkarın.

İşe yaradığını iki yerden anlarsınız. Script yeşildir ve BASELINE bir satır kısalmıştır. Daha somutu: Booking içinde bir view dosyasına boşluk ekleyip build aldığınızda build log'unda Profile için derleme adımı görünmemelidir. Görünüyorsa bir yerde hâlâ implementation'a bağlısınız.

Diğer taraftaki tuzak: kimsenin bir şey bulamadığı interface ormanı

Bu teknikleri öğrenen ekiplerin bir kısmı her module'e refleks olarak bir Interface kardeşi açar. Altmış feature yüz yirmi module olur, "bu protocol nerede uygulanıyor" sorusunun cevabı üç dosya atlamayı gerektirir ve Xcode'un "Jump to Definition"ı sizi her seferinde protocol'de bırakır. Module sayısının kendi maliyeti de var; ona modülerleşmenin bedeli yazımızda ayrıca bakıyoruz.

Çizgiyi şuraya çekiyoruz:

  • Interface module'ü, onu kullanan ikinci bir feature ortaya çıktığında açılır. Kimsenin çağırmadığı yaprak feature'ların interface'i olmaz.
  • Interface'in içinde protocol ve davranışsız değer tipleri bulunur. Extension içinde varsayılan uygulama, üçüncü parti bağımlılığı ya da ağ kodu görürsek geri çeviririz.
  • Interface, başka bir interface'i import etmez. Buna ihtiyaç doğduysa paylaşılan tip TravelModels'e iner.
  • Bir interface on protocol'ü geçtiyse durup sınırı sorgularız.

Interface module'ü bir söz listesidir. Liste uzuyorsa sorun, çizdiğiniz sınırdadır.

Küçük ekiplerde ve on module'ün altında, yalnızca app katmanındaki router ile aşağı indirilmiş modeller çoğu zaman yeter. Interface module'lerini ihtiyaç kendini gösterdiğinde eklemek, baştan hepsini açıp sonra silmekten kolaydır.

Pazartesi sabahı için kontrol listesi

  • swift package describe --type json (ya da tuist graph --format json) ile gerçek target graph'ını çıkardık.
  • Katmanları yazılı hâle getirdik: core, interface, feature, app ve kimin kime bağımlı olabileceği.
  • Yasak kenar script'i CI'da çalışıyor; mevcut ihlaller BASELINE'da ve her birinin bileti var.
  • Hiçbir core module bir feature'ı ya da interface'i import etmiyor.
  • Feature'lar arası her kenar için nedeni belirledik: model, navigasyon, sorgu ya da bildirim.
  • Yalnızca model yüzünden var olan kenarları tipleri aşağı indirerek kaldırdık.
  • Karşılıklı navigasyon interface protocol'ü ya da route tipi üzerinden yapılıyor; kablolama composition root'ta.
  • Nesne grafiğindeki karşılıklı bağımlılıklar closure ile ilk kullanımda çözülüyor.
  • Interface module'lerinde uygulama kodu ve üçüncü parti bağımlılığı yok.
  • Bir feature'ın implementation'ındaki değişiklik, onu kullanan feature'ları yeniden derletmiyor; build log'undan doğruladık.
Seviye 9 tamamlandıİlerlemen bu tarayıcıda saklanıyor.
Sıradaki: Compile-time dependency graph'i yönetmek: iOS

Bu konuda desteğe mi ihtiyacınız var?

Uygulamanızda bu kontrollerin doğru kurulup kurulmadığını birlikte inceleyelim. Güvenlik denetimi ya da danışmanlıkla buradayız.

Yorumlar ve sorular

Katılmadığınız bir nokta ya da projenizde takıldığınız bir yer mi var? Yazın, konuşalım.