Typowe wzorce modularyzacji

Nie ma jednej strategii modularyzacji, która sprawdzi się we wszystkich projektach. Z powodu Elastyczność Gradle. Nie ma ograniczeń, co do tego, i planować projekt. Na tej stronie znajduje się omówienie niektórych ogólnych reguł i typowych reguł które możesz wykorzystać przy tworzeniu wielomodułowych aplikacji na Androida.

Zasada wysokiej spójności i niskiego sprzężenia

Jednym ze sposobów do scharakteryzowania modułowej bazy kodu jest użycie łączenia. i spójność. Łączność mierzy stopień, w jakim moduły zależy od siebie. Spójność określa w tym kontekście, jak elementy pod względem funkcjonalności. Ogólnie rzecz biorąc, staraj się niski współczynnik sprzężenia i wysoka spójność:

  • Niski współczynnik sprzężenia oznacza, że moduły powinny być jak najbardziej niezależne od się poszczególne moduły, dzięki czemu zmiany w jednym module nie będą miały żadnego lub minimalnego wpływu innych modułów. Moduły nie powinny wiedzieć, jak działają innych modułów.
  • Wysoka spójność oznacza, że moduły powinny zawierać zbiór kodu, który działa jak system. Powinni mieć jasno określone obowiązki i powinny w zakresie wiedzy z konkretnej dziedziny. Rozważ skorzystanie z przykładowego e-booka aplikacji. Łączenie kodu związanego z książką i kodem płatności może być nieodpowiednie w jednym module, ponieważ są to 2 różne domeny funkcjonalne.
.

Typy modułów

Sposób porządkowania modułów zależy głównie od architektury aplikacji. Poniżej to typowe typy modułów, które możesz wprowadzić do aplikacji, jednocześnie śledząc zalecaną architekturę aplikacji.

Moduły danych

Moduł danych zwykle zawiera repozytorium, źródła danych i klasy modelu. 3 główne zadania modułu danych to:

  1. Uwzględnij wszystkie dane i logikę biznesową określonej domeny: każde dane powinien być odpowiedzialny za obsługę danych, które reprezentują w Twojej domenie. Może obsłużyć wiele typów danych, o ile są one ze sobą powiązane.
  2. Udostępnij repozytorium jako zewnętrzny interfejs API: publiczny interfejs API danych. powinien być repozytorium, ponieważ odpowiada on za udostępnianie danych reszta aplikacji.
  3. Ukryj wszystkie szczegóły implementacji i źródła danych z zewnątrz: Źródła danych powinny być dostępne tylko dla repozytoriów z tego samego modułu. Z zewnątrz pozostają ukryte. Możesz to egzekwować, korzystając z Słowo kluczowe dotyczące widoczności: private lub internal.
.
Rysunek 1. Przykładowe moduły danych i ich zawartość.

Moduły funkcji

Funkcja to wyizolowana część funkcjonalności aplikacji, która zwykle odpowiada ekranu lub serii ściśle powiązanych ze sobą ekranów, takich jak rejestracja lub płatność. przepływu danych. Jeśli Twoja aplikacja ma pasek nawigacyjny u dołu, prawdopodobnie każde miejsce docelowe jest funkcją.

Rysunek 2. Każdą kartę tej aplikacji można zdefiniować jako funkcję.

Funkcje są powiązane z ekranami lub miejscami docelowymi w aplikacji. Dlatego prawdopodobnie ma powiązany interfejs użytkownika i ViewModel obsługujące ich logikę i stanu. Nie trzeba ograniczać ich do pojedynczego wyświetlenia ani miejsce docelowe nawigacji. Moduły funkcji zależą od modułów danych.

Rysunek 3. Przykładowe moduły funkcji i ich zawartość.

Moduły aplikacji

Moduły aplikacji są punktem wejścia do aplikacji. Zależą one od funkcji i zwykle zapewniają nawigację główną. Można skompilować jeden moduł aplikacji w kilku różnych plikach binarnych dzięki tworzeniu wariantów.

Rysunek 4. Wykres zależności między modułami smakowymi produktu i *demonstracyjnym* oraz *pełnym*.

