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 pertamaMari 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 keduaIni 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 {
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();
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
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
APISetiap
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.
UtilsSatu-satunya pengecualian terhadap aturan di atas dapat dianggap beberapa hal yang sepenuhnya utilitarian, yang tidak masuk akal untuk menembus api dan implementasi.
ImplanDi 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 {
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 kecilMari 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 {
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 {
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:
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:
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-modulDalam 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!