Arsitektur multi-modul Android. A hingga Z

Halo semuanya!

Belum lama ini, kami menyadari bahwa aplikasi mobile bukan hanya thin client, tetapi sejumlah besar logika yang sangat berbeda yang perlu dirampingkan. Itulah sebabnya kami terinspirasi oleh ide-ide arsitektur Bersih, merasakan apa itu DI, belajar cara menggunakan Belati 2, dan sekarang dengan mata tertutup kami dapat memecah fitur apa pun menjadi berlapis-lapis.

Tetapi dunia tidak tinggal diam, dan dengan solusi dari masalah lama yang baru datang. Dan nama dari masalah baru ini adalah monomodularitas. Anda biasanya mencari tahu tentang masalah ini ketika waktu perakitan terbang ke angkasa. Itulah tepatnya berapa banyak laporan tentang transisi ke multimodularitas ( satu , dua ) dimulai.
Tetapi untuk beberapa alasan, semua orang pada saat yang sama entah bagaimana lupa bahwa monomodularitas sangat mempengaruhi tidak hanya waktu perakitan, tetapi juga arsitektur Anda. Di sini jawab pertanyaannya. Seberapa besar AppComponent Anda? Apakah Anda melihat secara berkala dalam kode yang fitur A karena suatu alasan menarik repositori fitur B, meskipun seharusnya tidak seperti ini, yah, atau apakah itu entah bagaimana menjadi lebih top-level? Apakah fitur memiliki kontrak apa pun? Dan bagaimana Anda mengatur komunikasi antar fitur? Apakah ada aturannya?
Anda merasa bahwa kami telah menyelesaikan masalah dengan lapisan, yaitu, secara vertikal semuanya tampak baik-baik saja, tetapi secara horizontal ada yang salah? Dan hanya menghancurkan paket-paket dan mengendalikan review tidak menyelesaikan masalah.

Dan pertanyaan keamanan untuk yang lebih berpengalaman. Ketika Anda pindah ke multi-modularitas, bukankah Anda harus menyekop setengah dari aplikasi, selalu menyeret kode dari satu modul ke modul lainnya dan hidup dengan proyek non-rakitan untuk jangka waktu yang layak?

Dalam artikel saya, saya ingin memberi tahu Anda bagaimana saya sampai pada multimodularitas tepatnya dari sudut pandang arsitektur. Masalah apa yang mengganggu saya, dan bagaimana saya mencoba menyelesaikannya secara bertahap. Dan pada akhirnya, Anda akan menemukan algoritma untuk beralih dari monomodularity ke multimodularity tanpa air mata dan rasa sakit.

Menjawab pertanyaan pertama, seberapa besar AppComponent itu, saya bisa akui - besar, sangat besar. Dan itu terus menerus menyiksaku. Bagaimana itu bisa terjadi? Pertama-tama, ini disebabkan oleh organisasi DI seperti itu. Dengan DI kita akan mulai.

Seperti yang saya lakukan sebelumnya


Saya pikir banyak orang telah terbentuk di kepala mereka sesuatu seperti diagram ini dari dependensi komponen dan cakupan yang sesuai:


Apa yang kita punya di sini


AppComponent , yang menyerap sepenuhnya semua dependensi dengan cakupan Singleton . Saya pikir hampir semua orang memiliki komponen ini.

Komponen Fitur . Setiap fitur dengan cakupannya sendiri dan merupakan subkomponen dari AppComponent atau fitur senior.
Mari kita sedikit membahas fitur-fiturnya. Pertama-tama, apa itu fitur? Saya akan mencoba dengan kata-kata saya sendiri. Sebuah fitur adalah modul program independen independen maksimum, maksimum yang memecahkan masalah pengguna tertentu, dengan ketergantungan eksternal yang jelas, dan yang relatif mudah digunakan lagi di program lain. Fitur bisa besar dan kecil. Fitur dapat berisi fitur lainnya. Dan mereka juga dapat menggunakan atau menjalankan fitur lain melalui dependensi eksternal yang jelas. Jika kita mengambil aplikasi kita (Kaspersky Internet Security untuk Android), maka fitur dapat dianggap Anti-Virus, Anti-Pencurian, dll.

Komponen Layar . Komponen untuk layar tertentu, juga dengan ruang lingkupnya sendiri dan juga menjadi subkomponen dari komponen fitur yang sesuai.

Sekarang daftar "mengapa begitu"


Mengapa subkomponen?
Dalam dependensi komponen, saya tidak suka fakta bahwa komponen dapat bergantung pada beberapa komponen sekaligus, yang, menurut saya, pada akhirnya dapat menyebabkan kekacauan komponen dan ketergantungannya. Ketika Anda memiliki hubungan satu-ke-banyak yang ketat (komponen dan subkomponennya), maka itu lebih aman dan lebih jelas. Selain itu, secara default, semua dependensi induk tersedia untuk subkomponen, yang juga lebih nyaman.

Mengapa ada ruang untuk setiap fitur?
Karena kemudian saya melanjutkan dari pertimbangan bahwa setiap fitur adalah semacam siklus hidupnya sendiri, yang tidak sama dengan yang lain, jadi logis untuk membuat ruang lingkup Anda sendiri. Ada satu hal lagi untuk banyak hal pelit, yang akan saya sebutkan di bawah ini.