Jeśli Twoja aplikacja jest kierowana na kilka typów urządzeń, takich jak automatyczne, Wear czy TV, zdefiniuj moduł aplikacji dla każdego z nich. Pomaga to rozdzielić poszczególne platformy zależności.

Rysunek 5. Wykres zależności aplikacji na Wear.

Typowe moduły

Popularne moduły, nazywane też modułami podstawowymi, zawierają kod, który jest stosowany w innych modułach często używanych. Zmniejszają one nadmiarowość i nie reprezentują żadnej konkretnej warstwy architektury aplikacji. Oto przykłady typowych modułów:

  • Moduł interfejsu: jeśli wykorzystujesz niestandardowe elementy interfejsu lub wymyślne elementy marki w warto rozważyć umieszczenie kolekcji widżetów w module pod kątem wszystkich funkcji do ponownego wykorzystania. Może to pomóc w ujednoliceniu interfejsu różne funkcje. Jeśli na przykład układy tematyczne są scentralizowane, bolesna zmiana nazwy marki.
  • Moduł Analytics: śledzenie często zależy od wymagań biznesowych, takich jak przy niewielkich wyobrażeniach o architekturze oprogramowania. Moduły śledzące Analytics są często są wykorzystywane w wielu niepowiązanych ze sobą komponentach. Jeśli tak jest w Twoim przypadku, zalecanym modułem jest specjalny moduł analityczny.
  • Moduł sieci: jeśli wiele modułów wymaga połączenia sieciowego, może rozważ utworzenie modułu przeznaczonego dla klienta HTTP. Jest szczególnie przydatne, gdy Twój klient wymaga niestandardowej konfiguracji.
  • Moduł narzędziowy: narzędzia (nazywane też funkcjami pomocniczymi) są zwykle niewielkimi elementami. kodu, który będzie wielokrotnie wykorzystywany w aplikacji. Przykłady narzędzi: narzędzie do testowania, funkcję formatowania waluty, walidator poczty e-mail .

Moduły testowe

Moduły testowe to moduły na Androida, które są używane tylko do testowania. Moduły zawierają kod testowy, zasoby testowe i zależności testowe, które są tylko są wymagane do uruchamiania testów i nie są potrzebne w czasie działania aplikacji. Moduły testowe są tworzone w celu oddzielania kodu określonego do testów od kodu głównego aplikacji, co ułatwia zarządzanie kodem modułu i jego utrzymywanie.

Przypadki użycia modułów testowych

Poniższe przykłady pokazują sytuacje, w których zaimplementowanie modułów testowych mogą być szczególnie korzystne:

  • Wspólny kod testu: jeśli w projekcie masz wiele modułów i niektóre kod testu odnosi się do więcej niż jednego modułu, możesz utworzyć test do udostępnienia kodu. Zmniejsza to liczbę duplikatów i ułatwia test i łatwiej zarządzać kodem. Udostępniony kod testu może zawierać klasy narzędzi takich jak niestandardowe asercje i dopasowania, a także dane testowe, takie jak symulowanych odpowiedzi JSON.

  • Konfiguracje kompilacji oczyszczającej: moduły testowe umożliwiają uzyskanie bardziej przejrzystej konfiguracje, ponieważ mogą mieć własny plik build.gradle. Ty nie zaśmiecać plik build.gradle modułu aplikacji konfiguracjami, które są tylko w przypadku testów.

  • Testy integracji: moduły testowe mogą służyć do przechowywania danych o integracji. które służą do testowania interakcji między różnymi częściami aplikacji, uwzględniające interfejs użytkownika, logikę biznesową, żądania sieciowe i zapytania do bazy danych.

  • Aplikacje na dużą skalę: moduły testowe są szczególnie przydatne w przypadku: dużych aplikacji ze złożonymi bazami kodu i wieloma modułami. W takim W takich przypadkach moduły testowe mogą pomóc w ulepszaniu organizacji kodu i łatwiejszej konserwacji.

.
Rysunek 6. Moduły testowe można wykorzystać do izolowania modułów, które w innym przypadku byłyby od siebie zależne.

