Yaygın modülerleştirme kalıpları

Tüm projelere uygun tek bir modülerleştirme stratejisi yoktur. Gradle'ın esnek yapısı nedeniyle projeleri düzenleme konusunda çok az kısıtlama vardır. Bu sayfada, çok modüllü Android uygulamaları geliştirirken kullanabileceğiniz bazı genel kurallar ve yaygın kalıplar hakkında genel bilgiler verilmektedir.

Yüksek uyum ve düşük bağlantı ilkesi

Modüler bir kod tabanını karakterize etmenin bir yolu, bağlantı ve uyum özelliklerini kullanmaktır. Bağlantı, modüllerin birbirine bağımlılık derecesini ölçer. Bu bağlamda uyum, tek bir modülün öğelerinin işlevsel olarak ne kadar ilişkili olduğunu ölçer. Genel bir kural olarak, düşük bağlantı ve yüksek uyum için çabalamalısınız:

  • Düşük bağlantı, modüllerin mümkün olduğunca birbirinden bağımsız olması gerektiği anlamına gelir. Böylece bir modüldeki değişiklikler diğer modülleri hiç etkilemez veya minimum düzeyde etkiler. Modüller, diğer modüllerin iç işleyişi hakkında bilgi sahibi olmamalıdır.
  • Yüksek bağlılık, modüllerin bir sistem gibi davranan bir kod koleksiyonundan oluşması gerektiği anlamına gelir. Net bir şekilde tanımlanmış sorumlulukları olmalı ve belirli bir alan bilgisi sınırları içinde kalmalıdır. Örnek bir e-kitap uygulamasını inceleyelim. Kitap ve ödemeyle ilgili kodları aynı modülde birlikte kullanmak uygun olmayabilir. Çünkü bunlar iki farklı işlevsel alanlardır.

Modül türleri

Modüllerinizi düzenleme şekliniz temel olarak uygulama mimarinize bağlıdır. Aşağıda, önerilen uygulama mimarimize uygun olarak uygulamanıza ekleyebileceğiniz bazı yaygın modül türleri verilmiştir.

Veri modülleri

Veri modülü genellikle bir depo, veri kaynakları ve model sınıfları içerir. Veri modülünün üç temel sorumluluğu vardır:

  1. Belirli bir alanın tüm verilerini ve iş mantığını kapsar: Her veri modülü, belirli bir alanı temsil eden verilerin işlenmesinden sorumlu olmalıdır. İlgili olduğu sürece birçok veri türünü işleyebilir.
  2. Depoyu harici bir API olarak kullanıma sunma: Bir veri modülünün herkese açık API'si, verileri uygulamanın geri kalanına sunmaktan sorumlu oldukları için bir depo olmalıdır.
  3. Tüm uygulama ayrıntılarını ve veri kaynaklarını dışarıdan gizleyin: Veri kaynaklarına yalnızca aynı modüldeki depolar tarafından erişilebilir. Dışarıdan gizli kalırlar. Bu durumu, Kotlin'in private veya internal görünürlük anahtar kelimesini kullanarak zorunlu kılabilirsiniz.
Şekil 1. Örnek veri modülleri ve içerikleri.

Özellik modülleri

Özellik, bir uygulamanın işlevselliğinin izole edilmiş bir parçasıdır ve genellikle bir ekrana veya yakından ilişkili bir dizi ekrana (ör. kayıt veya ödeme akışı) karşılık gelir. Uygulamanızda alt çubuk gezinme özelliği varsa her hedef büyük olasılıkla bir özelliktir.

Şekil 2. Bu uygulamanın her sekmesi bir özellik olarak tanımlanabilir.

Özellikler, uygulamanızdaki ekranlarla veya hedeflerle ilişkilendirilir. Bu nedenle, ilişkili bir kullanıcı arayüzü ve ViewModel mantıklarını ve durumlarını yönetmek için bir kullanıcı arayüzü olması muhtemeldir. Tek bir özellik, tek bir görünüm veya gezinme hedefiyle sınırlı olmak zorunda değildir. Özellik modülleri, veri modüllerine bağlıdır.