Karena kita berbicara tentang Belati 2 dalam konteks Bersih, saya juga akan menyebutkan momen bagaimana dependensi disampaikan. Penyaji, Interaktor, Repositori, dan kelas dependensi bantu lainnya dipasok melalui konstruktor. Dalam tes, kami kemudian mengganti bertopik atau moki melalui konstruktor dan diam-diam menguji kelas kami.
Penutupan grafik ketergantungan biasanya terjadi dalam aktivitas, fragmen, kadang-kadang penerima dan layanan, secara umum, di tempat-tempat root dari mana android dapat memulai sesuatu. Situasi klasik adalah ketika suatu aktivitas dibuat untuk fitur, komponen fitur mulai dan hidup dalam aktivitas, dan dalam fitur itu sendiri ada tiga layar yang diimplementasikan dalam tiga fragmen.

Jadi, semuanya tampak logis. Tapi seperti biasa, hidup membuat penyesuaiannya sendiri.

Masalah hidup


Contoh tugas


Mari kita lihat contoh sederhana dari aplikasi kita. Kami memiliki fitur Pemindai dan fitur Antipencurian. Kedua fitur memiliki tombol Beli yang berharga. Selain itu, “Membeli” tidak hanya mengirim permintaan, tetapi juga banyak logika berbeda terkait dengan proses pembelian. Ini murni logika bisnis dengan beberapa dialog untuk pembelian segera. Artinya, ada fitur yang cukup terpisah untuk dirinya sendiri - Pembelian. Jadi, dalam dua fitur kita perlu menggunakan fitur ketiga.
Dari sudut pandang ui dan navigasi, kami memiliki gambar berikut. Layar utama dimulai, di mana dua tombol:


Dengan mengklik tombol-tombol ini kita mendapatkan fitur Pemindai atau Anti-Pencurian.
Pertimbangkan fitur Pemindai:


Dengan mengeklik "Mulai pemindaian antivirus", beberapa jenis pekerjaan pemindaian dilakukan, dengan mengeklik "Beli saya", kami hanya ingin membeli, yaitu, kami menarik fitur Pembelian, tetapi dengan "Bantuan" kami membuka layar sederhana dengan bantuan.
Fitur Anti-Theft terlihat hampir sama.

Solusi potensial


Bagaimana kita menerapkan contoh ini dalam hal DI? Ada beberapa opsi.

Opsi pertama


Pilih fitur pembelian sebagai komponen independen yang hanya bergantung pada AppComponent .


Tetapi kemudian kita dihadapkan dengan masalah: bagaimana cara menyuntikkan dependensi dari dua grafik (komponen) yang berbeda ke dalam satu kelas sekaligus? Hanya melalui kruk yang kotor, yang tentu saja merupakan hal semacam itu.

Opsi kedua


Kami memilih fitur pembelian di subkomponen, yang tergantung pada AppComponent. Dan komponen Scanner dan Anti-Theft dapat dibuat subkomponen dari komponen Purchase.


Tetapi, seperti yang Anda pahami, mungkin ada banyak situasi serupa dalam aplikasi. Dan ini berarti bahwa kedalaman dependensi komponen dapat benar-benar besar dan kompleks. Dan grafik seperti itu akan lebih membingungkan daripada membuat aplikasi Anda lebih koheren dan mudah dipahami.

Opsi ketiga


Kami memilih fitur pembelian bukan dalam komponen terpisah, tetapi dalam modul Belati terpisah . Dua cara dimungkinkan lebih lanjut.

Cara pertama
Mari menambahkan fitur Singleton scopes ke semua dependensi dan sambungkan ke AppComponent .


Opsi ini populer, tetapi menyebabkan AppComponent kembung. Akibatnya, ia mengembang dalam ukuran, berisi semua kelas aplikasi, dan seluruh titik menggunakan Belati turun ke pengiriman dependensi yang lebih nyaman ke kelas - melalui bidang atau konstruktor, dan bukan melalui singleton. Pada prinsipnya, ini DI, tapi kami kehilangan poin arsitektur, dan ternyata semua orang tahu tentang semua orang.
Secara umum, di awal jalur, jika Anda tidak tahu di mana atribut atribut kelas untuk fitur yang mana, lebih mudah untuk membuatnya global. Ini cukup umum ketika Anda bekerja dengan Legacy dan mencoba menghadirkan setidaknya beberapa jenis arsitektur, ditambah lagi Anda belum mengetahui semua kode dengan baik. Dan di sana, memang, mata terbelalak, dan tindakan ini dibenarkan. Kesalahannya adalah ketika semuanya lebih atau kurang menjulang, tidak ada yang mau menangani AppComponent ini.

Cara kedua
Ini adalah pengurangan semua fitur ke cakupan tunggal, misalnya PerFeature .


