Dari salin-tempel ke komponen: menggunakan kembali kode dalam aplikasi yang berbeda



Badoo mengembangkan beberapa aplikasi, dan masing-masing dari mereka adalah produk yang terpisah dengan karakteristik, manajemen, produk dan tim teknik sendiri. Tapi kami semua bekerja bersama di kantor yang sama dan menyelesaikan masalah serupa.

Pengembangan setiap proyek berbeda. Basis kode tidak hanya dipengaruhi oleh kerangka waktu dan solusi produk yang berbeda, tetapi juga oleh visi pengembang. Pada akhirnya, kami memperhatikan bahwa proyek-proyek tersebut memiliki fungsi yang sama, yang secara fundamental berbeda dalam implementasinya.

Kemudian kami memutuskan untuk datang ke struktur yang akan memberi kami kesempatan untuk menggunakan kembali fitur di antara aplikasi. Sekarang, alih-alih mengembangkan fungsionalitas dalam proyek individual, kami membuat komponen umum yang berintegrasi ke semua produk. Jika Anda tertarik dengan bagaimana kami sampai pada ini, selamat datang di kucing.

Tapi pertama-tama, mari kita memikirkan masalah, solusinya yang mengarah pada penciptaan komponen umum. Ada beberapa di antaranya:

  • salin-tempel antar aplikasi;
  • proses yang memasukkan tongkat ke roda;
  • arsitektur proyek yang berbeda.



Artikel ini adalah versi teks dari laporan saya dengan AppsConf 2019 , yang dapat dilihat di sini .

Masalah: salin tempel


Beberapa waktu yang lalu, ketika pohon-pohon lebih buram, rumput lebih hijau, dan saya setahun lebih muda, kami sering mengalami situasi berikut.

Ada pengembang, sebut saja dia Lesha. Dia membuat modul keren untuk tugasnya, memberi tahu rekan-rekannya tentang hal itu dan meletakkannya di repositori untuk aplikasinya, tempat dia menggunakannya.

Masalahnya adalah bahwa semua aplikasi kita berada di repositori yang berbeda.



Pengembang Andrey saat ini hanya bekerja pada aplikasi lain di repositori yang berbeda. Dia ingin menggunakan modul ini dalam tugasnya, yang mencurigakan mirip dengan yang digunakan Lesha. Tetapi ada masalah: proses menggunakan kembali kode sepenuhnya debugged.

Dalam situasi ini, Andrei akan menulis keputusannya (yang terjadi pada 80% kasus) atau menyalin-paste solusi Lyosha dan mengubah semua yang ada di dalamnya sehingga sesuai dengan aplikasi, tugas, atau suasana hatinya.



Setelah itu, Lesha dapat memperbarui modulnya dengan menambahkan perubahan pada kode untuk tugasnya. Dia tidak tahu tentang versi lain dan hanya akan memperbarui repositori-nya.

Situasi ini membawa beberapa masalah.

Pertama, kami memiliki beberapa aplikasi, masing-masing dengan sejarah pengembangannya sendiri. Ketika mengerjakan setiap aplikasi, tim produk sering menciptakan solusi yang sulit untuk dibawa ke struktur tunggal.

Kedua, tim yang terpisah terlibat dalam proyek-proyek, yang berkomunikasi dengan buruk satu sama lain dan, oleh karena itu, jarang saling menginformasikan tentang pembaruan / penggunaan kembali satu atau modul lain.

Ketiga, arsitektur aplikasi sangat berbeda: dari MVP ke MVI, dari aktivitas dewa ke aktivitas tunggal.

Nah, "sorotan program": aplikasi berada di repositori yang berbeda, masing-masing dengan prosesnya sendiri.

Pada awal perjuangan melawan masalah ini, kami menetapkan tujuan akhir: untuk menggunakan kembali praktik terbaik kami (baik logika dan UI) antara semua aplikasi.

Keputusan: kami menetapkan proses