Şekil 3. Örnek özellik modülleri ve içerikleri.

Uygulama modülleri

Uygulama modülleri, uygulamaya giriş noktasıdır. Özellik modüllerine bağlıdırlar ve genellikle kök gezinme sağlarlar. Tek bir uygulama modülü, derleme varyantları sayesinde çeşitli ikili programlara derlenebilir.

4. Şekil *Demo* ve *Full* ürün çeşidi modüllerinin bağımlılık grafiği.

Uygulamanız Android Auto, Wear veya TV gibi birden fazla cihaz türünü hedefliyorsa her biri için bir uygulama modülü tanımlayın. Bu, platforma özgü bağımlılıkların ayrılmasına yardımcı olur.

5. Şekil. Android Auto uygulama bağımlılığı grafiği.

Sık kullanılan modüller

Çekirdek modüller olarak da bilinen ortak modüller, diğer modüllerin sık kullandığı kodu içerir. Tekrarı azaltır ve uygulamanın mimarisinde belirli bir katmanı temsil etmezler. Yaygın modül örneklerini aşağıda bulabilirsiniz:

  • Kullanıcı arayüzü modülü: Uygulamanızda özel kullanıcı arayüzü öğeleri veya ayrıntılı markalama kullanıyorsanız tüm özelliklerin yeniden kullanılması için widget koleksiyonunuzu bir modül içinde kapsüllemeyi düşünebilirsiniz. Bu, kullanıcı arayüzünüzün farklı özelliklerde tutarlı olmasına yardımcı olabilir. Örneğin, temalandırmanız merkeziyse yeniden markalama yapıldığında zorlu bir yeniden düzenleme işleminden kaçınabilirsiniz.
  • Analytics modülü: İzleme genellikle yazılım mimarisiyle ilgili çok az değerlendirme yapılarak iş gereksinimlerine göre belirlenir. Analytics izleyicileri genellikle birçok alakasız bileşende kullanılır. Bu durum sizin için geçerliyse özel bir analiz modülü kullanmanız iyi bir fikir olabilir.
  • Ağ modülü: Birçok modülün ağ bağlantısı gerektirdiği durumlarda, HTTP istemcisi sağlamaya ayrılmış bir modül kullanabilirsiniz. Bu özellik, özellikle istemciniz özel yapılandırma gerektirdiğinde kullanışlıdır.
  • Yardımcı program modülü: Yardımcılar olarak da bilinen yardımcı programlar, genellikle uygulama genelinde yeniden kullanılan küçük kod parçalarıdır. Yardımcı test araçları, para birimi biçimlendirme işlevi, e-posta doğrulayıcı veya özel operatör gibi araçlar, yardımcı programlara örnek olarak verilebilir.

Test modülleri

Test modülleri yalnızca test amacıyla kullanılan Android modülleridir. Modüller, yalnızca testleri çalıştırmak için gereken ve uygulamanın çalışma zamanında ihtiyaç duyulmayan test kodu, test kaynakları ve test bağımlılıkları içerir. Test modülleri, teste özel kodu ana uygulamadan ayırmak için oluşturulur. Bu sayede modül kodu daha kolay yönetilir ve korunur.

Test modüllerinin kullanım alanları