Kemudian kita dapat menghubungkan modul Belati Belanja ke komponen yang diperlukan dengan mudah dan sederhana.
Tampaknya nyaman. Namun secara arsitektur ternyata tidak terpisah. Fitur-fitur dari Scanner dan Anti-Theft tahu betul segala sesuatu tentang fitur Purchase, semuanya jeroan. Secara tidak sengaja, sesuatu mungkin terlibat. Artinya, fitur Pembelian tidak memiliki API yang jelas, batas antara fitur buram, tidak ada kontrak yang jelas. Ini buruk. Nah, dalam multi-modular, gredloid akan sulit nantinya.

Nyeri arsitektur


Sejujurnya, untuk waktu yang lama saya menggunakan opsi ketiga. Cara pertama . Ini adalah langkah yang perlu ketika kami mulai secara bertahap mentransfer warisan kami ke rel normal. Tetapi, seperti yang saya sebutkan, dengan pendekatan ini, fitur Anda mulai sedikit bercampur. Setiap orang dapat mengetahui tentang masing-masing, tentang detail implementasi dan ini untuk semua orang. Dan kembungnya AppComponent dengan jelas mengindikasikan bahwa sesuatu harus dilakukan.
Kebetulan, opsi ketiga akan membantu dengan pembongkaran AppComponent . Cara kedua . Tetapi pengetahuan tentang implementasi dan pencampuran fitur tidak akan pergi ke mana pun. Yah, tentu saja, menggunakan kembali fitur di antara aplikasi akan sangat sulit.

Kesimpulan menengah


Jadi, apa yang kita inginkan pada akhirnya? Masalah apa yang ingin kita pecahkan? Mari langsung ke intinya, mulai dari DI dan beralih ke arsitektur:

  • Mekanisme DI yang nyaman yang memungkinkan Anda menggunakan fitur dalam fitur lain (dalam contoh kami, kami ingin menggunakan fitur Belanja dalam Pemindai dan Anti-Pencurian), tanpa kruk dan kesakitan.
  • AppComponent tertipis.
  • Fitur tidak boleh menyadari implementasi fitur lainnya.
  • Fitur tidak boleh diakses secara default kepada siapa pun, saya ingin memiliki semacam mekanisme kontrol yang ketat.
  • Dimungkinkan untuk memberikan fitur ke aplikasi lain dengan jumlah gerakan minimum.
  • Transisi logis ke multi-modularitas dan praktik terbaik untuk transisi ini.

Saya secara khusus berbicara tentang multi-modularitas hanya di bagian paling akhir. Kami akan menghubunginya, kami tidak akan maju.

”Hidup dengan cara baru"


Sekarang kita akan mencoba untuk mengimplementasikan Daftar Keinginan di atas secara bertahap.
Ayo pergi!

Peningkatan DI


Mari kita mulai dengan DI yang sama.

Penolakan sejumlah besar cakupan


Seperti yang saya tulis di atas, sebelum pendekatan saya adalah ini: untuk setiap fitur cakupannya sendiri. Padahal, tidak ada keuntungan khusus dari ini. Hanya mendapatkan sejumlah besar cakupan dan sakit kepala dalam jumlah tertentu.
Rantai ini cukup: Singleton - PerFeature - PerScreen .

Pengabaian Subkomponen yang mendukung dependensi Komponen