Dari masalah di atas, dua terkait dengan proses:

  1. Dua repositori yang berbagi proyek dengan tembok yang tidak bisa ditembus.
  2. Pisahkan tim tanpa komunikasi yang mapan dan persyaratan yang berbeda dari tim aplikasi produk.

Mari kita mulai dengan yang pertama: kita berurusan dengan dua repositori dengan versi modul yang sama. Secara teoritis, kita bisa menggunakan git-subtree atau solusi serupa dan meletakkan modul proyek umum ke dalam repositori terpisah.



Masalah terjadi selama modifikasi. Tidak seperti proyek open-source, yang memiliki API stabil dan didistribusikan melalui sumber eksternal, perubahan sering terjadi pada komponen internal yang merusak segalanya. Saat menggunakan subtree, setiap migrasi semacam itu menjadi menyakitkan.

Rekan-rekan saya dari tim iOS memiliki pengalaman serupa, dan ternyata itu tidak terlalu berhasil, seperti yang dibicarakan Anton Schukin di konferensi Mobius tahun lalu.

Setelah mempelajari dan memahami pengalaman mereka, kami beralih ke satu repositori. Semua aplikasi Android sekarang berada di satu tempat, yang memberi kita manfaat tertentu:

  • Anda dapat menggunakan kembali kode dengan aman menggunakan modul Gradle;
  • kami berhasil menghubungkan rantai alat pada CI menggunakan satu infrastruktur untuk pembuatan dan pengujian;
  • perubahan ini menghilangkan hambatan fisik dan mental antar tim, karena sekarang kita bebas menggunakan perkembangan dan solusi masing-masing.

Tentu saja, solusi ini juga memiliki kekurangan. Kami memiliki proyek besar, yang kadang-kadang tidak tunduk pada IDE dan Gradle. Masalahnya bisa diselesaikan sebagian oleh modul Load / Unload di Android Studio, tetapi sulit untuk menggunakannya jika Anda perlu bekerja secara bersamaan di semua aplikasi dan sering beralih.

Masalah kedua - interaksi antar tim - terdiri dari beberapa bagian:

  • tim terpisah tanpa komunikasi yang mapan;
  • distribusi tanggung jawab yang tidak jelas untuk modul-modul umum;
  • persyaratan yang berbeda dari tim produk.

Untuk mengatasinya, kami membentuk tim yang terlibat dalam penerapan fungsi tertentu di setiap aplikasi: misalnya, obrolan atau pendaftaran. Selain pengembangan, mereka juga bertanggung jawab untuk mengintegrasikan komponen-komponen ini ke dalam aplikasi.

Tim produk sudah memiliki komponen yang ada di tangan mereka, meningkatkan dan menyesuaikannya dengan kebutuhan proyek tertentu.

Dengan demikian, sekarang penciptaan komponen yang dapat digunakan kembali adalah bagian dari proses untuk seluruh perusahaan, dari tahap ide hingga awal produksi.

Solusi: perampingan arsitektur


Langkah kami selanjutnya untuk menggunakan kembali adalah merampingkan arsitektur. Kenapa kita melakukan ini?

Basis kode kami membawa warisan sejarah beberapa tahun pembangunan. Seiring dengan waktu dan orang, pendekatan berubah. Jadi kami menemukan diri kami dalam situasi dengan seluruh kebun binatang arsitektur, yang mengakibatkan masalah berikut:

  1. Integrasi modul umum hampir lebih lambat daripada menulis yang baru. Selain fitur fungsional, perlu untuk memasang dengan struktur komponen dan aplikasi.
  2. Pengembang yang harus beralih antar aplikasi sangat sering menghabiskan banyak waktu untuk menguasai pendekatan baru.
  3. Seringkali, pembungkus ditulis dari satu pendekatan ke yang lain, yang berjumlah setengah kode dalam integrasi modul.