Aşağıdaki örneklerde, test modüllerinin uygulanmasının özellikle faydalı olabileceği durumlar gösterilmektedir:

  • Paylaşılan test kodu: Projenizde birden fazla modül varsa ve test kodunun bir kısmı birden fazla modül için geçerliyse kodu paylaşmak üzere bir test modülü oluşturabilirsiniz. Bu, kodu tekrarlama sorununu azaltmaya ve test kodunuzun bakımını kolaylaştırmaya yardımcı olabilir. Paylaşılan test kodu, özel onaylar veya eşleştiriciler gibi yardımcı sınıfları ya da işlevlerin yanı sıra simüle edilmiş JSON yanıtları gibi test verilerini içerebilir.

  • Daha Temiz Derleme Yapılandırmaları: Test modülleri, kendi build.gradle dosyalarına sahip olabildikleri için daha temiz derleme yapılandırmaları oluşturmanıza olanak tanır. Uygulama modülünüzün build.gradle dosyasını yalnızca testlerle ilgili yapılandırmalarla doldurmanız gerekmez.

  • Entegrasyon Testleri: Test modülleri, kullanıcı arayüzü, iş mantığı, ağ istekleri ve veritabanı sorguları dahil olmak üzere uygulamanızın farklı bölümleri arasındaki etkileşimleri test etmek için kullanılan entegrasyon testlerini depolamak için kullanılabilir.

  • Büyük ölçekli uygulamalar: Test modülleri, özellikle karmaşık kod tabanları ve birden fazla modülü olan büyük ölçekli uygulamalar için kullanışlıdır. Bu gibi durumlarda, test modülleri kod düzenini ve sürdürülebilirliğini iyileştirmeye yardımcı olabilir.

Şekil 6. Test modülleri, aksi takdirde birbirine bağımlı olacak modülleri izole etmek için kullanılabilir.

Modüller arası iletişim

Modüller nadiren tamamen ayrı olarak bulunur ve genellikle diğer modüllere bağlıdır ve onlarla iletişim kurar. Modüller birlikte çalışıp sık sık bilgi alışverişinde bulunduğunda bile bağlantıyı düşük tutmak önemlidir. Bazen iki modül arasındaki doğrudan iletişim, mimari kısıtlamalar gibi durumlarda istenmez. Ayrıca, döngüsel bağımlılıklar gibi durumlarda bu işlem mümkün olmayabilir.

Şekil 7. Döngüsel bağımlılıklar nedeniyle modüller arasında doğrudan ve iki yönlü iletişim kurmak mümkün değildir. Diğer iki bağımsız modül arasındaki veri akışını koordine etmek için bir aracı modül gerekir.

Bu sorunu aşmak için diğer iki modül arasında uyumlulaştırma yapan üçüncü bir modül kullanabilirsiniz. Aracı modül, her iki modülden gelen mesajları dinleyebilir ve gerektiğinde iletebilir. Örnek uygulamamızda, etkinlik farklı bir özelliğin parçası olan ayrı bir ekranda başlamış olsa bile ödeme ekranının hangi kitabın satın alınacağını bilmesi gerekir. Bu durumda, aracı, gezinme grafiğinin sahibi olan modüldür (genellikle bir uygulama modülü). Örnekte, Navigation bileşenini kullanarak verileri ana sayfa özelliğinden ödeme özelliğine aktarmak için gezinme özelliğini kullanıyoruz.

navController.navigate("checkout/$bookId")

Ödeme hedefi, kitapla ilgili bilgileri getirmek için kullandığı bir kitap kimliğini bağımsız değişken olarak alır. Hedef özelliğinin ViewModel içinde gezinme bağımsız değişkenlerini almak için kaydedilmiş durum işleyicisini kullanabilirsiniz.

class CheckoutViewModel(savedStateHandle: SavedStateHandle, ) : ViewModel() {

   val uiState: StateFlow<CheckoutUiState> =
      savedStateHandle.getStateFlow<String>("bookId", "").map { bookId ->
          // produce UI state calling bookRepository.getBook(bookId)
      }
      
}

Nesneleri gezinme bağımsız değişkeni olarak iletmemelisiniz. Bunun yerine, özelliklerin veri katmanından istenen kaynaklara erişmek ve bunları yüklemek için kullanabileceği basit kimlikler kullanın. Bu şekilde, bağlantıyı düşük tutar ve tek ve doğru kaynak ilkesini ihlal etmezsiniz.

