CodeGym/Java Blog/Random-IT/SOLID: Cinque principi di base della progettazione di cla...
John Squirrels
Livello 41
San Francisco

SOLID: Cinque principi di base della progettazione di classi in Java

Pubblicato nel gruppo Random-IT
membri
Le classi sono gli elementi costitutivi delle applicazioni. Proprio come i mattoni di un edificio. Classi scritte male possono eventualmente causare problemi. SOLID: Cinque principi di base della progettazione di classi in Java - 1Per capire se una classe è scritta correttamente, puoi verificare come si misura con gli "standard di qualità". In Java, questi sono i cosiddetti principi SOLID, e ne parleremo.

Principi SOLID in Java

SOLID è un acronimo formato dalle lettere maiuscole dei primi cinque principi di OOP e class design. I principi sono stati espressi da Robert Martin nei primi anni 2000, e poi l'abbreviazione è stata introdotta successivamente da Michael Feathers. Ecco i SOLIDI principi:
  1. Principio di responsabilità unica
  2. Principio aperto chiuso
  3. Principio di sostituzione di Liskov
  4. Principio di segregazione dell'interfaccia
  5. Principio di inversione delle dipendenze

Principio di responsabilità unica (SRP)

Questo principio afferma che non dovrebbe mai esserci più di un motivo per cambiare classe. Ogni oggetto ha una responsabilità, che è completamente incapsulata nella classe. Tutti i servizi di una classe sono finalizzati a sostenere questa responsabilità. Tali classi saranno sempre facili da modificare se necessario, perché è chiaro di cosa è responsabile la classe e di cosa non è responsabile. In altre parole, potremo apportare modifiche e non avere paura delle conseguenze, ovvero l'impatto su altri oggetti. Inoltre, tale codice è molto più facile da testare, perché i tuoi test coprono una parte di funzionalità isolata da tutte le altre. Immagina un modulo che elabora gli ordini. Se un ordine è formato correttamente, questo modulo lo salva in un database e invia un'e-mail per confermare l'ordine:
public class OrderProcessor {

    public void process(Order order){
        if (order.isValid() && save(order)) {
            sendConfirmationEmail(order);
        }
    }

    private boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }

    private void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}
Questo modulo potrebbe cambiare per tre motivi. Innanzitutto, la logica per l'elaborazione degli ordini potrebbe cambiare. In secondo luogo, il modo in cui gli ordini vengono salvati (tipo di database) può cambiare. In terzo luogo, il modo in cui viene inviata la conferma può cambiare (ad esempio, supponiamo di dover inviare un messaggio di testo anziché un'e-mail). Il principio di responsabilità unica implica che i tre aspetti di questo problema siano in realtà tre responsabilità diverse. Ciò significa che dovrebbero essere in classi o moduli diversi. La combinazione di più entità che possono cambiare in momenti diversi e per motivi diversi è considerata una decisione di progettazione sbagliata. È molto meglio dividere un modulo in tre moduli separati, ognuno dei quali svolge una singola funzione:
public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}

public class ConfirmationEmailSender {
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}

public class OrderProcessor {
    public void process(Order order){

        MySQLOrderRepository repository = new MySQLOrderRepository();
        ConfirmationEmailSender mailSender = new ConfirmationEmailSender();

        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }

}

Principio aperto chiuso (OCP)

Questo principio è descritto come segue: le entità software (classi, moduli, funzioni, ecc.) dovrebbero essere aperte per l'estensione, ma chiuse per la modifica . Ciò significa che dovrebbe essere possibile modificare il comportamento esterno di una classe senza apportare modifiche al codice esistente della classe. Secondo questo principio, le classi sono progettate in modo che modificare una classe per soddisfare condizioni specifiche richieda semplicemente l'estensione e l'override di alcune funzioni. Ciò significa che il sistema deve essere flessibile, in grado di funzionare in condizioni mutevoli senza modificare il codice sorgente. Continuando il nostro esempio relativo all'elaborazione degli ordini, supponiamo di dover eseguire alcune azioni prima che un ordine venga elaborato e dopo l'invio dell'e-mail di conferma. Invece di cambiare ilOrderProcessorclass stessa, la estenderemo per raggiungere il nostro obiettivo senza violare il Principio Open Closed:
public class OrderProcessorWithPreAndPostProcessing extends OrderProcessor {

