Liderzy opinii

Wdrożenie zasad SOLID w rozwoju oprogramowania Android

mm
Dodaj Unite.AI do preferowanych źródeł w Google

Pisanie oprogramowania jest aktem tworzenia, a rozwój oprogramowania Android nie jest wyjątkiem. Chodzi o coś więcej niż tylko sprawienie, by coś działało. Chodzi o projektowanie aplikacji, które mogą rosnąć, dostosowywać się i pozostawać zarządzalnymi w czasie.

Jako deweloper Android, który spotkał się z licznymi wyzwaniami architektonicznymi, odkryłem, że przestrzeganie zasad SOLID może przekształcić nawet najbardziej splątane bazy kodu w czyste systemy. Są to nie abstrakcyjne zasady, ale metody dające wyniki, które pozwalają pisać solidny, skalowalny i łatwy do utrzymania kod.

Artykuł ten dostarczy wglądu w to, jak zasady SOLID mogą być stosowane w rozwoju oprogramowania Android za pomocą przykładów z życia wziętych, praktycznych technik i doświadczeń zespołu Meta WhatsApp.

Poznaj zasady SOLID

Zasady SOLID, zaproponowane przez Roberta C. Martina, to pięć zasad projektowych dla programowania obiektowego, które gwarantują czystą i wydajną architekturę oprogramowania.

  • Zasada odpowiedzialności pojedynczej (SRP): Klasa powinna mieć tylko jeden powód do zmiany.
  • Zasada otwartej/zamkniętej (OCP): Jednostki oprogramowania powinny być otwarte na rozszerzenia, ale zamknięte na modyfikacje.
  • Zasada podstawienia Liskova (LSP): Podtypy muszą być podstawialne dla ich typów bazowych.
  • Zasada segregacji interfejsu (ISP): Interfejsy powinny być specyficzne dla klienta i nie powinny wymuszać implementacji niepotrzebnych metod.
  • Zasada odwrócenia zależności (DIP): Moduły wysokiego poziomu powinny zależeć od abstrakcji, a nie od modułów niskiego poziomu.

Integrując te zasady w rozwój oprogramowania Android, możemy tworzyć aplikacje, które są łatwiejsze do skalowania, testowania i utrzymania.

Zasada odpowiedzialności pojedynczej (SRP): uproszczenie odpowiedzialności

Zasada odpowiedzialności pojedynczej jest podstawą pisania utrzymywalnego kodu. Mówi ona, że każda klasa musi mieć jeden konkretny zakres odpowiedzialności. Częstym antywzorcem jest traktowanie aktywności lub fragmentów jako “klas bogów”, które zajmują się odpowiedzialnościami od renderowania interfejsu użytkownika, aż po pobieranie danych, obsługę błędów itd. Ten podejście sprawia, że testowanie i utrzymanie stają się koszmarem.

Z zasadą SRP, oddzielne problemy są rozdzielane na różne komponenty: na przykład, w aplikacji do wiadomości, tworzenie lub odczytywanie wiadomości.


class NewsRepository {
fun fetchNews(): List {
// Obsługa logiki pobierania danych
}
}