Pada akhirnya, kami memutuskan pada pendekatan MVI, yang kami susun di perpustakaan MVICore kami ( GitHub ). Kami terutama tertarik pada salah satu fiturnya - pembaruan keadaan atom, yang selalu menjamin validitas. Kami melangkah lebih jauh dan menggabungkan kondisi lapisan logis dan presentasi, mengurangi fragmentasi. Dengan demikian, kita sampai pada struktur di mana satu-satunya entitas bertanggung jawab atas logika, dan tampilan hanya menampilkan model yang dibuat dari negara.



Pemisahan tanggung jawab terjadi melalui transformasi model antar level. Berkat ini, kami mendapatkan bonus dalam bentuk kegunaan ulang. Kami menghubungkan elemen-elemen dari luar, yaitu masing-masing tidak mencurigai ada yang lain - mereka hanya memberikan beberapa model dan bereaksi terhadap apa yang datang kepada mereka. Ini memungkinkan Anda untuk mengeluarkan komponen dan menggunakannya di tempat lain dengan menulis adaptor untuk model mereka.

Mari kita lihat contoh layar sederhana bagaimana tampilannya dalam kenyataan.



Kami menggunakan antarmuka RxJava dasar untuk menunjukkan jenis elemen yang bekerja. Input ditandai oleh antarmuka Konsumen <T>, output - ObservableSource <T>.

// input = Consumer<ViewModel> // output = ObservableSource<Event> class View( val events: PublishRelay<Event> ): ObservableSource<Event> by events, Consumer<ViewModel> { val button: Button val textView: TextView init { button.setOnClickListener { events.accept(Event.ButtonClick) } } override fun accept(model: ViewModel) { textView.text = model.text } } 

Dengan menggunakan antarmuka ini, kita dapat mengekspresikan View as Consumer <ViewModel> dan ObservableSource <Event>. Perhatikan bahwa ViewModel hanya berisi keadaan layar dan tidak ada hubungannya dengan MVVM. Setelah menerima model, kami dapat menunjukkan data dari itu, dan ketika kami mengklik tombol, kami mengirim acara, yang dikirim di luar.

 // input = Consumer<Wish> // output = ObservableSource<State> class Feature: ReducerFeature<Wish, State>( initialState = State(counter = 0), reducer = ReducerImpl() ) { class ReducerImpl: Reducer<Wish, State> { override fun invoke(state: State, wish: Wish) = when (wish) { is Increment -> state.copy(counter = state.counter + 1) } } } 

Fitur sudah mengimplementasikan ObservableSource dan Konsumen untuk kami; kita perlu mentransfer di sana keadaan awal (penghitung sama dengan 0) dan menunjukkan cara mengubah keadaan ini.

Setelah transfer Wish, Reducer dipanggil, yang membuat yang baru berdasarkan pada status terakhir. Selain Peredam, logika dapat dijelaskan oleh komponen lain. Anda dapat mempelajari lebih lanjut tentang mereka di sini .

Setelah membuat dua elemen, kita tetap harus menghubungkannya.


 val eventToWish: (Event) -> Wish = { when (it) { is ButtonClick -> Increment } } val stateToModel: (State) -> ViewModel = { ViewModel(text = state.counter.toString()) } Binder().apply { bind(view to feature using eventToWish) bind(feature to view using stateToModel) } 

Pertama, kami menunjukkan bagaimana kami mengubah elemen dari satu jenis menjadi yang lain. Jadi, ButtonClick menjadi Peningkatan, dan bidang penghitung dari Status masuk ke teks.

Sekarang kita dapat membuat masing-masing rantai dengan transformasi yang diinginkan. Untuk ini kami menggunakan Binder. Ini memungkinkan Anda untuk membuat hubungan antara ObservableSource dan Konsumen, mengamati siklus hidup. Dan semua ini dengan sintaks yang bagus. Jenis koneksi ini membawa kita ke sistem yang fleksibel yang memungkinkan kita untuk menarik dan menggunakan elemen secara individual.

MVICore-elements bekerja sangat baik dengan "kebun binatang" arsitektur kami setelah menulis pembungkus dari ObservableSource dan Konsumen. Sebagai contoh, kita dapat membungkus metode Use Case dari Clean Architecture in Wish / State dan menggunakan dalam rantai alih-alih Fitur.