    @Override
    public void process(Order order) {
        beforeProcessing();
        super.process(order);
        afterProcessing();
    }

    private void beforeProcessing() {
        // Take some action before processing the order
    }

    private void afterProcessing() {
        // Take some action after processing the order
    }
}

Principio di sostituzione di Liskov (LSP)

Questa è una variazione del principio aperto chiuso che abbiamo menzionato in precedenza. Può essere definito come segue: gli oggetti possono essere sostituiti da oggetti di sottoclassi senza modificare le proprietà di un programma. Ciò significa che una classe creata estendendo una classe base deve eseguire l'override dei suoi metodi in modo che la funzionalità non sia compromessa dal punto di vista del client. Cioè, se uno sviluppatore estende la tua classe e la utilizza in un'applicazione, non dovrebbe modificare il comportamento previsto di alcun metodo sottoposto a override. Le sottoclassi devono sovrascrivere i metodi della classe base in modo che la funzionalità non venga interrotta dal punto di vista del client. Possiamo esplorare questo in dettaglio nel seguente esempio. Supponiamo di avere una classe responsabile della convalida di un ordine e del controllo se tutte le merci nell'ordine sono disponibili.isValid()metodo che restituisce true o false :
public class OrderStockValidator {

    public boolean isValid(Order order) {
        for (Item item : order.getItems()) {
            if (!item.isInStock()) {
                return false;
            }
        }

        return true;
    }
}
Supponiamo anche che alcuni ordini debbano essere convalidati in modo diverso rispetto ad altri, ad esempio per alcuni ordini dobbiamo verificare se tutte le merci nell'ordine sono disponibili e se tutte le merci sono imballate. Per fare ciò, estendiamo la OrderStockValidatorclasse creando la OrderStockAndPackValidatorclasse:
public class OrderStockAndPackValidator extends OrderStockValidator {

    @Override
    public boolean isValid(Order order) {
        for (Item item : order.getItems()) {
            if ( !item.isInStock() || !item.isPacked() ){
                throw new IllegalStateException(
                     String.format("Order %d is not valid!", order.getId())
                );
            }
        }

        return true;
    }
}
Ma qui abbiamo violato il principio di sostituzione di Liskov, perché invece di restituire false se l'ordine fallisce la convalida, il nostro metodo lancia un IllegalStateException. I client che utilizzano questo codice non si aspettano questo: si aspettano un valore di ritorno true o false . Questo può portare a errori di runtime.

Principio di segregazione dell'interfaccia (ISP)

Questo principio è caratterizzato dalla seguente affermazione: il client non dovrebbe essere costretto a implementare metodi che non utilizzerà . Il principio della segregazione dell'interfaccia significa che le interfacce troppo "spesse" devono essere divise in interfacce più piccole e più specifiche, in modo che i client che utilizzano interfacce piccole conoscano solo i metodi di cui hanno bisogno per il loro lavoro. Di conseguenza, quando un metodo di interfaccia cambia, tutti i client che non utilizzano quel metodo non dovrebbero cambiare. Considera questo esempio: Alex, uno sviluppatore, ha creato un'interfaccia "report" e ha aggiunto due metodi: generateExcel()egeneratedPdf(). Ora un cliente desidera utilizzare questa interfaccia, ma intende utilizzare solo report in formato PDF, non in Excel. Questa funzionalità soddisferà questo cliente? No. Il cliente dovrà implementare due metodi, uno dei quali in gran parte non serve ed esiste solo grazie ad Alex, colui che ha progettato il software. Il client utilizzerà un'interfaccia diversa o non farà nulla con il metodo per i report Excel. Quindi qual è la soluzione? Serve per dividere l'interfaccia esistente in due più piccole. Uno per i report PDF, l'altro per i report Excel. Ciò consente ai clienti di utilizzare solo le funzionalità di cui hanno bisogno.

