Seguici sui social

Leader del pensiero

Implementazione dei principi SOLID nello sviluppo Android

mm

Scrivere software è un atto creativo e lo sviluppo Android non fa eccezione. Si tratta di molto più che far funzionare qualcosa. Si tratta di progettare applicazioni che possano crescere, adattarsi e rimanere gestibili nel tempo.

Come sviluppatore Android che ha affrontato innumerevoli sfide architettoniche, ho scoperto che aderire ai principi SOLID può trasformare anche le basi di codice più intricate in sistemi puliti. Questi non sono principi astratti, ma modi orientati ai risultati e riproducibili per scrivere codice robusto, scalabile e manutenibile.

Questo articolo fornirà approfondimenti su come i principi SOLID possono essere applicati allo sviluppo Android attraverso esempi concreti, tecniche pratiche ed esperienza del team di Meta WhatsApp.

Comprensione dei principi SOLID

I principi SOLID, proposti da Robert C. Martin, sono cinque principi di progettazione per la programmazione orientata agli oggetti che garantiscono un'architettura software pulita ed efficiente.

  • Principio di responsabilità unica (SRP): Una classe dovrebbe avere una e una sola ragione per cambiare.
  • Principio aperto/chiuso (OCP): Le entità software dovrebbero essere aperte alle estensioni ma chiuse alle modifiche.
  • Principio di sostituzione di Liskov (LSP): I sottotipi devono essere sostituibili con i loro tipi base.
  • Principio di segregazione dell'interfaccia (ISP): Le interfacce devono essere specifiche del client e non forzare l'implementazione di metodi inutilizzati.
  • Principio di inversione della dipendenza (DIP): I moduli di alto livello dovrebbero dipendere dalle astrazioni, non dai moduli di basso livello.

Integrando questi principi nello sviluppo Android, possiamo creare applicazioni più facili da scalare, testare e gestire.

Principio di responsabilità unica (SRP): semplificazione delle responsabilità

Il principio di responsabilità singola è il fondamento della scrittura di codice manutenibile. Afferma che ogni classe deve avere una sola preoccupazione di cui assumersi la responsabilità. Un anti-pattern comune è considerare le attività o i frammenti come delle "classi di Dio" che gestiscono le responsabilità a partire dal rendering dell'interfaccia utente, quindi il recupero dei dati, la gestione degli errori, ecc. Questo approccio crea un incubo di test e manutenzione.

Con l'SRP, separa le diverse attività in componenti diverse: ad esempio, in un'app per le notizie, crea o leggi le notizie.


class NewsRepository {
    fun fetchNews(): List {
        // Handles data fetching logic
    }
}

class NewsViewModel(private val newsRepository: NewsRepository) {
    fun loadNews(): LiveData<List> {
        // Manages UI state and data flow
    }
}

class NewsActivity : AppCompatActivity() {
    // Handles only UI rendering
}

 

Ogni classe ha una sola responsabilità; quindi è facile da testare e modificare senza effetti collaterali.

Nello sviluppo Android moderno, SRP è per lo più implementato insieme all'architettura consigliata usando Jetpack. Ad esempio, la logica relativa alla logica di manipolazione dei dati potrebbe risiedere all'interno di ViewModel, mentre le Attività o i Frammenti dovrebbero occuparsi solo dell'interfaccia utente e delle interazioni. Il recupero dei dati potrebbe essere delegato a un Repository separato, sia da database locali come Room o livelli di rete come Retrofit. Ciò riduce il rischio di bloat delle classi dell'interfaccia utente, poiché ogni componente ottiene una sola responsabilità. Allo stesso tempo, il tuo codice sarà molto più facile da testare e supportare.

Principio aperto/chiuso (OCP): progettazione per l'estensione

Il principio Open/Closed dichiara che una classe dovrebbe essere aperta per l'estensione ma non per la modifica. È più ragionevole per le applicazioni Android, poiché vengono costantemente aggiornate e aggiungono nuove funzionalità.

Il miglior esempio di come usare il principio OCP nelle applicazioni Android sono le interfacce e le classi astratte. Ad esempio:


interface PaymentMethod {
    fun processPayment(amount: Double)
}

class CreditCardPayment : PaymentMethod {
    override fun processPayment(amount: Double) {
        // Implementation for credit card payments
    }
}

class PayPalPayment : PaymentMethod {
    override fun processPayment(amount: Double) {
        // Implementation for PayPal payments
    }
}

 

Aggiungere nuovi metodi di pagamento non richiede modifiche alle classi esistenti; richiede la creazione di nuove classi. È qui che il sistema diventa flessibile e può essere scalato.

Nelle applicazioni create per dispositivi Android, il principio Open/Closed è piuttosto utile quando si tratta di toggle e configurazioni di funzionalità prese dinamicamente. Ad esempio, nel caso in cui la tua app abbia un'interfaccia di base AnalyticsTracker che segnala eventi a diversi servizi di analisi, Firebase e Mixpanel e tracker interni personalizzati, ogni nuovo servizio può essere aggiunto come una classe separata senza modifiche al codice esistente. Ciò mantiene il tuo modulo di analisi aperto per l'estensione (puoi aggiungere nuovi tracker) ma chiuso per la modifica: non riscrivi le classi esistenti ogni volta che aggiungi un nuovo servizio.

Principio di sostituzione di Liskov (LSP): garantire l'intercambiabilità

Il principio di sostituzione di Liskov afferma che le sottoclassi devono essere sostituibili per le loro classi base e il comportamento dell'applicazione non deve cambiare. In Android, questo principio è fondamentale per progettare componenti riutilizzabili e prevedibili.

Ad esempio, un'app di disegno:


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
}

 

Entrambi Rettangolo e Cerchio può essere sostituito da qualsiasi altro in modo intercambiabile senza che si verifichi un guasto del sistema, il che significa che il sistema è flessibile e segue LSP.

Considera Android RecyclerView.Adapter sottoclassi. Ogni sottoclasse dell'adattatore si estende da AdattatoreRecyclerView e sovrascrive le funzioni principali come onCreateViewHolder, onBindViewHoldere getItemCount. RiciclatoreVedi puoi usare qualsiasi sottoclasse in modo intercambiabile, a patto che tali metodi siano implementati correttamente e non interrompano la funzionalità della tua app. Qui, l'LSP viene mantenuto e il tuo RecyclerView può essere flessibile per sostituire qualsiasi sottoclasse dell'adattatore a piacimento.

Principio di segregazione dell'interfaccia (ISP): interfacce snelle e mirate

Nelle applicazioni più grandi, è comune definire interfacce con troppa responsabilità, specialmente per quanto riguarda la rete o l'archiviazione dei dati. Invece, suddividile in interfacce più piccole e mirate. Ad esempio, un'interfaccia ApiAuth responsabile degli endpoint di autenticazione utente dovrebbe essere diversa da un'interfaccia ApiPosts responsabile dei post del blog o degli endpoint dei feed social. Questa separazione impedirà ai client che necessitano solo dei metodi correlati ai post di essere costretti a dipendere e implementare chiamate di autenticazione, mantenendo quindi il codice, così come la copertura dei test, più snelli.

Il principio di segregazione delle interfacce significa che invece di avere interfacce grandi, se ne dovrebbero usare diverse più piccole e mirate. Il principio impedisce situazioni in cui le classi implementano metodi non necessari.

Ad esempio, anziché avere un'unica grande interfaccia che rappresenti le azioni degli utenti, prendiamo in considerazione il codice Kotlin:


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

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

 

Le classi che implementano queste interfacce possono concentrarsi solo sulla funzionalità di cui hanno bisogno, ripulendo così il codice e rendendolo più gestibile.

Principio di inversione della dipendenza (DIP): astrazione delle dipendenze

Il Dependency Inversion Principle promuove il disaccoppiamento assicurando che i moduli di alto livello dipendano da astrazioni piuttosto che da implementazioni concrete. Questo principio si allinea perfettamente con le moderne pratiche di sviluppo di Android, in particolare con framework di iniezione di dipendenza come Dagger e Hilt.

Per esempio:


class UserRepository @Inject constructor(private val apiService: ApiService) {
    fun fetchUserData() {
        // Fetches user data from an abstraction
    }
}

 

Qui, Repository utente dipende dall'astrazione Servizio API, rendendolo flessibile e testabile. Questo approccio ci consente di sostituire l'implementazione, ad esempio utilizzando un servizio fittizio durante i test.

Framework come Hilt, Dagger e Koin facilitano l'iniezione di dipendenza fornendo un modo per fornire dipendenze ai componenti Android, eliminando la necessità di istanziarle direttamente. In un repository, ad esempio, invece di istanziare un'implementazione Retrofit, inietterai un'astrazione, ad esempio un'interfaccia ApiService. In questo modo, potresti facilmente cambiare l'implementazione di rete, ad esempio un servizio fittizio in memoria per i test locali, e non avresti bisogno di cambiare nulla nel codice del tuo repository. Nelle applicazioni reali, puoi scoprire che le classi sono annotate con @Inject o @Provides per fornire queste astrazioni, rendendo quindi la tua app modulare e adatta ai test.

Vantaggi pratici dei principi SOLID

L'adozione dei principi SOLID nello sviluppo Android produce vantaggi tangibili:

  1. Testabilità migliorata: Le classi e le interfacce mirate semplificano la scrittura di test unitari.
  2. Manutenibilità migliorata: Una netta separazione delle problematiche semplifica il debug e gli aggiornamenti.
  3. Scalabilità: I design modulari consentono l'aggiunta di funzionalità senza soluzione di continuità.
  4. Collaborazione: Un codice ben strutturato facilita il lavoro di squadra e riduce i tempi di onboarding per i nuovi sviluppatori.
  5. Ottimizzazione delle prestazioni: Le architetture snelle ed efficienti riducono al minimo l'elaborazione non necessaria e l'utilizzo di memoria.

Applicazioni del mondo reale

Nelle applicazioni ricche di funzionalità, come le app di e-commerce o social network, l'applicazione dei principi SOLID può ridurre notevolmente il rischio di regressioni ogni volta che viene aggiunta una nuova funzionalità o un nuovo servizio. Ad esempio, se un nuovo requisito richiede un flusso di acquisto in-app, puoi introdurre un modulo separato che implementerà le interfacce richieste (pagamento, analisi) senza toccare i moduli esistenti. Questo tipo di approccio modulare, guidato da SOLID, consente alla tua app Android di adattarsi rapidamente alle richieste del mercato e impedisce che la base di codice si trasformi in spaghetti nel tempo.

Mentre si lavora su un progetto di grandi dimensioni che richiede la collaborazione di molti sviluppatori, è altamente consigliato mantenere una base di codice complessa con principi SOLID. Ad esempio, separare il recupero dei dati, la logica aziendale e la gestione dell'interfaccia utente nel modulo chat ha contribuito a ridurre la possibilità di regressioni durante il ridimensionamento del codice con nuove funzionalità. Allo stesso modo, l'applicazione di DIP è stata fondamentale per le operazioni di rete astratte, quindi è stata in grado di cambiare quasi senza interruzioni tra i client di rete.

Conclusione

Più che una guida teorica, i principi di SOLID sono in realtà la filosofia pratica per creare software resiliente, adattabile e manutenibile. Nel mondo in rapida evoluzione dello sviluppo Android, con requisiti che cambiano quasi con la stessa frequenza delle tecnologie, l'aderenza a questi principi fornisce una base solida su cui può essere fondato il successo.

Un buon codice non consiste solo nel far funzionare qualcosa, ma nel creare un sistema che possa continuare a funzionare e crescere con esigenze in continua evoluzione. Adottando i principi SOLID, non solo scriverai codice migliore, ma creerai anche applicazioni che sono un piacere da sviluppare, scalare e gestire.

Farhana è un'esperta sviluppatrice di applicazioni mobili che ha distribuito con successo numerose applicazioni mobili da zero. Ha condotto corsi di formazione sullo sviluppo Android per funzionari governativi ICT in Bhutan, ha fatto da mentore a molti nuovi arrivati ​​e ha guidato i team per raggiungere il successo insieme.