Aşağıdaki örnekte, her iki özellik modülü de aynı veri modülüne bağlıdır. Bu sayede, aracı modülünün iletmesi gereken veri miktarı en aza indirilebilir ve modüller arasındaki bağlantı zayıf tutulabilir. Modüller, nesneleri iletmek yerine temel kimlikleri değiştirmeli ve kaynakları paylaşılan bir veri modülünden yüklemelidir.

Şekil 8. Paylaşılan bir veri modülüne dayanan iki özellik modülü.

Bağımlılık ters çevirme

Bağımlılık ters çevirme, kodunuzu soyutlamanın somut bir uygulamadan ayrı olacak şekilde düzenlemenizdir.

  • Soyutlama: Uygulamanızdaki bileşenlerin veya modüllerin birbirleriyle nasıl etkileşimde bulunacağını tanımlayan bir sözleşme. Soyutlama modülleri, sisteminizin API'sini tanımlar ve arayüzler ile modeller içerir.
  • Somut uygulama: Soyutlama modülüne bağlı olan ve soyutlamanın davranışını uygulayan modüller.

Soyutlama modülünde tanımlanan davranışa dayanan modüller, belirli uygulamalara değil yalnızca soyutlamaya bağlı olmalıdır.

Şekil 9. Doğrudan alt düzey modüllere bağlı olan üst düzey modüller yerine, üst düzey ve uygulama modülleri soyutlama modülüne bağlıdır.

Örnek

Çalışmak için veritabanı gerektiren bir özellik modülü olduğunu düşünün. Özellik modülü, veritabanının nasıl uygulandığıyla (yerel bir Room veritabanı veya uzak bir Firestore örneği) ilgilenmez. Yalnızca uygulama verilerini depolaması ve okuması gerekir.

Bu amaca ulaşmak için özellik modülü, belirli bir veritabanı uygulamasına değil, soyutlama modülüne bağlıdır. Bu soyutlama, uygulamanın veritabanı API'sini tanımlar. Başka bir deyişle, veritabanıyla nasıl etkileşime girileceğine dair kuralları belirler. Bu sayede özellik modülü, temel uygulama ayrıntılarını bilmesine gerek kalmadan herhangi bir veritabanını kullanabilir.

Somut uygulama modülü, soyutlama modülünde tanımlanan API'lerin gerçek uygulamasını sağlar. Bunu yapabilmek için uygulama modülü de soyutlama modülüne bağlıdır.

Bağımlılık ekleme

Bu noktada, özellik modülünün uygulama modülüne nasıl bağlandığını merak ediyor olabilirsiniz. Yanıt bağımlılık ekleme'dir. Özellik modülü, gerekli veritabanı örneğini doğrudan oluşturmaz. Bunun yerine, hangi bağımlılıklara ihtiyaç duyduğunu belirtir. Bu bağımlılıklar daha sonra genellikle uygulama modülünde harici olarak sağlanır.

releaseImplementation(project(":database:impl:firestore"))

debugImplementation(project(":database:impl:room"))

androidTestImplementation(project(":database:impl:mock"))

Avantajları