Principio di inversione delle dipendenze (DIP)

In Java, questo principio SOLID è descritto come segue: le dipendenze all'interno del sistema sono costruite sulla base di astrazioni. I moduli di livello superiore non dipendono dai moduli di livello inferiore. Le astrazioni non dovrebbero dipendere dai dettagli. I dettagli dovrebbero dipendere dalle astrazioni. Il software deve essere progettato in modo che i vari moduli siano autonomi e collegati tra loro attraverso l'astrazione. Un'applicazione classica di questo principio è lo Spring Framework. In Spring Framework, tutti i moduli sono implementati come componenti separati che possono lavorare insieme. Sono così autonomi che possono essere utilizzati altrettanto facilmente in moduli di programma diversi da Spring Framework. Ciò si ottiene grazie alla dipendenza dei principi chiusi e aperti. Tutti i moduli forniscono l'accesso solo all'astrazione, che può essere utilizzata in un altro modulo. Proviamo a illustrarlo usando un esempio. Parlando del principio di responsabilità unica, abbiamo considerato ilOrderProcessorclasse. Diamo un'altra occhiata al codice di questa classe:
public class OrderProcessor {
    public void process(Order order){

        MySQLOrderRepository repository = new MySQLOrderRepository();
        ConfirmationEmailSender mailSender = new ConfirmationEmailSender();

        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }

}
In questo esempio, la nostra OrderProcessorclasse dipende da due classi specifiche: MySQLOrderRepositorye ConfirmationEmailSender. Presenteremo anche il codice di queste classi:
public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}

public class ConfirmationEmailSender {
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}
Queste classi sono lontane da ciò che chiameremmo astrazioni. E dal punto di vista del principio di inversione della dipendenza, sarebbe meglio iniziare creando alcune astrazioni con cui possiamo lavorare in futuro, piuttosto che implementazioni specifiche. Creiamo due interfacce: MailSendere OrderRepository). Queste saranno le nostre astrazioni:
public interface MailSender {
    void sendConfirmationEmail(Order order);
}

public interface OrderRepository {
    boolean save(Order order);
}
Ora implementiamo queste interfacce in classi che sono già state preparate per questo:
public class ConfirmationEmailSender implements MailSender {

    @Override
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }

}

public class MySQLOrderRepository implements OrderRepository {

    @Override
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}
Abbiamo svolto il lavoro preparatorio in modo che la nostra OrderProcessorclasse dipenda non da dettagli concreti, ma da astrazioni. Lo cambieremo aggiungendo le nostre dipendenze al costruttore della classe:
public class OrderProcessor {

    private MailSender mailSender;
    private OrderRepository repository;

    public OrderProcessor(MailSender mailSender, OrderRepository repository) {
        this.mailSender = mailSender;
        this.repository = repository;
    }

    public void process(Order order){
        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }
}
Ora la nostra classe dipende da astrazioni, non da implementazioni specifiche. Possiamo facilmente cambiare il suo comportamento aggiungendo la dipendenza desiderata al momento OrderProcessordella creazione di un oggetto. Abbiamo esaminato i principi di progettazione SOLID in Java. Imparerai di più sull'OOP in generale e sulle basi della programmazione Java - niente di noioso e centinaia di ore di pratica - nel corso CodeGym. È ora di risolvere alcuni compiti :)
Commenti
  • Popolari
  • Nuovi
  • Vecchi
Devi avere effettuato l'accesso per lasciare un commento
Questa pagina non ha ancora commenti