Komponen


Akhirnya, kita beralih ke komponen. Seperti apa mereka?

Pertimbangkan layar dalam aplikasi dan membaginya menjadi bagian-bagian yang logis.



Itu dapat dibedakan:

  • toolbar dengan logo dan tombol di bagian atas;
  • kartu dengan profil dan logo;
  • Bagian Instagram.

Masing-masing bagian ini adalah komponen yang dapat digunakan kembali dalam konteks yang sama sekali berbeda. Jadi, bagian Instagram dapat menjadi bagian dari pengeditan profil di aplikasi lain.



Dalam kasus umum, komponen adalah beberapa tampilan, elemen logika dan komponen bersarang di dalamnya, disatukan oleh fungsi umum. Dan segera muncul pertanyaan: bagaimana cara mengumpulkan mereka menjadi struktur yang didukung?

Masalah pertama yang kami temui adalah bahwa MVICore membantu membuat dan mengikat elemen, tetapi tidak menawarkan struktur yang sama. Ketika menggunakan kembali elemen dari modul umum, tidak jelas di mana harus menempatkan potongan-potongan ini: di dalam bagian umum atau di sisi aplikasi?

Dalam kasus umum, kami pasti tidak ingin memberikan potongan aplikasi yang tersebar. Idealnya, kami mengupayakan beberapa jenis struktur yang akan memungkinkan kami untuk mendapatkan dependensi dan mengumpulkan komponen secara keseluruhan dengan siklus hidup yang diinginkan.

Awalnya, kami membagi komponen menjadi layar. Koneksi elemen terjadi di sebelah pembuatan wadah DI untuk aktivitas atau fragmen. Wadah ini sudah tahu tentang semua dependensi, memiliki akses ke View dan siklus hidup.

 object SomeScopedComponent : ScopedComponent<SomeComponent>() { override fun create(): SomeComponent { return DaggerSomeComponent.builder() .build() } override fun SomeComponent.subscribe(): Array<Disposable> = arrayOf( Binder().apply { bind(feature().news to otherFeature()) bind(feature() to view()) } ) } 

Masalah dimulai di dua tempat sekaligus:

  1. DI mulai bekerja dengan logika, yang menyebabkan deskripsi seluruh komponen dalam satu kelas.
  2. Karena wadah dilampirkan ke Aktivitas atau Fragmen dan menjelaskan setidaknya seluruh layar, ada banyak elemen di layar / wadah tersebut, yang diterjemahkan menjadi sejumlah besar kode untuk menghubungkan semua dependensi layar ini.

Memecahkan masalah secara berurutan, kami mulai dengan menempatkan logika dalam komponen yang terpisah. Jadi, kami dapat mengumpulkan semua Fitur di dalam komponen ini dan berkomunikasi dengan View melalui input dan output. Dari sudut pandang antarmuka, ini terlihat seperti elemen MVICore biasa, tetapi pada saat yang sama dibuat dari beberapa yang lain.