Komunikacja między modułami

Moduły rzadko występują w całkowitym rozdzieleniu i często bazują na innych modułach oraz w kontakcie z klientami. Ważne jest, aby połączenie było niskie, nawet jeśli moduły współpracować i często wymieniać się informacjami. Czasami bezpośrednia komunikacja między dwoma modułami jest lub z ograniczeniami architektury. Może się to też okazać niemożliwe, np. w przypadku cyklicznego zależności.

Rysunek 7. Bezpośrednia, dwukierunkowa komunikacja między modułami jest niemożliwa z powodu zależności cyklicznych. Moduł zapośredniczenia jest niezbędny do skoordynowania przepływu danych między 2 innymi niezależnymi modułami.

Aby rozwiązać ten problem, możesz zastosować trzeci moduł zapośredniczenia między dwoma innymi modułami. Moduł mediacji może nasłuchiwać wiadomości od i przekazywać je dalej w razie potrzeby. W naszej przykładowej aplikacji proces płatności musi wiedzieć, którą książkę kupić, mimo że wydarzenie ma swoje początki na osobnym ekranie, który jest częścią innej funkcji. W tym przypadku parametr mediator to moduł będący właścicielem wykresu nawigacji (zwykle jest to moduł aplikacji). W tym przykładzie używamy nawigacji, aby przekazywać dane z funkcji Dom za pomocą komponentu Nawigacja.

navController.navigate("checkout/$bookId")

Miejsce docelowe płatności otrzymuje identyfikator książki jako argument, którego używa do pobrać informacje o książce. Możesz użyć nicka zapisanego stanu, aby: możesz pobrać argumenty nawigacyjne w elemencie ViewModel funkcji docelowej.

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

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

Nie należy przekazywać obiektów jako argumentów nawigacyjnych. Zamiast tego użyj prostych identyfikatorów dzięki którym funkcje mogą uzyskiwać dostęp do wybranych zasobów z warstwy danych i wczytywać je z poziomu warstwy danych. W ten sposób unikniesz problemów z pojedynczym źródłem danych. tej zasady.

W przykładzie poniżej oba moduły funkcji są zależne od tego samego modułu danych. Ten pozwala zminimalizować ilość danych, których potrzebuje moduł mediatora do przodu i zachowuje połączenie między modułami. Zamiast przekazywać , moduły powinny wymieniać identyfikatory podstawowe i wczytywać zasoby z modułu udostępniania danych.

Rysunek 8. 2 moduły funkcji korzystające z modułu udostępnianych danych.

Odwrócenie zależności

Odwrócenie zależności występuje wtedy, gdy uporządkujesz kod w taki sposób, aby abstrakcja co nie jest związane z konkretnym wdrożeniem.

  • abstrakcja: umowa określająca, jak komponenty lub moduły w współdziałają ze sobą. Moduły abstrakcyjne definiują interfejs API i zawierać interfejsy i modele.
  • Konkretna implementacja: moduły zależne od modułu abstrakcji i wdrożyć zachowanie abstrakcji.

Moduły korzystające z zachowania zdefiniowanego w module abstrakcji powinny zależą od samej abstrakcji, a nie od konkretnych implementacji.

Rysunek 9. Zamiast modułów wysokiego poziomu bazujących bezpośrednio na modułach niskiego poziomu zależne od modułu abstrakcji.

Przykład

Wyobraź sobie moduł funkcji, który wymaga do działania bazy danych. Moduł funkcji nie jest związane ze sposobem wdrożenia bazy danych, np. lokalnej bazy danych sal lub zdalną instancję Firestore. Wystarczy, że będzie przechowywać i odczytywać dane aplikacji.

W tym celu moduł funkcji bazuje na module abstrakcji, niż w konkretnej implementacji baz danych. Ta abstrakcja definiuje API bazy danych. Innymi słowy, określa reguły interakcji z w bazie danych. Dzięki temu moduł funkcji może używać dowolnej bazy danych bez konieczności i poznać szczegóły implementacji.

