Modüllere bölünmüş bir proje şemada her zaman iyi görünür: kutular, oklar, her kutunun bir sahibi. Şemada görünmeyen şey, o kutuların her birinin bir build dosyası, bir kaynak klasörü, bir test hedefi ve bakım isteyen bir sınırla birlikte geldiğidir. Denetlediğimiz projelerde en az "hâlâ tek modül" kadar sık gördüğümüz tablo, altı kişilik bir ekibin elli modülü ayakta tutmaya çalışmasıdır.
Bu yazı modularization karşıtı bir yazı sayılmaz. Büyük ekiplerde ve büyük kod tabanlarında modüller işe yarar, biz de öneririz. Ama kararı bir maliyet hesabı olarak görmek gerekiyor ve o hesabın gider tarafını pek kimse yazmıyor. Android'in kendi modularization rehberi bile bunu açıkça söylüyor: her modül build karmaşıklığı ve boilerplate olarak bir ek yük getirir, kod tabanı belli bir eşiği aşmayacaksa ölçeklenebilirlik ve build süresi kazanımları geçerli olmaz.
Örnek olarak kurgusal bir seyahat rezervasyon uygulaması kullanacağız: Rota. Üç iOS, üç Android geliştirici, on iki ekran grubu, iki yaşında bir ürün.
Kimsenin göstermediği fatura
Rota ekibi bir konferans dönüşü "her feature bir modül" kararını alıyor. Altı ay sonra iOS tarafında tablo şöyle:
Packages/
Core/ ← her şey buna bağlı
CoreUI/
Networking/
SearchInterface/ SearchFeature/ SearchFeatureTests/
BookingInterface/ BookingFeature/ BookingFeatureTests/
PaymentInterface/ PaymentFeature/ PaymentFeatureTests/
ProfileInterface/ ProfileFeature/ ProfileFeatureTests/
... (8 feature daha)
SearchDemoApp/ BookingDemoApp/ ... ← preview ve test host'ları
On iki feature, otuz altı hedef eder: her feature için bir implementasyon, bir interface ve bir test modülü. Buna altyapı modülleri ve demo uygulamaları eklenince sayı elliyi geçiyor. Kod miktarı değişmedi, yalnızca kodu taşıyan kap sayısı arttı. Bu kapların her birinin maliyetini tek tek yazalım.
Modül başına build yapılandırması. Her modülün deployment target'ı, Swift dil modu, derleyici bayrakları, lint ayarı ve bağımlılık listesi vardır. Android'de bu build.gradle.kts, namespace, compileSdk, ProGuard kuralları ve plugin listesidir. Convention plugin ya da ortak xcconfig ile bu tekrarı azaltırsınız, ama artık bakım isteyen bir build mantığı kod tabanınız olur. Swift 6 dil moduna geçiş gibi bir iş, elli hedefte elli ayrı karar demektir.
Interface modülleri sayıyı ikiye katlar. Feature'ların birbirine doğrudan bağlanmasını engellemek için her feature'a bir interface modülü açılır. Doğru bir tekniktir, ama on iki feature'ın yirmi dört modüle dönüştüğü andan itibaren her yeni ekran "protokol hangi modüle, model hangi modüle" tartışmasıyla başlar.
Modüller arası navigation ve DI. Tek modülde PaymentViewController(order:) yazıp geçtiğiniz yerde artık bir protokol, bir kayıt ve bir çözümleme adımı var:
// :feature:payment:api
interface PaymentEntry {
fun intent(context: Context, orderId: String): Intent
}
// :feature:payment:impl
internal class PaymentEntryImpl @Inject constructor() : PaymentEntry {
override fun intent(context: Context, orderId: String): Intent =
PaymentActivity.newIntent(context, orderId)
}
@Module
@InstallIn(SingletonComponent::class)
internal abstract class PaymentEntryModule {
@Binds abstract fun bind(impl: PaymentEntryImpl): PaymentEntry
}
// :feature:booking:impl, ödeme ekranını açmak isteyen taraf
@HiltViewModel
class BookingSummaryViewModel @Inject constructor(
private val paymentEntry: PaymentEntry,
) : ViewModel()
Bu kod kötü kod sayılmaz. Sorun, bir ekrandan diğerine geçmek için üç dosya ve iki modül gerekmesidir ve bu bedel her geçişte yeniden ödenir. Ayrıca derleme zamanında yakalanan "bu ekran yok" hatası, DI grafiğinin kurulduğu ana ya da çalışma zamanına kayar.
Kaynaklar ve yerelleştirme. iOS'ta bir Swift package hedefine taşınan kodda Bundle.main üzerinden yapılan her kaynak araması bozulur, Bundle.module'e çevrilmesi gerekir. String'ler modüllere dağıldığında çeviri süreci tek dosya yerine on iki dosyayla çalışır. Android'de AGP 8.0'dan beri non-transitive R sınıfları varsayılandır: her modülün R sınıfı yalnızca kendi kaynaklarını içerir. Build için iyi bir şeydir, ama ortak bir string'i üç feature kullanıyorsa onu bir yere koymanız gerekir ve o yer genellikle herkesin bağlı olduğu bir :core:ui modülü olur.
Preview'lar ve test host'ları. Bir feature modülünü yalıtılmış çalıştırmak istiyorsanız ona sahte bağımlılıklar veren bir demo uygulaması gerekir. Keychain ya da entitlement isteyen testler bir host uygulamaya ihtiyaç duyar. Bunların her biri bakımı unutulan, bir sabah kırık bulunan hedeflerdir.
Proje yükleme ve indeksleme. IDE'nin açılışta ve her branch değişiminde yaptığı iş hedef sayısıyla artar. Android tarafında bunun belgelenmiş bir nedeni var: Gradle dokümantasyonuna göre IDE import ve sync işlemleri configuration cache'i henüz kullanmıyor, yani modül sayısının sync süresine etkisini cache ile kapatamazsınız. iOS tarafında aynı şeyi kendi projenizde ölçmenizi öneririz: branch değiştirdikten sonra indekslemenin bitmesine kadar geçen süreyi bir hafta not edin.
Onboarding yükü. Yeni gelen geliştirici "bu sınıfı nereye koyacağım" sorusunu elli seçenek arasından cevaplamak zorundadır. Yanlış cevap verdiğinde de derleyici onu circular dependency hatasıyla karşılar.
En pahalısı: yanlış sınırın donması. Tek modülde bir sınıfı başka klasöre taşımak bir sürükle bırak işlemidir. Modüller arasında aynı taşıma, public API değişikliği, bağımlılık grafiği değişikliği ve çoğu zaman bir interface modülü değişikliği demektir. Bu yüzden yanlış çizilmiş sınır düzeltilmez, etrafından dolaşılır. Rota'da "sepet" kavramı önce Booking'e konmuş, sonra Payment'ın da ihtiyacı olmuş, sonunda Core'a taşınmıştır. Core böyle büyür.
Modül sınırı, taşımayı pahalı hale getiren bir karardır. Sınır doğruysa bu pahalılık sizi korur, yanlışsa sizi kilitler.
Sınırı nereye çizeceğinizi domain sınırları yazımızda ayrıca anlatıyoruz. Buradaki iddiamız daha dar: sınırın nereden geçtiğinden emin değilseniz onu betona dökmeyin.
iOS: bağlama biçimi launch süresini ve binary boyutunu belirler
iOS'ta modül sayısından ayrı bir karar daha var: her modül uygulamaya nasıl bağlanacak. Apple'ın dokümantasyonu takası şöyle özetliyor: statik kütüphane build sırasında binary'ye kopyalanır, bu build süresini ve uygulama boyutunu artırabilir. Dinamik kütüphanede semboller uygulama açılırken dyld tarafından çözülür, bu da launch süresini artırabilir. Apple, uygulamanın yüklediği her ek framework'ün launch süresine eklendiğini, dyld'nin bu işin bir kısmını launch closure içinde cache'lese bile kalan işin yüklenen kütüphanelerin sayısına ve boyutuna bağlı olduğunu yazıyor.
Elli modülü dinamik framework olarak gömerseniz bu maliyeti kullanıcı öder. Hepsini statik bağlarsanız launch düzelir, ama iki yeni sorun çıkar: debug build'de her değişiklikten sonra büyük bir link adımı ve aynı statik modülün hem uygulamaya hem bir extension'a (ya da iki ayrı dinamik framework'e) bağlanmasıyla kodun iki kopya halinde paketlenmesi. İkincisi sinsi bir sorundur: binary büyür ve "tek" olduğunu sandığınız bir singleton'dan iki tane olur.
Swift Package Manager'da bir library ürününün tipini belirtmezseniz statik mi dinamik mi bağlanacağına SwiftPM karar verir, Apple da tipi boş bırakmanızı önerir. Yani çoğu ekip bu kararı bilerek vermemiştir. Build log'unda hangi modülün nasıl bağlandığına bir kere bakın.
Mergeable libraries neyi değiştirir
Xcode 15 ile gelen mergeable libraries bu takasın büyük kısmını ortadan kaldırıyor. Dokümantasyondaki tanımıyla: release build'de statik bağlamaya yakın launch süresi, debug build'de dinamik bağlamanın build süresi. Debug'da Xcode mergeable bağımlılıkları normal dinamik kütüphane gibi ele alır ve yeniden dışa aktarır (reexport). Release'de kütüphaneleri hedef binary'nin içine birleştirir. Ayar tarafı kısa:
// App.xcconfig: uygulama hedefinde
MERGED_BINARY_TYPE = automatic
// Elle yönetmek isterseniz:
// uygulama hedefinde MERGED_BINARY_TYPE = manual
// her framework'te MERGEABLE_LIBRARY = YES
automatic yalnızca doğrudan bağımlılıkları birleştirir: hedefin Link Binary With Libraries fazında listelenen ve aynı projedeki başka bir hedefin ürünü olan kütüphaneler. Dolaylı bağımlılıklar için elle yapılandırma gerekir. Apple'ın kendi notlarıyla birlikte okuyun: release'de binary biraz büyür ve uygulama ile extension arasında paylaşılan bağımlılıkları otomatik birleştirmek extension boyutunu şişirebilir, bu durumda elle yapılandırma önerilir.
Mergeable libraries framework ve dylib hedefleri için bir çözümdür. Modülleriniz Swift package hedefleriyse ve SwiftPM onları zaten statik bağlıyorsa değişen bir şey olmaz. Bizim çıkarımımız şu: iOS'ta "kaç modül" sorusunu "nasıl bağlanıyor" sorusundan ayrı cevaplayamazsınız ve ikisini de release build'de, gerçek cihazda ölçmeden karar veremezsiniz. Ölçüm için Apple'ın önerdiği araçlar Instruments'taki App Launch şablonu, Xcode Organizer'daki Launch Time paneli ve MetricKit'tir.
Android: her modül configuration süresine yazılır
Gradle bir build'i üç fazda çalıştırır ve configuration fazı, hangi task'ı istediğinizden bağımsız olarak çalışır. Gradle'ın performans rehberi bunu doğrudan söylüyor: bu fazdaki pahalı her işlem, gradle help gibi basit komutlar dahil her build'i yavaşlatır. Modül sayısı arttıkça configure edilecek proje sayısı artar. Seksen modüllü bir projede tek satır değiştirip uygulamayı çalıştırdığınızda derleme bir saniye sürebilir, ama ondan önce seksen build.gradle.kts değerlendirilir.
Configuration cache ve isolated projects neyi değiştirir
Configuration cache, configuration fazının sonucunu saklar ve girdiler değişmediyse sonraki build'lerde fazı tamamen atlar. Gradle 9.0'dan beri tercih edilen çalışma modu olarak belgeleniyor ve dokümantasyona göre Gradle 10'da varsayılan olacak. Bugün hâlâ sizin açmanız gerekiyor:
# gradle.properties
org.gradle.configuration-cache=true
org.gradle.parallel=true
org.gradle.caching=true
Bunun faturayı ne kadar düşürdüğünü abartmayın. Cache, build script'leri, gradle.properties, version catalog ya da configuration sırasında okunan bir environment değişkeni değiştiğinde geçersiz olur. Branch değiştiren, bağımlılık güncelleyen bir ekipte ilk build yine tam configuration öder. IDE sync'i de yukarıda yazdığımız gibi cache'i kullanmaz.
Bu kalan kısmı hedefleyen özellik isolated projects'tir: projeleri birbirinden yalıtarak configuration'ı paralel çalıştırır ve sonucu proje bazında cache'ler. Gradle 9.7'de experimental'dan incubating'e yükseldi ve org.gradle.isolated-projects=true ile açılıyor. Bedeli, bir projenin başka bir projenin değiştirilebilir durumuna erişememesidir: kök build.gradle.kts içindeki allprojects {} ve subprojects {} blokları convention plugin'lere taşınmak zorundadır. Incubating bir özelliğe build'inizi bağlamadan önce plugin'lerinizin uyumunu kendi projenizde deneyin.
Çıkarım iOS ile aynı yöne bakıyor: modül maliyeti düşüyor ama sıfırlanmıyor ve düşmesi için build mantığınızın temiz olması gerekiyor. Kök dosyada subprojects {} ile ayar dağıtan seksen modüllü bir proje, bu özelliklerin hiçbirinden tam yararlanamaz.
"Ama build hızlanıyor" iddiası ne zaman doğru
Doğru olduğu durumlar şunlar. Incremental build'de değişiklik bir yaprak modüldeyse ve o modülün public API'si değişmediyse yalnızca o modül yeniden derlenir. Birbirine bağlı olmayan modüller paralel derlenir, çekirdek sayınız arttıkça bu kazanç büyür. Remote build cache kullanıyorsanız değişmeyen modüller CI'da ve geliştirici makinesinde hiç derlenmez.
Doğru olmadığı durumlar daha az konuşulur. Clean build'de toplam iş azalmaz, modül başına sabit maliyetlerle artar. Derin bir grafikte paralellik işe yaramaz, çünkü A, B'yi; B, C'yi; C, D'yi bekliyorsa dört modül sırayla derlenir. WWDC22'deki build paralelliği oturumu bunu kritik yol kavramıyla anlatıyor: build süresinin alt sınırını en uzun bağımlılık zinciri belirler, modül sayısının buna etkisi yoktur.
En yaygın bozulma ise Core modülüdür:
Search ──┐
Booking ─┤
Payment ─┼──► Core (modeller, extension'lar, analytics, feature flag'ler, Money, DateFormatter…)
Profile ─┤
... ─────┘
Core'da bir dosyaya dokunduğunuzda ona bağlı her modül yeniden derlenir. Ekibin günlük değişikliklerinin çoğu Core'a dokunuyorsa elli modüllü projeniz incremental build açısından tek modül gibi davranır, üstüne elli modülün sabit maliyetini ödersiniz. Apple'ın incremental build rehberi aynı şeyi monolitik bir utilities framework'ü örneğiyle anlatıyor. Android tarafında implementation yerine api ile verilen her bağımlılık aynı etkiyi bir kat daha yayar.
Karardan önce ölçün
Build süresi şikâyeti olan ekiplerin çoğu, sürenin nereye gittiğini ölçmemiştir. Modularization haftalar süren bir yatırımdır, yarım günlük ölçüm onun yanında ucuzdur.
iOS
# Temiz build, hedef ve task tipi bazında süre özetiyle
xcodebuild -workspace Rota.xcworkspace -scheme Rota \
-configuration Debug \
-destination 'generic/platform=iOS Simulator' \
-showBuildTimingSummary clean build | tail -n 60
Xcode içinden aynı rapor Product > Perform Action > Build With Timing Summary ile alınır ve Report navigator'da ilgili build'in altında görünür. Bundan sonra üç incremental senaryoyu ayrı ayrı çalıştırın: bir yaprak feature'da gövde değişikliği, aynı feature'da public bir imza değişikliği, en çok bağımlısı olan modülde bir değişiklik.
Build timeline (Xcode 14 ve sonrası) asıl soruya cevap verir. Build log'u açıkken editör seçeneklerinden Assistant'ı açın, timeline log'un yanında belirir. Dikey eksen aynı anda çalışan task sayısını, renkler hedefleri, boşluklar ise bitmemiş bir task'ı bekleyen işleri gösterir. Timeline'ın büyük kısmı tek satırsa sorununuz paralellik eksikliğidir ve modül eklemek onu çözmez, grafiği düzleştirmek çözer. Tek bir uzun blok bir script fazıysa sorununuz modüllerle hiç ilgili olmayabilir. Apple'ın rehberi de önce bunu kontrol etmenizi söyler: girdi ve çıktı dosyası tanımlanmamış her Run Script fazı her build'de çalışır.
Android
# Configuration süresinin tek başına ne kadar tuttuğunu görün
./gradlew help --profile
# Android dokümantasyonunun önerdiği temiz ölçüm
./gradlew clean
./gradlew --profile --offline --rerun-tasks assembleDebug
--profile raporu build/reports/profile/ altına HTML olarak yazılır ve configuration, bağımlılık çözümleme ve task çalıştırma sürelerini ayrı sekmelerde verir. Proje bazındaki configuration sürelerine bakın: toplam sürenin kayda değer bir kısmı buradaysa modül eklemek durumu kötüleştirir. --scan ile üretilen Build Scan aynı veriyi task zaman çizelgesi ve cache isabet oranlarıyla birlikte gösterir.
Tek bir ölçüm gürültülüdür. Karşılaştırılabilir sayı için gradle-profiler kullanın (brew install gradle-profiler). Varsayılan olarak altı ısınma ve on ölçüm build'i çalıştırır:
# build.scenarios
clean_build {
tasks = [":app:assembleDebug"]
cleanup-tasks = ["clean"]
}
feature_govde_degisikligi {
tasks = [":app:assembleDebug"]
apply-non-abi-change-to = "feature/search/src/main/kotlin/com/rota/search/SearchViewModel.kt"
}
core_abi_degisikligi {
tasks = [":app:assembleDebug"]
apply-abi-change-to = "core/common/src/main/kotlin/com/rota/core/Money.kt"
}
gradle-profiler --benchmark --project-dir . \
--scenario-file build.scenarios \
clean_build feature_govde_degisikligi core_abi_degisikligi
# Sonuçlar: profile-out/benchmark.html
İkinci ve üçüncü senaryo arasındaki fark size Core sorununuzun büyüklüğünü verir. Bu üç sayıyı bugünkü yapınızla alın, bir kenara yazın. Modülleme sonrası aynı senaryoları yeniden çalıştırmadan "hızlandı" demeyin.
Modüllememenizi önerdiğimiz durumlar
Küçük ekip. Modül sınırlarının en somut faydası sahipliktir: kim neyi değiştirebilir, kim neyi review eder. Üç kişilik bir platform ekibinde herkes her yeri değiştirir, sınır yalnızca sürtünme üretir.
Genç ürün. Ürün yönünü ararken feature'lar birleşir, bölünür, silinir. İlk yıl çizilen sınırların ikinci yıl doğru çıkma ihtimali düşüktür ve yanlış sınırın maliyetini yukarıda yazdık.
Sınırlar hâlâ oynuyor. Son üç ayda "bu sınıf aslında nereye ait" tartışmasını kaç kez yaptığınıza bakın. Sık yapıyorsanız kavramlarınız oturmamış demektir, onları derleyiciye emanet etmenin zamanı gelmemiştir.
Darboğaz build süresi olmayabilir. Ölçtünüz ve incremental build kabul edilebilir çıktıysa, yavaşlığın kaynağı bir script fazı, kapt ya da yapılandırılmamış bir cache ise modularization yanlış ilaçtır. Android dokümantasyonu örneğin kapt'ın KSP'den belirgin biçimde yavaş olduğunu yazıyor, bu geçiş elli modül açmaktan çok daha ucuzdur.
Ucuz orta yol: tek modül, sıkı kurallar
Modüllerin vaat ettiği faydaların çoğu, yani okunabilir sınırlar, dar public yüzey ve bağımlılık yönü, modül açmadan da büyük oranda elde edilebilir. Elde edilemeyen kısım derleyici zorlaması ve build yalıtımıdır. Onları ihtiyaç doğduğunda satın alırsınız.
Android
Kotlin'de paketler ve import'lar bu işi kolaylaştırır. Her feature kendi paketinde yaşar, dışarıya açtığı her şey api alt paketindedir:
app/src/main/kotlin/com/rota/
feature/
search/ { api/ ui/ data/ }
booking/ { api/ ui/ data/ }
payment/ { api/ ui/ data/ }
core/
designsystem/ network/ model/
Kuralı bir unit test zorlar. Yirmi satırdır ve CI'da çalışır:
class FeatureBoundaryTest {
private val root = File("src/main/kotlin/com/rota/feature")
private val featureImport = Regex("""^import com\.rota\.feature\.(\w+)\.(\w+)""")
@Test
fun `feature'lar birbirine yalnizca api paketi uzerinden erisir`() {
val violations = root.walkTopDown()
.filter { it.extension == "kt" }
.flatMap { file ->
val owner = file.relativeTo(root).invariantSeparatorsPath.substringBefore('/')
file.readLines().mapNotNull { line ->
val (feature, subpackage) = featureImport.find(line)?.destructured
?: return@mapNotNull null
if (feature == owner || subpackage == "api") null
else "${file.name}: $line"
}
}
.toList()
assertTrue(violations.joinToString("\n"), violations.isEmpty())
}
}
Aynı kuralı hazır araçlarla kurmanın yollarını Android'de mimari kurallarını zorlama yazımızda anlatıyoruz. Buradaki nokta şu: bu test bir modül sınırının verdiği bilginin büyük kısmını verir ve silmesi bir dakika sürer. Sınırı yanlış çizdiyseniz paketi taşır, testi güncellersiniz.
iOS
Swift'te modül altında namespace olmadığı için iş daha zordur ve bunu gizlemeyeceğiz: aynı modüldeki internal her tip her yerden görünür, import olmadığı için import kuralı da yazamazsınız. Elinizdeki araçlar private ve fileprivate'ı cömertçe kullanmak, her feature'ın dışarıya tek bir giriş tipi açması ve bunu isim kuralıyla lint'e bağlamaktır:
// Features/Payment/PaymentEntry.swift: feature'ın dışarıya açık tek yüzü
@MainActor
enum PaymentEntry {
static func makeCheckout(orderID: Order.ID, onFinish: @escaping (PaymentResult) -> Void) -> UIViewController {
let viewModel = PaymentCheckoutViewModel(orderID: orderID, onFinish: onFinish)
return PaymentCheckoutViewController(viewModel: viewModel)
}
}
enum PaymentResult { case paid, cancelled }
# .swiftlint.yml
custom_rules:
payment_internals:
name: "Payment iç tipleri"
included: "Rota/Features/.*\\.swift"
excluded: "Rota/Features/Payment/.*\\.swift"
regex: "\\bPayment(?!Entry\\b|Result\\b)[A-Z]\\w*"
message: "Payment'a yalnızca PaymentEntry üzerinden erişin."
severity: error
Kaba bir kuraldır, isim disiplinine dayanır ve yorum satırlarında yanlış alarm verebilir. iOS'ta orta yolun Android'dekinden zayıf olduğunu kabul ediyoruz. Bu yüzden iOS'ta ilk modülü biraz daha erken çıkarmayı öneririz. Daha sağlam seçenekleri iOS'ta mimari kurallarını zorlama yazımızda ele alıyoruz.
Zamanının geldiğini gösteren sinyaller ve çıkarma sırası
Şu sinyallerden birkaçı aynı anda varsa modül açmanın zamanı gelmiştir. Ölçümünüz incremental build süresinin büyük kısmının değişmeyen kodu yeniden derlemekle geçtiğini gösteriyor. Aynı kodu ikinci bir hedef kullanacak: widget, App Clip, share extension, Wear OS uygulaması. Ayrı roadmap'i olan ikinci bir ekip aynı kod tabanına geldi ve merge conflict'ler sahiplik sorununa dönüştü. Lint kuralınız aylardır aynı sınırı koruyor ve o sınır hiç tartışılmıyor. Sonuncusu en güvenilir sinyaldir: sınır kendini kanıtlamıştır.
Çıkarırken izlediğimiz sıra, en kararlı ve en az domain bilgisi taşıyan koddan başlar:
1. Design system (renkler, tipografi, bileşenler; domain bilmez, nadiren kırılır)
2. Altyapı (network, persistence, analytics istemcisi; dar bir API arkasında)
3. Kararlı domain çekirdeği (aylardır imzası değişmeyen modeller ve kurallar)
4. Sınırı en net feature (kendi ekibi olan ya da ikinci bir hedefin kullanacağı)
5. Geri kalanı (yalnızca sinyal geldikçe, tek tek)
İlk üç adım Core sorununu baştan engeller: her şeyin bağlı olduğu tek modül yerine değişme nedeni farklı üç küçük modül olur. Dördüncü adımda interface modülünü yalnızca gerçekten iki feature birbirini çağırıyorsa açın. Çoğu feature'ı uygulama hedefi birbirine bağlayabilir ve bu durumda interface modülüne gerek kalmaz. Her adımdan sonra aynı ölçüm senaryolarını yeniden çalıştırın. Sayı kötüleştiyse durun ve nedenini bulun.
Kendi projeniz için maliyet ve fayda çizelgesi
Aşağıdaki tabloyu ekipçe doldurun. Üçüncü sütuna tahmin yazmayın, ölçtüğünüz ya da saydığınız değeri yazın.
| Soru | Nasıl ölçersiniz | Sizin değeriniz | Modüllemeye iter mi |
|---|---|---|---|
| Yaprak feature'da gövde değişikliği sonrası incremental build süresi | Timing summary, gradle-profiler senaryosu | Geliştiriciyi akıştan koparıyorsa evet | |
| Aynı sürenin ne kadarı değişmeyen kodu yeniden derliyor | Build timeline, --profile task sekmesi |
Çoğuysa evet | |
| Configuration ya da proje yükleme süresi | ./gradlew help --profile, Xcode açılış ve indeksleme süresi |
Şimdiden yüksekse hayır, önce onu düzeltin | |
| Release build'de launch süresi | Instruments App Launch, Organizer | Bağlama planınız yoksa hayır | |
| Platform başına geliştirici sayısı | Sayın | Beş altı kişinin altındaysa hayır | |
| Bağımsız roadmap'i olan ekip sayısı | Sayın | İki ve üzeriyse evet | |
| Kodu paylaşacak ikinci hedef | Roadmap'e bakın | Varsa evet, yalnızca o kod için | |
| Son üç ayda feature'lar arası taşınan sınıf sayısı | git log --diff-filter=R --stat |
Çoksa hayır, sınırlar oturmamış | |
| Lint ile korunan ve aylardır değişmeyen sınır | Kural dosyanızın geçmişi | Varsa evet, o sınırdan başlayın | |
| Build mantığına bakacak kişi | İsim yazın | İsim yoksa hayır |
Kararımız şöyle işler: "evet" satırları üçten azsa modül açmayın, orta yolu uygulayın ve altı ay sonra tabloyu yeniden doldurun. Üç ve üzeriyse yalnızca "evet" çıkan satırların işaret ettiği kodu çıkarın. Tablonun hiçbir satırı "her feature bir modül" sonucunu vermez, o karar hep tablonun dışından gelir.
Kontrol listesi
- Temiz build ve üç incremental senaryo (yaprak gövde, yaprak imza, en bağımlı modül) ölçüldü ve kaydedildi.
- Build timeline ya da
--profileraporunda sürenin nereye gittiğine bakıldı: script fazı, configuration, annotation processing, link. - iOS: her modülün statik mi dinamik mi bağlandığı build log'undan doğrulandı, release build launch süresi gerçek cihazda ölçüldü.
- iOS: framework hedefleri varsa
MERGED_BINARY_TYPEdeğerlendirildi, extension'larla paylaşılan bağımlılıklar ayrıca kontrol edildi. - Android: configuration cache açık, kök dosyada
allprojects {}ya dasubprojects {}kalmadı, ortak ayarlar convention plugin'de. - Her şeyin bağlı olduğu bir
Coreya daCommonmodülü varsa değişme nedenine göre bölünmesi planlandı. - Tek modül içinde klasör ve paket sınırları yazıldı ve lint ya da test ile CI'da zorlanıyor.
- Çıkarılacak ilk modül, aylardır değişmeyen bir sınırın arkasındaki kararlı kod.
- Interface modülü yalnızca iki feature'ın birbirini gerçekten çağırdığı yerde açılıyor.
- Her çıkarmadan sonra aynı ölçümler yeniden alınıyor, sayı kötüleşirse duruluyor.
- Build mantığının bir sahibi var.
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.
Kaynaklar
- Apple · Configuring your project to use mergeable libraries
- Apple · Reducing your app’s launch time
- Apple · Improving the speed of incremental builds
- Apple · WWDC22, Demystify parallelization in Xcode builds
- Android · Guide to Android app modularization
- Android · Profile your build
- Android · Optimize your build speed
- Gradle · Configuration cache
- Gradle · Isolated projects
- Gradle · gradle-profiler
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.