Setelah menyelesaikan masalah ini, kami berbagi tanggung jawab untuk menghubungkan elemen-elemen tersebut. Tapi kami masih berbagi komponen di layar, yang jelas-jelas tidak ada di tangan kami, menghasilkan banyak ketergantungan di satu tempat.

 @Scope internal class ComponentImpl @Inject constructor( private val params: ScreenParams, news: NewsRelay, @OnDisposeAction onDisposeAction: () -> Unit, globalFeature: GlobalFeature, conversationControlFeature: ConversationControlFeature, messageSyncFeature: MessageSyncFeature, conversationInfoFeature: ConversationInfoFeature, conversationPromoFeature: ConversationPromoFeature, messagesFeature: MessagesFeature, messageActionFeature: MessageActionFeature, initialScreenFeature: InitialScreenFeature, initialScreenExplanationFeature: InitialScreenExplanationFeature?, errorFeature: ErrorFeature, conversationInputFeature: ConversationInputFeature, sendRegularFeature: SendRegularFeature, sendContactForCreditsFeature: SendContactForCreditsFeature, screenEventTrackingFeature: ScreenEventTrackingFeature, messageReadFeature: MessageReadFeature?, messageTimeFeature: MessageTimeFeature?, photoGalleryFeature: PhotoGalleryFeature?, onlineStatusFeature: OnlineStatusFeature?, favouritesFeature: FavouritesFeature?, isTypingFeature: IsTypingFeature?, giftStoreFeature: GiftStoreFeature?, messageSelectionFeature: MessageSelectionFeature?, reportingFeature: ReportingFeature?, takePhotoFeature: TakePhotoFeature?, giphyFeature: GiphyFeature, goodOpenersFeature: GoodOpenersFeature?, matchExpirationFeature: MatchExpirationFeature, private val pushIntegration: PushIntegration ) : AbstractMviComponent<UiEvent, States>( 

Solusi yang tepat dalam situasi ini adalah memecah komponen. Seperti yang kita lihat di atas, setiap layar terdiri dari banyak elemen logis yang dapat kita bagi menjadi beberapa bagian independen.

Setelah sedikit refleksi, kami sampai pada struktur pohon dan, secara naif membangunnya dari komponen yang ada, kami mendapat skema ini:



Tentu saja, mempertahankan sinkronisasi dua pohon (dari Tampilan dan dari logika) hampir tidak mungkin. Namun, jika komponen bertanggung jawab untuk menampilkan View-nya, kami dapat menyederhanakan skema ini. Setelah mempelajari solusi yang telah dibuat, kami memikirkan kembali pendekatan kami, dengan mengandalkan RIB Uber.



Gagasan di balik pendekatan ini sangat mirip dengan dasar-dasar MVICore. RIB adalah semacam "kotak hitam", komunikasi yang terjadi melalui antarmuka yang didefinisikan secara ketat dari dependensi (yaitu, input dan output). Terlepas dari kerumitan nyata mendukung antarmuka seperti itu dalam produk berulang yang cepat, kami mendapatkan peluang besar untuk menggunakan kembali kode.

Jadi, dibandingkan dengan iterasi sebelumnya, kita mendapatkan:

  • logika dikemas di dalam komponen;
  • dukungan untuk bersarang, yang memungkinkan untuk membagi layar menjadi beberapa bagian;
  • interaksi dengan komponen lain melalui antarmuka yang ketat dari input / output dengan dukungan untuk MVICore;
  • kompilasi waktu aman dari dependensi komponen (mengandalkan Dagger sebagai DI).

Tentu saja, ini jauh dari semua. Repositori di GitHub berisi deskripsi yang lebih terperinci dan terkini.

Dan di sini kita memiliki dunia yang sempurna. Ini memiliki komponen dari mana kita dapat membangun pohon yang sepenuhnya dapat digunakan kembali.

Tapi kita hidup di dunia yang tidak sempurna.

Selamat datang di kenyataan!


Di dunia yang tidak sempurna, ada banyak hal yang harus kita hadapi. Kami khawatir tentang hal berikut:

  • fungsi yang berbeda: meskipun semua penyatuan, kami masih berurusan dengan produk individu dengan persyaratan yang berbeda;
  • dukungan: bagaimana tanpa fungsi baru di bawah uji A / B?
  • Legacy (semua yang ditulis sebelum arsitektur baru kami).

Kompleksitas solusi meningkat secara eksponensial, karena setiap aplikasi menambahkan sesuatu sendiri ke komponen umum.

Pertimbangkan proses pendaftaran sebagai contoh komponen umum yang terintegrasi ke dalam aplikasi. Secara umum, pendaftaran adalah rangkaian layar dengan tindakan yang memengaruhi seluruh aliran. Setiap aplikasi memiliki layar berbeda dan UI sendiri. Tujuan utamanya adalah membuat komponen yang dapat digunakan kembali secara fleksibel, yang juga akan membantu kita memecahkan masalah dari daftar di atas.



Persyaratan lain-lain


Setiap aplikasi memiliki variasi registrasi yang unik, baik dari sisi logika dan dari sisi UI. Oleh karena itu, kami mulai menggeneralisasi fungsionalitas dalam komponen dengan minimum: dengan mengunduh data dan merutekan seluruh aliran.



Wadah seperti itu mentransfer data ke aplikasi dari server, yang diubah menjadi layar jadi dengan logika. Satu-satunya persyaratan adalah bahwa layar yang diteruskan ke wadah seperti itu harus memenuhi dependensi untuk berinteraksi dengan logika dari seluruh aliran.

Setelah melakukan trik ini dengan beberapa aplikasi, kami perhatikan bahwa logika layar hampir sama. Di dunia yang ideal, kami akan membuat logika umum dengan menyesuaikan Tampilan. Pertanyaannya adalah bagaimana menyesuaikannya.

Seperti yang dapat Anda ingat dari deskripsi MVICore, baik View maupun Feature didasarkan pada antarmuka dari ObservableSource dan Consumer. Menggunakannya sebagai abstraksi, kita dapat mengganti implementasi tanpa mengubah bagian utama.



Jadi kami menggunakan kembali logika dengan membagi UI. Akibatnya, dukungan menjadi jauh lebih nyaman.

Dukungan


Pertimbangkan uji A / B untuk variasi elemen visual. Dalam hal ini, logika kami tidak berubah, yang memungkinkan kami untuk mengganti implementasi tampilan lain untuk antarmuka yang ada dari ObservableSource dan Konsumen.



Tentu saja, terkadang persyaratan baru bertentangan dengan logika yang sudah tertulis. Dalam hal ini, kami selalu dapat kembali ke skema asli, tempat aplikasi memasok seluruh layar. Bagi kami ini semacam "kotak hitam", dan tidak masalah bagi wadah apa yang mereka berikan padanya, asalkan antar mukanya dihormati.

Integrasi


Seperti yang ditunjukkan oleh praktik, sebagian besar aplikasi menggunakan Activity sebagai unit dasar, alat komunikasi yang sudah lama dikenal. Yang harus kami lakukan adalah mempelajari cara membungkus komponen dalam Aktivitas dan meneruskan data melalui input dan output. Ternyata, pendekatan ini berfungsi baik dengan fragmen.

Untuk aplikasi aktivitas tunggal, tidak ada banyak perubahan. Hampir semua kerangka kerja menawarkan elemen dasar mereka di mana komponen RIB memungkinkan mereka untuk dibungkus.

Pada akhirnya


Setelah melewati tahap ini, kami telah secara signifikan meningkatkan persentase penggunaan kembali kode antara proyek-proyek perusahaan kami. Saat ini, jumlah komponen mendekati 100, dan sebagian besar dari mereka menerapkan fungsionalitas untuk beberapa aplikasi sekaligus.

Pengalaman kami menunjukkan bahwa:

  • Meskipun kompleksitas yang semakin meningkat dalam merancang komponen umum, mengingat persyaratan aplikasi yang berbeda, dukungan mereka jauh lebih mudah dalam jangka panjang;
  • membangun komponen secara terpisah satu sama lain , kami sangat menyederhanakan integrasi mereka ke dalam aplikasi yang dibangun berdasarkan prinsip yang berbeda;
  • revisi proses, ditambah dengan penekanan pada pengembangan dan dukungan komponen, memiliki efek positif pada kualitas fungsionalitas keseluruhan.

Rekan saya Zsolt Kocsi sebelumnya menulis tentang MVICore dan ide-ide di baliknya. Saya sangat merekomendasikan membaca artikelnya, yang telah kami terjemahkan di blog kami ( 1 , 2 , 3 ).

Tentang RIB, Anda dapat membaca artikel asli dari Uber . Dan untuk pengetahuan praktis, saya sarankan mengambil beberapa pelajaran dari kami (dalam bahasa Inggris).

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


All Articles