Moduł konkretnych implementacji zapewnia rzeczywiste wdrożenie Interfejsy API zdefiniowane w module abstrakcji. W tym celu implementacja zależy też od modułu abstrakcji.

Wstrzykiwanie zależności

Być może zastanawiasz się, jak moduł funkcji jest połączony z modułu wdrożenia. Wynik to Wstrzykiwanie zależności. Funkcja moduł nie tworzy bezpośrednio wymaganej instancji bazy danych. Zamiast tego określa potrzebne zależności. Zależności te są następnie podawane zewnętrznie, zwykle w module aplikacji.

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

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

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

Zalety

Korzyści z oddzielenia interfejsów API i ich implementacji są następujące:

  • Wymienność: dzięki wyraźnemu oddzieleniu interfejsu API od implementacji modułów, można opracować wiele implementacji dla tego samego interfejsu API oraz przełączać bez konieczności zmiany kodu korzystającego z tego interfejsu. Może to być Jest to szczególnie przydatne w sytuacjach, gdy chcesz udostępniać różne treści umiejętności lub zachowania użytkowników w różnych kontekstach. Na przykład makiet do testowania, a nie do rzeczywistej implementacji w środowisku produkcyjnym.
  • Rozłączanie: oznacza to, że moduły korzystające z abstrakcyjnych funkcji nie zależy od konkretnej technologii. Jeśli zmienisz nazwę bazy danych z W przyszłości dostęp do Firestore będzie łatwiejszy, ponieważ zmiany występują w konkretnym module wykonującym zadanie (moduł implementacji), i nie wpływa na inne moduły za pomocą interfejsu API bazy danych.
  • Testowalność: oddzielenie interfejsów API od ich implementacji może i umożliwiają przeprowadzanie testów. Możesz pisać przypadki testowe na podstawie umów dotyczących interfejsu API. Dostępne opcje stosują też różne implementacje do testowania różnych scenariuszy i przypadków skrajnych, również na przykładach implementacji.
  • Większa wydajność kompilacji: gdy oddzielisz interfejs API od jego wdrożenia w różnych modułach, zmian w implementacji nie wymusza na systemie kompilacji ponownej kompilacji modułów w zależności od API. Przyspiesza to tworzenie i zwiększa produktywność, zwłaszcza w dużych projektach, w których czas tworzenia może być istotny.

Kiedy należy rozdzielać

Warto oddzielić interfejsy API od ich implementacji w następujących przypadkach:

  • Zróżnicowane możliwości: jeśli możesz wdrożyć wybrane części swojego systemu klarowny interfejs API umożliwia wymianę różnych implementacji. Możesz na przykład mieć system renderowania korzystający z trybu OpenGL. albo systemu rozliczeniowego Vulkan, który współpracuje z Google Play API.
  • Wiele aplikacji: jeśli tworzysz wiele aplikacji za pomocą wspólne możliwości dla różnych platform, można definiować wspólne interfejsy API opracować konkretne implementacje dla każdej platformy.
  • Niezależne zespoły: rozdzielenie umożliwia różnym programistom lub zespołom pracować nad różnymi częściami bazy kodu w tym samym czasie. Deweloperzy powinni się skupić poznawanie umów dotyczących interfejsu API i prawidłowe korzystanie z nich. Nie muszą szczegóły implementacji innych modułów.
  • Duża baza kodu: jeśli baza kodu jest duża lub złożona, rozdzielenie interfejsu API. co sprawia, że kod jest łatwiejszy do zarządzania. Umożliwia to naruszenie bazy kodu na bardziej szczegółowe, zrozumiałe i łatwe w utrzymaniu jednostki.

Jak wdrożyć?