API'lerinizi ve uygulamalarını ayırmanın avantajları şunlardır:

  • Değiştirilebilirlik: API ve uygulama modüllerinin net bir şekilde ayrılması sayesinde aynı API için birden fazla uygulama geliştirebilir ve API'yi kullanan kodu değiştirmeden bunlar arasında geçiş yapabilirsiniz. Bu özellik, özellikle farklı bağlamlarda farklı özellikler veya davranışlar sunmak istediğiniz senaryolarda faydalı olabilir. Örneğin, test için sahte bir uygulama ile üretim için gerçek bir uygulama.
  • Bağlantıyı kaldırma: Ayrım, soyutlamaları kullanan modüllerin belirli bir teknolojiye bağlı olmadığı anlamına gelir. Veritabanınızı daha sonra Room'dan Firestore'a değiştirmeyi seçerseniz değişiklikler yalnızca işi yapan belirli bir modülde (uygulama modülü) gerçekleşeceğinden ve veritabanınızın API'sini kullanan diğer modülleri etkilemeyeceğinden bu işlem daha kolay olur.
  • Test edilebilirlik: API'lerin uygulamalarından ayrılması, test etmeyi büyük ölçüde kolaylaştırabilir. API sözleşmelerine karşı test durumları yazabilirsiniz. Ayrıca, sahte uygulamalar da dahil olmak üzere çeşitli senaryoları ve uç durumları test etmek için farklı uygulamalar kullanabilirsiniz.
  • Daha iyi derleme performansı: Bir API'yi ve uygulamasını farklı modüllere ayırdığınızda, uygulama modülündeki değişiklikler derleme sistemini API modülüne bağlı modülleri yeniden derlemeye zorlamaz. Bu, özellikle derleme sürelerinin önemli olabileceği büyük projelerde daha hızlı derleme sürelerine ve daha yüksek verimliliğe yol açar.

Ne zaman ayırmalısınız?

Aşağıdaki durumlarda API'lerinizi uygulamalarından ayırmak faydalıdır:

  • Çeşitli özellikler: Sisteminizi birden fazla şekilde uygulayabiliyorsanız net bir API, farklı uygulamaların birbirinin yerine kullanılabilmesini sağlar. Örneğin, OpenGL veya Vulkan kullanan bir oluşturma sisteminiz ya da Play veya şirket içi faturalandırma API'nizle çalışan bir faturalandırma sisteminiz olabilir.
  • Birden fazla uygulama: Farklı platformlar için ortak özelliklere sahip birden fazla uygulama geliştiriyorsanız ortak API'ler tanımlayabilir ve platforma özel uygulamalar geliştirebilirsiniz.
  • Bağımsız ekipler: Ayrım, farklı geliştiricilerin veya ekiplerin kod tabanının farklı bölümleri üzerinde aynı anda çalışmasına olanak tanır. Geliştiriciler, API sözleşmelerini anlamaya ve bunları doğru şekilde kullanmaya odaklanmalıdır. Diğer modüllerin uygulama ayrıntılarıyla ilgilenmeleri gerekmez.
  • Büyük kod tabanı: Kod tabanı büyük veya karmaşık olduğunda API'yi uygulamadan ayırmak kodu daha yönetilebilir hale getirir. Bu sayede kod tabanını daha ayrıntılı, anlaşılır ve bakımı kolay birimlere ayırabilirsiniz.

Nasıl uygulanır?

Bağımlılık ters çevirmeyi uygulamak için aşağıdaki adımları uygulayın:

  1. Soyutlama modülü oluşturun: Bu modül, özelliğinizin davranışını tanımlayan API'ler (arayüzler ve modeller) içermelidir.
  2. Uygulama modülleri oluşturun: Uygulama modülleri, API modülünü temel almalı ve bir soyutlamanın davranışını uygulamalıdır.
    Doğrudan düşük seviyeli modüllere bağlı olan yüksek seviyeli modüller yerine, yüksek seviyeli ve uygulama modülleri soyutlama modülüne bağlıdır.
    Şekil 10. Uygulama modülleri, soyutlama modülüne bağlıdır.
  3. Üst düzey modülleri soyutlama modüllerine bağımlı hale getirin: Modüllerinizi belirli bir uygulamaya doğrudan bağımlı hale getirmek yerine soyutlama modüllerine bağımlı hale getirin. Üst düzey modüllerin uygulama ayrıntılarını bilmesi gerekmez, yalnızca sözleşmeye (API) ihtiyacı vardır.
    Üst düzey modüller, uygulamaya değil soyutlamalara bağlıdır.
    Şekil 11. Üst düzey modüller, uygulamaya değil soyutlamalara bağlıdır.
  4. Uygulama modülü sağlama: Son olarak, bağımlılıklarınız için gerçek uygulamayı sağlamanız gerekir. Özel uygulama, proje kurulumunuza bağlıdır ancak genellikle uygulama modülü bu işlem için iyi bir yerdir. Uygulamayı sağlamak için seçtiğiniz derleme varyantı veya test kaynağı grubu için bağımlılık olarak belirtin.
    Uygulama modülü, gerçek uygulamayı sağlar.
    Şekil 12. Uygulama modülü, gerçek uygulamayı sağlar.