class NewsViewModel(private val newsRepository: NewsRepository) { fun loadNews(): LiveData<List> { // Zarządzanie stanem interfejsu użytkownika i przepływem danych } }

class NewsActivity : AppCompatActivity() { // Obsługa tylko renderowania interfejsu użytkownika }

 

Każda klasa ma tylko jedną odpowiedzialność; jest więc łatwo ją przetestować i zmodyfikować bez skutków ubocznych.

We współczesnym rozwoju oprogramowania Android, SRP jest najczęściej wdrażany wraz z zalecaną architekturą przy użyciu Jetpack. Na przykład, logika związana z manipulacją danymi może znajdować się wewnątrz ViewModel, podczas gdy aktywności lub fragmenty powinny tylko troszczyć się o interfejs użytkownika i interakcje. Pobieranie danych może być delegowane do oddzielnego repozytorium, albo z lokalnych baz danych, takich jak Room, albo warstw sieciowych, takich jak Retrofit. To redukuje ryzyko rozdęcia klas UI, ponieważ każdy komponent otrzymuje tylko jedną odpowiedzialność. Jednocześnie, Twój kod będzie znacznie łatwiejszy do testowania i wspierania.

Zasada otwartej/zamkniętej (OCP): projektowanie z myślą o rozszerzalności

Zasada otwartej/zamkniętej deklaruje, że klasa powinna być otwarta na rozszerzenia, ale nie na modyfikacje. Jest to bardziej uzasadnione dla aplikacji Android, które ciągle ewoluują i dodają nowe funkcje.

Najlepszym przykładem zastosowania zasady OCP w aplikacjach Android jest użycie interfejsów i klas abstrakcyjnych. Na przykład:


interface PaymentMethod {
fun processPayment(amount: Double)
}

class CreditCardPayment : PaymentMethod { override fun processPayment(amount: Double) { // Implementacja dla płatności kartą kredytową } }

class PayPalPayment : PaymentMethod { override fun processPayment(amount: Double) { // Implementacja dla płatności PayPal } }

 

Dodawanie nowych metod płatności nie wymaga zmian w istniejących klasach; wymaga tworzenia nowych klas. To sprawia, że system staje się elastyczny i może być skalowany.

W aplikacjach tworzonych dla urządzeń z Androidem, zasada otwartej/zamkniętej jest bardzo przydatna, gdy chodzi o przełączniki funkcji i konfiguracje pobierane dynamicznie. Na przykład, jeśli Twoja aplikacja ma interfejs AnalyticsTracker, który raportuje zdarzenia do różnych usług analitycznych, Firebase i Mixpanel, oraz niestandardowych śledczych wewnętrznych, każda nowa usługa może być dodana jako oddzielna klasa bez zmian w istniejącym kodzie. To utrzymuje Twój moduł analityczny otwarty na rozszerzenia – możesz dodać nowe śledcze, ale zamknięty na modyfikacje: nie musisz przepisywać istniejących klas za każdym razem, gdy dodajesz nową usługę.

Zasada podstawienia Liskova (LSP): zapewnienie zamienialności

Zasada podstawienia Liskova mówi, że podklasy powinny być zamienialne dla ich klas bazowych, a zachowanie aplikacji nie powinno ulegać zmianie. W Androidzie, ta zasada jest fundamentalna dla projektowania ponownie wykorzystywanych i przewidywalnych komponentów.

Na przykład, aplikacja do rysowania:


abstract class Shape {
abstract fun calculateArea(): Double
}

class Rectangle(private val width: Double, private val height: Double) : Shape() { override fun calculateArea() = width * height }

class Circle(private val radius: Double) : Shape() { override fun calculateArea() = Math.PI * radius * radius }

 

Obiekt Rectangle i Circle mogą być zastąpione przez dowolny inny w sposób zamienialny bez awarii systemu, co oznacza, że system jest elastyczny i przestrzega LSP.

Rozważmy podklasy RecyclerView.Adapter. Każda podklasa adaptera rozszerza się z RecyclerView.Adapter<VH> i zastępuje podstawowe funkcje, takie jak onCreateViewHolder, onBindViewHolder, i getItemCount. RecyclerView może używać dowolnej podklasy w sposób zamienialny, o ile te metody są implementowane poprawnie i nie naruszają funkcjonalności Twojej aplikacji. Tutaj LSP jest utrzymany, a Twój RecyclerView może być elastyczny i zamieniać dowolną podklasę adaptera według własnego uznania.

Zasada segregacji interfejsu (ISP): interfejsy zwarte i ukierunkowane

W większych aplikacjach jest powszechne definiowanie interfejsów z zbyt dużą odpowiedzialnością, szczególnie wokół sieci lub przechowywania danych. Zamiast tego, należy je rozbić na mniejsze, bardziej ukierunkowane interfejsy. Na przykład, interfejs ApiAuth odpowiedzialny za punkty końcowe uwierzytelniania użytkowników powinien być inny niż interfejs ApiPosts odpowiedzialny za posty na blogu lub social feed. To rozdzielenie zapobiega sytuacjom, w których klienci, którzy potrzebują tylko metod związanych z postami, są zmuszeni zależeć od i implementować wywołania uwierzytelniania, co utrzymuje Twój kod, a także pokrycie testowe, w bardziej zwartej formie.

Zasada segregacji interfejsu oznacza, że zamiast jednego dużego interfejsu, należy używać kilku mniejszych, ukierunkowanych. Zasada ta zapobiega sytuacjom, w których klasy implementują niepotrzebne metody.

Na przykład, zamiast jednego dużego interfejsu reprezentującego akcje użytkownika, rozważmy kod w Kotlinie:


interface Authentication {
fun login()
fun logout()
}

interface ProfileManagement { fun updateProfile() fun deleteAccount() }

 

Klasy, które implementują te interfejsy, mogą koncentrować się tylko na funkcjonalności, której potrzebują, co oczyszcza kod i czyni go bardziej utrzymywalnym.

Zasada odwrócenia zależności (DIP): abstrakcja zależności

Zasada odwrócenia zależności promuje odłączanie przez zapewnienie, że moduły wysokiego poziomu zależą od abstrakcji, a nie od konkretnych implementacji. Ta zasada idealnie współgra z nowoczesnymi praktykami rozwoju Android, szczególnie z frameworkami wstrzykiwania zależności, takimi jak Dagger i Hilt.

Na przykład:


class UserRepository @Inject constructor(private val apiService: ApiService) {
fun fetchUserData() {
// Pobiera dane użytkownika z abstrakcji
}
}

 

Tutaj, UserRepository zależy od abstrakcji ApiService, co sprawia, że jest elastyczny i testowalny. Ten podejście pozwala nam na zastąpienie implementacji, takiej jak użycie usługi mockowej podczas testowania.

Frameworki takie jak Hilt, Dagger i Koin ułatwiają wstrzykiwanie zależności, dostarczając sposób dostarczania zależności do komponentów Android. W repozytorium, na przykład, zamiast tworzenia implementacji Retrofit, wstrzyknięcie abstrakcji – na przykład interfejsu ApiService – pozwala łatwo przełączać się między implementacjami sieciowymi, na przykład między usługą mockową do testów lokalnych, bez konieczności zmiany czegokolwiek w kodzie repozytorium. W aplikacjach rzeczywistych można znaleźć klasy adnotowane @Inject lub @Provides, aby dostarczyć te abstrakcje, co sprawia, że aplikacja jest modułowa i przyjazna testom.

Praktyczne korzyści z zasad SOLID

Przyjęcie zasad SOLID w rozwoju oprogramowania Android daje nam wymierne korzyści:

  1. Poprawiona testowalność: Skoncentrowane klasy i interfejsy ułatwiają pisanie testów jednostkowych.
  2. Ulepszona utrzymywalność: Wyraźne rozdzielenie odpowiedzialności upraszcza debugowanie i aktualizacje.
  3. Skalowalność: Modułowe projekty umożliwiają łatwe dodawanie nowych funkcji.
  4. Współpraca: Dobrze zorganizowany kod ułatwia współpracę i redukuje czas wdrożenia nowych deweloperów.
  5. Optymalizacja wydajności: Cienkie, wydajne architektury minimalizują niepotrzebne przetwarzanie i użycie pamięci.

Przykłady z życia wzięte

W aplikacjach bogatych w funkcje, takich jak e-commerce lub aplikacje społecznościowe, zastosowanie zasad SOLID może znacznie zmniejszyć ryzyko regresji za każdym razem, gdy dodawana jest nowa funkcja lub usługa. Na przykład, jeśli nowy wymóg wymaga implementacji płatności wewnątrz aplikacji, można wprowadzić oddzielny moduł, który zaimplementuje wymagane interfejsy (Płatność, Analiza), bez zmiany istniejących modułów. Taki modułowy podejście, napędzany przez SOLID, pozwala aplikacji Android szybko dostosowywać się do wymagań rynku i utrzymuje bazę kodu w porządku w czasie.

Podczas pracy nad dużym projektem, który wymaga współpracy wielu deweloperów, jest bardzo zalecane utrzymanie złożonej bazy kodu z zasadami SOLID. Na przykład, oddzielenie pobierania danych, logiki biznesowej i obsługi interfejsu użytkownika w module czatu pomogło zmniejszyć ryzyko regresji podczas skalowania kodu z nowymi funkcjami. Podobnie, zastosowanie DIP było kluczowe do abstrakcji operacji sieciowych, co pozwoliło na zmianę z minimalnymi zakłóceniami między klientami sieciowymi.

Podsumowanie

Więcej niż teoretyczny przewodnik, zasady SOLID są praktyczną filozofią tworzenia odpornych, dostosowujących się i utrzymywalnych oprogramowań. W dynamicznym świecie rozwoju Android, gdzie wymagania zmieniają się niemal tak często, jak technologie, przestrzeganie tych zasad daje solidne podstawy, na których można zbudować sukces.

Dobry kod to nie tylko sprawienie, by coś działało – to stworzenie systemu, który może działać i rosnąć wraz z ewoluującymi potrzebami. Przyjmując zasady SOLID, nie tylko piszesz lepszy kod, ale także budujesz aplikacje, które są przyjemne w rozwoju, skalowaniu i utrzymaniu.

Farhana jest ekspertem w dziedzinie tworzenia aplikacji mobilnych, który z powodzeniem dostarczył wiele aplikacji mobilnych od podstaw. Prowadziła szkolenia z zakresu rozwoju aplikacji Android dla urzędników rządowych w Bhutanie, mentorowała wielu nowych pracowników i prowadziła zespoły do osiągnięcia sukcesu wspólnie.