Aby wdrożyć odwrócenie zależności, wykonaj te czynności:

  1. Utwórz moduł abstrakcji: ten moduł powinien zawierać interfejsy API (interfejsy). i modele), które definiują działanie danej cechy.
  2. Utwórz moduły implementacji: moduły implementacji powinny opierać się na API i zaimplementować zachowanie abstrakcji.
    Zamiast modułów wysokiego poziomu bazujących bezpośrednio na modułach niskiego poziomu zależne od modułu abstrakcji.
    Rysunek 10. Moduły implementacji zależą od modułu abstrakcji.
  3. Uzależnić moduły wysokiego poziomu od modułów abstrakcyjnych: zamiast w zależności od konkretnej implementacji, zadbaj o to, by Twoje moduły modułów abstrakcji. Moduły ogólne nie wymagają wdrożenia potrzebują tylko umowy (API).
    Moduły ogólne opierają się na abstrakcjach, a nie na implementacji.
    Rysunek 11. Moduły ogólne opierają się na abstrakcjach, a nie na implementacji.
  4. Moduł wdrażania: na koniec podaj rzeczywistą wartość dla zależności. Konkretna implementacja zależy od konfiguracji projektu, ale zwykle mieści się do tego moduł aplikacji. Aby udostępnić implementację, określ ją jako zależność dla wybranych wariant kompilacji lub zbiór źródeł testowych.
    Moduł aplikacji zapewnia faktyczną implementację.
    Rysunek 12. Moduł aplikacji zapewnia faktyczną implementację.

Ogólne sprawdzone metody

Jak wspomnieliśmy na początku, nie ma jednego słusznego sposobu aplikacja wielomodułowa. Tak jak jest wiele architektur oprogramowania, na wiele sposobów modularyzacji aplikacji. Mimo wszystko to ogólne które pomogą Ci zwiększyć czytelność i łatwiejszy w utrzymaniu kod możliwy do przetestowania.

Zadbaj o spójność konfiguracji

W każdym module są związane z konfiguracją. Jeśli liczba modułów osiąga określony próg, zarządzanie spójną konfiguracją staje się do wyzwania. Ważne jest na przykład, aby moduły używały zależności tego samego wersji. Jeśli musisz zaktualizować dużą liczbę modułów, aby tylko zestawić wersji zależnej, oznacza to nie tylko wysiłek, ale także miejsce na potencjalne błędów. Aby rozwiązać ten problem, możesz użyć jednego z narzędzi Gradle do scentralizuj konfigurację:

  • Katalogi wersji to lista zależności bezpiecznych typów. wygenerowanych przez Gradle podczas synchronizacji. To miejsce, w którym możesz zadeklarować zależności i jest dostępny dla wszystkich modułów w projekcie.
  • Używaj wtyczek konwencjonalnych, aby współdzielić logikę kompilacji między modułami.

Udostępniaj jak najmniej

Publiczny interfejs modułu powinien być minimalny i zawierać tylko podstawowe funkcje. Żadne szczegóły implementacji nie powinny wyciekać na zewnątrz. Zakres w najkrótszym możliwym zakresie. Użyj aplikacji private lub internal od Kotlina i określić zakres widoczności modułu, aby stał się prywatny. Podczas deklarowania zależności w module, użyj ścieżki implementation zamiast api. To ostatnie naraża użytkowników modułu na pośrednie zależności. Zastosowanie wdrożenie może skrócić czas kompilacji, ponieważ zmniejsza liczbę modułów które wymagają rekonstrukcji.

Preferuj Kotlin & Moduły Java

Android Studio obsługuje 3 podstawowe typy modułów:

  • Moduły aplikacji są punktem wejścia do aplikacji. Mogą zawierać kodu źródłowego, zasobów i zasobów oraz AndroidManifest.xml. Dane wyjściowe funkcji modułem aplikacji jest Android App Bundle (Android App Bundle) lub Android Application Package; (plik APK).
  • Moduły biblioteki mają tę samą treść co moduły aplikacji. Są używane przez inne moduły Androida jako zależność. Dane wyjściowe modułu biblioteki to archiwum Androida (AAR) są identyczne pod względem konstrukcji z modułami aplikacji, ale są skompilowane w plik Android Archive (AAR), którego mogą później używać inne jako zależność. Moduł biblioteki umożliwia obejmować i wykorzystywać ponownie tę samą logikę i zasoby w wielu modułach aplikacji.
  • Biblioteki Kotlin i Java nie zawierają żadnych zasobów ani zasobów Androida. plików manifestu.

Moduły na Androida są płatne, więc lepiej użyć Kotlin czy Java.