Genel en iyi uygulamalar

Başlangıçta da belirtildiği gibi, çok modüllü bir uygulama geliştirmenin tek bir doğru yolu yoktur. Birçok yazılım mimarisi olduğu gibi, bir uygulamayı modüler hale getirmenin de sayısız yolu vardır. Bununla birlikte, aşağıdaki genel öneriler kodunuzu daha okunabilir, sürdürülebilir ve test edilebilir hale getirmenize yardımcı olabilir.

Yapılandırmanızı tutarlı tutma

Her modül, yapılandırma ek yükü getirir. Modüllerinizin sayısı belirli bir eşiğe ulaşırsa tutarlı yapılandırmayı yönetmek zorlaşır. Örneğin, modüllerin aynı sürümdeki bağımlılıkları kullanması önemlidir. Yalnızca bir bağımlılık sürümünü yükseltmek için çok sayıda modülü güncellemeniz gerekiyorsa bu hem zahmetli bir iş hem de olası hatalara yol açabilir. Bu sorunu çözmek için yapılandırmanızı merkezileştirmek üzere Gradle'ın araçlarından birini kullanabilirsiniz:

  • Sürüm katalogları, senkronizasyon sırasında Gradle tarafından oluşturulan, tür açısından güvenli bir bağımlılık listesidir. Tüm bağımlılıklarınızı bildirebileceğiniz merkezi bir yerdir ve bir projedeki tüm modüller tarafından kullanılabilir.
  • Modüller arasında derleme mantığını paylaşmak için kural eklentilerini kullanın.

Mümkün olduğunca az bilgi paylaşın

Bir modülün herkese açık arayüzü minimum düzeyde olmalı ve yalnızca temel bilgileri göstermelidir. Dışarıya uygulama ayrıntıları sızdırmamalıdır. Her şeyi mümkün olduğunca küçük bir kapsamda tutun. Bildirimleri modüle özel hale getirmek için Kotlin'in private veya internal görünürlük kapsamını kullanın. Modülünüzde bağımlılıkları bildirirken api yerine implementation kullanın. İkincisi, modülünüzün tüketicilerine geçişli bağımlılıkları gösterir. Uygulama, yeniden oluşturulması gereken modül sayısını azalttığı için derleme süresini kısaltabilir.

Kotlin ve Java modüllerini tercih etme

Android Studio'nun desteklediği üç temel modül türü vardır:

  • Uygulama modülleri, uygulamanıza giriş noktasıdır. Kaynak kodu, kaynaklar, öğeler ve AndroidManifest.xml içerebilirler. Uygulama modülünün çıktısı bir Android App Bundle (AAB) veya Android uygulama paketi (APK) olur.
  • Kitaplık modülleri, uygulama modülleriyle aynı içeriğe sahiptir. Diğer Android modülleri tarafından bağımlılık olarak kullanılır. Kitaplık modülünün çıkışı olan Android Archive (AAR), yapısal olarak uygulama modüllerine benzer ancak daha sonra diğer modüller tarafından bağımlılık olarak kullanılabilen bir Android Archive (AAR) dosyası olarak derlenir. Kitaplık modülü, aynı mantık ve kaynakların birçok uygulama modülünde kapsüllenip yeniden kullanılmasını sağlar.
  • Kotlin ve Java kitaplıkları Android kaynakları, öğeleri veya manifest dosyaları içermez.

Android modülleri ek yükle geldiğinden mümkün olduğunca Kotlin veya Java türünü kullanmanız önerilir.