Sudah poin yang lebih menarik. Dengan Subkomponen, Anda tampaknya memiliki hierarki yang lebih ketat, tetapi pada saat yang sama Anda memiliki tangan yang sepenuhnya terikat dan tidak ada cara untuk melakukan manuver. Selain itu, AppComponent tahu tentang semua fitur, dan Anda juga mendapatkan kelas DaggerAppComponent yang sangat besar.
Dengan dependensi Komponen, Anda mendapatkan satu keuntungan yang sangat keren. Dalam dependensi komponen, Anda dapat menentukan bukan komponen, tetapi membersihkan antarmuka (terima kasih kepada Denis dan Volodya). Berkat ini, Anda dapat mengganti implementasi antarmuka yang Anda suka, Belati akan memakan semuanya. Bahkan jika komponen dengan cakupan yang sama adalah implementasi ini:
@Component( dependencies = FeatureDependencies.class, modules = FeatureModule.class ) @PerFeature public abstract class FeatureComponent { // ... } public interface FeatureDependencies { SomeDependency someDependency(); } @Component( modules = AnotherFeatureModule.class ) @PerFeature public abstract class AnotherFeatureComponent implements FeatureDependencies { // ... } 


Dari Peningkatan DI ke Peningkatan Arsitektur


Mari kita ulangi definisi fitur. Sebuah fitur adalah modul program independen independen maksimum, maksimum yang memecahkan masalah pengguna tertentu, dengan ketergantungan eksternal yang jelas, dan yang relatif mudah digunakan kembali dalam program lain. Salah satu ekspresi kunci dalam definisi fitur adalah "dengan ketergantungan eksternal yang jelas". Karena itu, mari kita gambarkan semua yang kita inginkan dari dunia luar untuk fitur, kami akan menjelaskan dalam antarmuka khusus.
Di sini, katakanlah, antarmuka ketergantungan eksternal dari fitur Belanja:
 public interface PurchaseFeatureDependencies { HttpClientApi httpClient(); } 

Atau antarmuka ketergantungan eksternal dari fitur Pemindai:
 public interface ScannerFeatureDependencies { DbClientApi dbClient(); HttpClientApi httpClient(); SomeUtils someUtils(); //       PurchaseInteractor purchaseInteractor(); } 

Seperti yang telah disebutkan di bagian DI, dependensi dapat diimplementasikan oleh siapa saja dan sesuka Anda, ini adalah antarmuka murni, dan fitur kami dibebaskan dari pengetahuan ekstra ini.

Komponen penting lain dari fitur "murni" adalah keberadaan api yang jelas, dimana dunia luar dapat mengakses fitur tersebut.
Berikut adalah fitur api Belanja:
 public interface PurchaseFeatureApi { PurchaseInteractor purchaseInteractor(); } 

Artinya, dunia luar bisa mendapatkan PurchaseInteractor dan mencoba melakukan pembelian melaluinya. Sebenarnya, di atas kami melihat bahwa Pemindai membutuhkan PurchaseInteractor untuk menyelesaikan pembelian.

Dan berikut ini adalah fitur api dari Scanner:
 public interface ScannerFeatureApi { ScannerStarter scannerStarter(); } 

Dan segera saya membawa antarmuka dan implementasi ScannerStarter :
 public interface ScannerStarter { void start(Context context); } @PerFeature public class ScannerStarterImpl implements ScannerStarter { @Inject public ScannerStarterImpl() { } @Override public void start(Context context) { Class<?> cls = ScannerActivity.class; Intent intent = new Intent(context, cls); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } } 

Lebih menarik di sini. Faktanya adalah bahwa Scanner dan Anti-Theft adalah fitur yang cukup tertutup dan terisolasi. Dalam contoh saya, fitur-fitur ini diluncurkan pada Aktivitas terpisah, dengan navigasi sendiri, dll. Yaitu, cukup bagi kami untuk memulai Aktivitas di sini. Aktivitas mati - fitur mati. Anda dapat bekerja pada prinsip "Aktivitas Tunggal", dan kemudian melalui fitur api melewati, katakanlah, FragmentManager dan beberapa panggilan balik yang melaluinya fitur melaporkan bahwa ia telah selesai. Ada banyak variasi.
Kami juga dapat mengatakan bahwa kami memiliki hak untuk mempertimbangkan fitur-fitur seperti Scanner dan Anti-Theft sebagai aplikasi independen. Berbeda dengan fitur Pembelian, yang merupakan fitur tambahan untuk sesuatu dan dengan sendirinya, itu entah bagaimana tidak ada. Ya, itu independen, tetapi merupakan pelengkap logis untuk fitur lainnya.

Seperti yang dapat Anda bayangkan, harus ada titik yang menghubungkan fitur, implementasinya, dan fitur ketergantungan yang diperlukan. Poin ini adalah komponen Belati.
Contoh komponen fitur Pemindai:
 @Component(modules = { ScannerFeatureModule.class, ScreenNavigationModule.class // ScannerFeatureDependencies - api    }, dependencies = ScannerFeatureDependencies.class) @PerFeature // ScannerFeatureApi - api   public abstract class ScannerFeatureComponent implements ScannerFeatureApi { private static volatile ScannerFeatureComponent sScannerFeatureComponent; //   public static ScannerFeatureApi initAndGet( ScannerFeatureDependencies scannerFeatureDependencies) { if (sScannerFeatureComponent == null) { synchronized (ScannerFeatureComponent.class) { if (sScannerFeatureComponent == null) { sScannerFeatureComponent = DaggerScannerFeatureComponent.builder() .scannerFeatureDependencies(scannerFeatureDependencies) .build(); } } } return sScannerFeatureComponent; } //           public static ScannerFeatureComponent get() { if (sScannerFeatureComponent == null) { throw new RuntimeException( "You must call 'initAndGet(ScannerFeatureDependenciesComponent scannerFeatureDependenciesComponent)' method" ); } return sScannerFeatureComponent; } //    (   ) public void resetComponent() { sScannerFeatureComponent = null; } public abstract void inject(ScannerActivity scannerActivity); //         Moxy public abstract ScannerScreenComponent scannerScreenComponent(); } 


Saya pikir tidak ada yang baru untuk Anda.

Transisi ke multi-modularitas


Jadi, Anda dan saya dapat dengan jelas menentukan batas-batas fitur melalui api dependensi dan api eksternal. Kami juga menemukan cara untuk menghidupkan semuanya di Dagger. Dan sekarang kita sampai pada langkah logis dan menarik berikutnya - pembagian menjadi modul.
Buka test case segera - itu akan lebih mudah.
Mari kita lihat gambar secara umum:

Dan lihat struktur paket dari contoh:

Sekarang mari kita bicara dengan hati-hati setiap item.

Pertama-tama, kita melihat empat blok besar: Aplikasi , API , Impl , dan Utils . Di API , Impl, dan Utils, Anda mungkin memperhatikan bahwa semua modul dimulai dari inti atau fitur- . Mari kita bicarakan mereka dulu.

Pemisahan menjadi inti dan fitur


Saya membagi semua modul menjadi dua kategori: core- dan fitur- .
Dalam fitur- , seperti yang mungkin Anda duga, fitur kami. Pada intinya - ada hal-hal seperti utilitas, bekerja dengan jaringan, database, dll. Tetapi tidak ada fitur antarmuka di sana. Dan inti bukanlah monolit. Saya telah memecahkan modul inti menjadi potongan-potongan logis dan melawan memuatnya dengan beberapa fitur antarmuka lainnya.
Atas nama modul, tulis inti atau fitur terlebih dahulu . Lebih jauh dalam nama modul adalah nama logis ( pemindai , jaringan , dll.).

Sekarang sekitar empat blok besar: Aplikasi, API, Impl, dan Utils


API
Setiap fitur atau modul inti dibagi menjadi API dan Impl . API berisi api eksternal di mana Anda dapat mengakses fitur atau inti. Hanya ini, dan tidak lebih:

Selain itu, modul api tidak tahu apa-apa tentang siapa pun, itu adalah modul yang benar-benar terisolasi.

Utils
Satu-satunya pengecualian terhadap aturan di atas dapat dianggap beberapa hal yang sepenuhnya utilitarian, yang tidak masuk akal untuk menembus api dan implementasi.

Implan
Di sini kita memiliki subdivisi menjadi core-impl dan feature-impl .
Modul-modul dalam core-impl juga sepenuhnya independen. Satu-satunya ketergantungan mereka adalah modul api . Sebagai contoh, lihat build.gradle dari modul core-db-impl :
 // bla-bla-bla dependencies { implementation project(':core-db-api') // bla-bla-bla } 

Sekarang tentang fitur-impl . Sudah ada bagian terbesar dari logika aplikasi. Modul-modul dari grup impl-fitur dapat mengetahui tentang modul-modul dari API atau grup Utils , tetapi mereka tentu tidak tahu apa-apa tentang modul-modul lain dari grup Impl .
Seperti yang kita ingat, semua dependensi eksternal fitur terakumulasi dalam dependensi eksternal. Misalnya, untuk fitur Pindai, api ini terlihat sebagai berikut:
 public interface ScannerFeatureDependencies { // core-db-api DbClientApi dbClient(); // core-network-api HttpClientApi httpClient(); // core-utils SomeUtils someUtils(); // feature-purchase-api PurchaseInteractor purchaseInteractor(); } 

Dengan demikian, fitur build.gradle-scanner-impl akan seperti ini:
 // bla-bla-bla dependencies { implementation project(':core-utils') implementation project(':core-network-api') implementation project(':core-db-api') implementation project(':feature-purchase-api') implementation project(':feature-scanner-api') // bla-bla-bla } 

Anda mungkin bertanya, mengapa api dependensi eksternal tidak ada dalam modul api? Faktanya adalah ini adalah detail implementasi. Artinya, itu adalah implementasi khusus yang membutuhkan beberapa dependensi tertentu. Untuk dependency api scanner ada di sini:


Retret arsitektur kecil
Mari kita cermati semua hal di atas dan memahami sendiri beberapa poin arsitektur mengenai fitur -...- modul-impl dan ketergantungannya pada modul-modul lain.
Saya bertemu dua pola pemetaan ketergantungan paling populer untuk modul:

  • Modul dapat mengetahui siapa saja. Tidak ada aturan. Tidak ada yang perlu dikomentari.
  • Modul hanya tahu tentang modul inti . Dan dalam modul inti semua antarmuka dari semua fitur terkonsentrasi. Pendekatan ini tidak terlalu menarik bagi saya, karena ada risiko mengubah inti menjadi tempat pembuangan sampah lainnya. Selain itu, jika kita ingin mentransfer modul kita ke aplikasi lain, kita perlu menyalin antarmuka ini ke aplikasi lain, dan juga meletakkannya di inti . Copy-paste antarmuka yang bodoh itu sendiri tidak begitu menarik dan dapat digunakan kembali di masa depan, ketika antarmuka dapat diperbarui.

Dalam contoh kami, saya menganjurkan pengetahuan tentang api dan hanya modul api (well, utils-groups). Fitur tidak tahu apa-apa tentang implementasi.

Tapi ternyata fitur bisa tahu tentang fitur lain (via api, tentu saja) dan menjalankannya. Mungkinkah itu berantakan?
Komentar yang adil. Sulit untuk membuat beberapa aturan yang sangat jelas. Seharusnya ada ukuran dalam segala hal. Kami sudah menyentuh masalah ini sedikit di atas, membagi fitur menjadi yang independen (Scanner dan Anti-Theft) - sepenuhnya independen dan terpisah, dan fitur "dalam konteks", yaitu, mereka selalu diluncurkan sebagai bagian dari sesuatu (Membeli) dan biasanya menyiratkan logika bisnis tanpa ui. Itulah mengapa Scanner dan Anti-Theft mengetahui tentang Pembelian.
Contoh lain. Bayangkan bahwa di Anti-Theft ada yang namanya menghapus data, yaitu membersihkan semua data dari ponsel. Ada banyak logika bisnis, ya, benar-benar terisolasi. Oleh karena itu, logis untuk mengalokasikan data penghapusan sebagai fitur terpisah. Dan kemudian garpu. Jika menghapus data selalu diluncurkan hanya dari Anti-Theft dan selalu hadir di Anti-Theft, logis bahwa Anti-Theft akan tahu tentang menghapus data dan menjalankannya sendiri. Dan modul akumulasi, aplikasi, maka hanya akan tahu tentang Anti-Pencurian. Tetapi jika menghapus data dapat dimulai di tempat lain atau tidak selalu ada di Anti-Pencurian (yaitu, itu bisa berbeda di aplikasi yang berbeda), maka logis bahwa Anti-pencurian tidak tahu tentang fitur ini dan hanya mengatakan sesuatu yang eksternal (melalui Router, melalui beberapa panggilan balik, tidak masalah) bahwa pengguna telah menekan tombol ini dan itu, dan apa yang harus diluncurkan di bawahnya sudah menjadi fitur konsumen dari Anti-Theft (aplikasi spesifik, aplikasi spesifik).

Ada juga pertanyaan menarik tentang mentransfer fitur ke aplikasi lain. Jika, misalnya, kami ingin mentransfer Pemindai ke aplikasi lain, maka kami juga harus mentransfer selain modul : fitur-pemindai-api dan : fitur-pemindai-impl dan modul tempat Pemindai bergantung ( : core-utils ,: core-network- api ,: core-db-api ,: fitur-beli-api ).
Ya tapi! Pertama, semua modul api Anda sepenuhnya independen, dan hanya ada antarmuka dan model data. Tidak ada logika Dan modul-modul ini jelas dipisahkan secara logis, dan : core-utils biasanya merupakan modul umum untuk semua aplikasi.
Kedua, Anda dapat mengumpulkan modul-api dalam bentuk aar dan mengirimkannya melalui pakar ke aplikasi lain, atau Anda dapat menghubungkannya dalam bentuk sub-modul pertunjukan. Tetapi Anda akan memiliki versi, akan ada kontrol, akan ada integritas.
Dengan demikian, penggunaan kembali modul (lebih tepatnya, modul implementasi) di aplikasi lain terlihat jauh lebih sederhana, lebih jelas dan lebih aman.

Aplikasi


Tampaknya kita memiliki gambar yang ramping dan dapat dimengerti dengan fitur, modul, dependensinya, dan hanya itu. Sekarang kita sampai pada klimaks - ini adalah kombinasi api dan implementasinya, menggantikan semua dependensi yang diperlukan, dll., Tetapi dari sudut pandang modul Gredloi. Titik koneksi biasanya adalah aplikasi itu sendiri.
Ngomong-ngomong, dalam contoh kita, titik ini masih fitur-pemindai-contoh . Pendekatan di atas memungkinkan Anda untuk menjalankan setiap fitur Anda sebagai aplikasi terpisah, yang sangat menghemat waktu pembuatan selama pengembangan aktif. Cantik!

Sebagai permulaan, mari kita pertimbangkan bagaimana semuanya melalui aplikasi terjadi dengan contoh Scanner yang sudah dicintai.
Ingat fitur dengan cepat:
Sci dependensi eksternal api adalah:
 public interface ScannerFeatureDependencies { // core-db-api DbClientApi dbClient(); // core-network-api HttpClientApi httpClient(); // core-utils SomeUtils someUtils(); // feature-purchase-api PurchaseInteractor purchaseInteractor(); } 

Oleh karena itu : feature-scanner-impl tergantung pada modul-modul berikut:
 // bla-bla-bla dependencies { implementation project(':core-utils') implementation project(':core-network-api') implementation project(':core-db-api') implementation project(':feature-purchase-api') implementation project(':feature-scanner-api') // bla-bla-bla } 


Berdasarkan ini, kita dapat membuat komponen Belati yang mengimplementasikan api dependensi eksternal:
 @Component(dependencies = { CoreUtilsApi.class, CoreNetworkApi.class, CoreDbApi.class, PurchaseFeatureApi.class }) @PerFeature interface ScannerFeatureDependenciesComponent extends ScannerFeatureDependencies { } 

Saya menempatkan antarmuka ini di ScannerFeatureComponent untuk kenyamanan:
 @Component(modules = { ScannerFeatureModule.class, ScreenNavigationModule.class }, dependencies = ScannerFeatureDependencies.class) @PerFeature public abstract class ScannerFeatureComponent implements ScannerFeatureApi { // bla-bla-bla @Component(dependencies = { CoreUtilsApi.class, CoreNetworkApi.class, CoreDbApi.class, PurchaseFeatureApi.class }) @PerFeature interface ScannerFeatureDependenciesComponent extends ScannerFeatureDependencies { } } 


Sekarang Aplikasi. App tahu tentang semua modul yang dibutuhkan ( core-, fitur-, api, impl ):
 // bla-bla-bla dependencies { implementation project(':core-utils') implementation project(':core-db-api') implementation project(':core-db-impl') implementation project(':core-network-api') implementation project(':core-network-impl') implementation project(':feature-scanner-api') implementation project(':feature-scanner-impl') implementation project(':feature-antitheft-api') implementation project(':feature-antitheft-impl') implementation project(':feature-purchase-api') implementation project(':feature-purchase-impl') // bla-bla-bla } 

Selanjutnya, buat kelas pembantu. Misalnya, FeatureProxyInjector . Ini akan membantu menginisialisasi semua komponen dengan benar, dan melalui kelas ini kita akan beralih ke fitur. Mari kita lihat bagaimana komponen fitur Pemindai diinisialisasi:
 public class FeatureProxyInjector { // another... public static ScannerFeatureApi getFeatureScanner() { return ScannerFeatureComponent.initAndGet( DaggerScannerFeatureComponent_ScannerFeatureDependenciesComponent.builder() .coreDbApi(CoreDbComponent.get()) .coreNetworkApi(CoreNetworkComponent.get()) .coreUtilsApi(CoreUtilsComponent.get()) .purchaseFeatureApi(featurePurchaseGet()) .build() ); } } 

Secara lahiriah, kami memberikan fitur antarmuka ( ScannerFeatureApi ), dan di dalam kami hanya menginisialisasi seluruh grafik ketergantungan implementasi (melalui metode ScannerFeatureComponent.initAndGet (...) ).
DaggerPurchaseComponent_PurchaseFeatureDependenciesComponent adalah implementasi dari PurchaseFeatureDependenciesComponent yang dihasilkan oleh Dagger, yang telah kita bahas di atas, di mana kami mengganti implementasi modul-api dalam pembangun.
Itu semua ajaib. Lihat contoh lagi.

Berbicara tentang contoh . Sebagai contoh, kita juga harus memenuhi semua dependensi eksternal : feature-scanner-impl . Tapi karena ini adalah contoh, kita bisa mengganti kelas boneka.
Bagaimana tampilannya:
 //     ScannerFeatureDependencies public class ScannerFeatureDependenciesFake implements ScannerFeatureDependencies { @Override public DbClientApi dbClient() { return new DbClientFake(); } @Override public HttpClientApi httpClient() { return new HttpClientFake(); } @Override public SomeUtils someUtils() { return CoreUtilsComponent.get().someUtils(); } @Override public PurchaseInteractor purchaseInteractor() { return new PurchaseInteractorFake(); } } //  -  Application-   public class ScannerExampleApplication extends Application { @Override public void onCreate() { super.onCreate(); ScannerFeatureComponent.initAndGet( // ,     =) new ScannerFeatureDependenciesFake() ); } } 

Dan fitur Pemindai itu sendiri misalnya diluncurkan melalui manifes, agar tidak memblokir aktivitas kosong tambahan:
 <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.scanner_example"> <application android:name=".ScannerExampleApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <!--   --> <activity android:name="com.example.scanner.presentation.view.ScannerActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> 


Algoritma transisi dari monomodularity ke multimodularity


Hidup adalah hal yang keras. Dan kenyataannya adalah bahwa kita semua bekerja dengan Legacy. Jika seseorang sekarang melihat proyek baru, di mana Anda dapat segera memberkati segalanya, maka saya iri Anda, kawan. Tapi ini tidak terjadi pada saya, dan pria itu juga salah =).

Bagaimana cara menerjemahkan aplikasi Anda ke dalam multi-modul? Saya mendengar sebagian besar tentang dua opsi.
Yang pertama. Mempartisi aplikasi di sini dan sekarang. Benar, proyek Anda mungkin tidak dirakit selama satu atau dua bulan =).
Yang kedua Cobalah untuk mengeluarkan fitur secara bertahap. Tetapi pada saat yang sama, semua jenis dependensi fitur ini meregang. Dan di sini kesenangan dimulai. Kode dependensi dapat menarik kode lain, semuanya bermigrasi ke modul umum , ke modul inti dan sebaliknya, dan sebagainya. Akibatnya, menarik satu fitur dapat mengharuskan bekerja dengan sebagian aplikasi yang bagus. Dan sekali lagi, pada awalnya, proyek Anda tidak akan mengumpulkan periode waktu yang layak.

Saya menganjurkan transfer bertahap aplikasi ke multi-modularitas, karena secara paralel kita masih perlu melihat fitur-fitur baru. Gagasan utamanya adalah jika modul Anda memerlukan beberapa dependensi, Anda tidak boleh langsung menyeret kode ini ke modul secara fisik juga . Mari kita lihat algoritma penghapusan modul menggunakan Scanner sebagai contoh:

  • Buat fitur api, masukkan ke dalam modul api baru. Yaitu, untuk sepenuhnya membuat modul : feature-scanner-api dengan semua antarmuka.
  • Buat : feature-scanner-impl . Secara fisik mentransfer semua kode yang terkait dengan fitur ke modul ini. Segala sesuatu yang bergantung pada fitur Anda, studio akan segera menyoroti.
  • Identifikasi dependensi fitur eksternal. Buat antarmuka yang sesuai. Antarmuka ini dibagi menjadi modul-api logis. Yaitu, dalam contoh kita, membuat modul : core-utils ,: core-network-api ,: core-db-api ,: fitur-beli-api dengan antarmuka yang sesuai.
    Saya menyarankan Anda untuk segera berinvestasi dalam nama dan makna modul. Jelas bahwa seiring berjalannya waktu, antarmuka dan modul dapat sedikit dikocok, diciutkan, dll., Ini normal.
  • Buat api dependensi eksternal ( ScannerFeatureDependencies ). Tergantung : fitur-scanner-impl mendaftar modul-api yang baru dibuat.
  • Karena kami memiliki semua warisan dalam aplikasi , inilah yang kami lakukan. Dalam aplikasi, kami menghubungkan semua modul yang dibuat untuk fitur (modul api fitur, modul impl fitur, modul api ketergantungan fitur eksternal).
    Poin yang sangat penting . Selanjutnya, dalam aplikasi, kami membuat implementasi dari semua antarmuka ketergantungan fitur yang diperlukan (Pemindai dalam contoh kami). Implementasi ini mungkin hanya akan menjadi proksi dari dependensi api Anda ke implementasi dependensi ini di proyek saat ini. Saat menginisialisasi komponen fitur, gantikan data implementasi.
    Sulit kata, mau contoh? Jadi dia sudah ada! Bahkan, sesuatu yang serupa sudah ada di contoh fitur-scanner. Sekali lagi, saya akan memberikan kode yang sedikit disesuaikan:
     //     ScannerFeatureDependencies  app- public class ScannerFeatureDependenciesLegacy implements ScannerFeatureDependencies { @Override public DbClientApi dbClient() { return new DbClientLegacy(); } @Override public HttpClientApi httpClient() { // -  // ,      return NetworkFabric.createHttpClientLegacy(); } @Override public SomeUtils someUtils() { return new SomeUtils(); } @Override public PurchaseInteractor purchaseInteractor() { return new PurchaseInteractorLegacy(); } } //  -   ScannerFeatureComponent.initAndGet( new ScannerFeatureDependenciesLegacy() ); 

    Artinya, pesan utama di sini adalah ini. Biarkan semua kode eksternal yang diperlukan untuk fitur tinggal di aplikasi , seperti yang terjadi. Dan fitur itu sendiri sudah akan bekerja dengannya dengan cara normal, melalui api (artinya ketergantungan api dan modul-api). Di masa depan, implementasinya akan secara bertahap beralih ke modul. Tapi kemudian kita akan menghindari permainan tanpa akhir dengan menyeret dari modul ke modul kode eksternal yang diperlukan untuk fitur tersebut. Kita bisa bergerak dalam iterasi yang jelas!
  • Untung

Berikut ini adalah algoritma yang sederhana namun berfungsi yang memungkinkan Anda bergerak menuju tujuan Anda langkah demi langkah.

Kiat tambahan


Seberapa besar / kecil fitur yang seharusnya?
Itu semua tergantung pada proyek, dll. Tetapi pada awal transisi ke multi-modularitas, saya menyarankan Anda untuk memecah menjadi potongan-potongan besar. Selanjutnya, jika perlu, Anda akan memilih lebih banyak modul dari modul-modul ini. Tapi jangan digiling.Jangan lakukan ini: satu / beberapa kelas = satu modul.

Kemurnian aplikasi-modul
Dalam transisi untuk multi-moduling aplikasi kita akan cukup besar, dan akan termasuk kedutan fitur yang dipilih. Ada kemungkinan bahwa selama pekerjaan Anda harus membuat perubahan pada warisan ini, untuk menyelesaikan sesuatu di sana, baik, atau Anda hanya memiliki rilis, dan Anda tidak sampai memotong modul. Dalam hal ini, Anda ingin aplikasi , dan dengan itu semua Legacy, untuk mengetahui tentang fitur yang disorot hanya melalui api, tidak ada pengetahuan tentang implementasinya. Tetapi aplikasi tersebut , pada kenyataannya, menggabungkan modul api dan modul , dan oleh karena itu aplikasi tersebut mengetahui segalanya.
Dalam hal ini, Anda dapat membuat modul khusus: adaptor , yang hanya akan menjadi titik penghubung api dan impl, dan aplikasi kemudian hanya akan tahu tentang api. Saya pikir idenya jelas. Anda dapat melihat contoh di cabang clean_app . Saya akan menambahkannya dengan Moxy, atau lebih tepatnya MoxyReflector, ada beberapa masalah ketika dipecah menjadi modul, karena itu saya harus membuat modul tambahan lain : stub-moxy-java . Sedikit sihir, di mana tanpa itu.
Satu-satunya amandemen. Ini hanya akan berfungsi ketika fitur Anda dan dependensi terkait sudah ditransfer secara fisik ke modul lain. Jika Anda membuat fitur, tetapi dependensinya masih ada di aplikasi , seperti pada algoritma di atas, maka ini tidak akan berfungsi.

Kata penutup


Artikel itu ternyata agak besar. Tapi saya harap ini sangat membantu Anda dalam memerangi monomodularitas, memahami bagaimana seharusnya, dan bagaimana berteman dengan DI.
Jika Anda tertarik untuk terjun ke masalah dengan kecepatan build, cara mengukur semuanya, maka saya merekomendasikan laporan Denis Neklyudov dan Zhenya Suvorov (Mobius 2018 Piter, video belum tersedia untuk umum).
Tentang Gradle. Perbedaan antara api dan implementasi dalam gradle ditunjukkan dengan sempurna oleh Vova Tagakov . Jika Anda ingin mengurangi boilerplate multi-modul, Anda bisa mulai di sini dengan artikel ini .
Saya akan senang memberikan komentar, koreksi, dan juga suka! Semua kode bersih!

Source: https://habr.com/ru/post/id422555/


All Articles