JavaRush /Java Blog /Random-IT /Cinque principi di base della progettazione di classi (SO...
Ve4niY
Livello 14

Cinque principi di base della progettazione di classi (SOLID) in Java

Pubblicato nel gruppo Random-IT
Le classi sono i blocchi da cui viene creata un'applicazione. Proprio come i mattoni di un edificio. Le lezioni scritte male possono causare problemi un giorno. Cinque principi di base della progettazione di classi (SOLID) in Java - 1Per capire se una lezione è scritta correttamente puoi controllare gli “standard di qualità”. In Java, questi sono i cosiddetti principi SOLID. Parliamo di loro.

Principi SOLIDI in Java

SOLID è un acronimo formato dalle lettere maiuscole dei primi cinque principi dell'OOP e del design. I principi furono inventati da Robert Martin all'inizio degli anni 2000 e l'acronimo fu successivamente coniato da Michael Feathers. Ecco cosa includono i principi SOLID:
  1. Principio di responsabilità unica.
  2. Principio aperto e chiuso.
  3. Principio di sostituzione di Liskov.
  4. Principio di segregazione delle interfacce.
  5. Principio di inversione delle dipendenze.

Principio di responsabilità unica (SRP)

Questo principio afferma che non dovrebbe mai esserci più di una ragione per cambiare classe. Ogni oggetto ha una responsabilità, completamente incapsulata in una classe. Tutti i servizi di classe sono finalizzati ad assicurare questa responsabilità. Tali classi saranno sempre facili da cambiare se necessario, perché è chiaro di cosa è responsabile la classe e cosa no. Cioè, sarà possibile apportare modifiche e non aver paura delle conseguenze: l'impatto su altri oggetti. E tale codice è molto più semplice da testare, perché copri una funzionalità con test isolati da tutte le altre. Immagina un modulo che elabora gli ordini. Se l'ordine è formato correttamente, lo salva nel database e invia una email 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 to the database

        return true;
    }

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

        // Sending a letter to the client
    }
}
Tale modulo può cambiare per tre ragioni. In primo luogo, la logica di elaborazione dell'ordine può essere diversa, in secondo luogo, il metodo di salvataggio (tipo di database), in terzo luogo, il metodo di invio di una lettera di conferma (ad esempio, invece di e-mail è necessario inviare SMS). Il principio di responsabilità unica implica che i tre aspetti di questo problema siano in realtà tre responsabilità diverse. Ciò significa che devono appartenere a classi o moduli diversi. Combinare più entità che possono cambiare in momenti diversi e per ragioni diverse è considerata una cattiva decisione progettuale. È molto meglio dividere il modulo in tre moduli distinti, ognuno dei quali svolgerà una sola funzione:
public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // save the order to the database

        return true;
    }
}

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

        // Sending a letter to the client
    }
}

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 di apertura/chiusura (OCP)

Questo principio è brevemente descritto come segue: le entità software (classi, moduli, funzioni, ecc.) devono essere aperte all'estensione, ma chiuse al cambiamento . Ciò significa che dovrebbe essere possibile modificare il comportamento esterno di una classe senza apportare modifiche fisiche alla classe stessa. Seguendo questo principio, le classi vengono sviluppate in modo tale che per adattare la classe a specifiche condizioni applicative è sufficiente estenderla e ridefinire alcune funzioni. Pertanto, il sistema deve essere flessibile, in grado di funzionare in condizioni variabili senza modificare il codice sorgente. Continuando con il nostro esempio di ordine, diciamo che dobbiamo eseguire alcune azioni prima che l'ordine venga elaborato e dopo l'invio dell'e-mail di conferma. Invece di cambiare la classe stessa OrderProcessor, la estenderemo e raggiungeremo una soluzione al problema in questione senza violare il principio OCP:
public class OrderProcessorWithPreAndPostProcessing extends OrderProcessor {

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

    private void beforeProcessing() {
        // Perform some actions before processing the order
    }

    private void afterProcessing() {
        // Perform some actions after order processing
    }
}

Principio di sostituzione di Barbara Liskov (LSP)

Questa è una variazione del principio aperto/chiuso discusso in precedenza. Può essere descritto come segue: gli oggetti in un programma possono essere sostituiti dai loro eredi senza modificare le proprietà del programma. Ciò significa che una classe sviluppata estendendo una classe base deve sovrascrivere i suoi metodi in modo da non interrompere la funzionalità 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 dei metodi sovrascritti. Le sottoclassi devono sovrascrivere i metodi della classe base in modo da non interrompere la funzionalità dal punto di vista del client. Ciò può essere esaminato in dettaglio utilizzando il seguente esempio. Supponiamo di avere una classe responsabile della convalida dell'ordine e che controlla se tutti gli articoli dell'ordine sono disponibili. Questa classe ha un metodo isValidche 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: controlla se tutta la merce nell'ordine è disponibile e se tutta la merce è imballata. Per fare ciò, abbiamo esteso la classe OrderStockValidatorcon la classe OrderStockAndPackValidator:
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;
    }
}
Tuttavia, in questa classe abbiamo violato il principio LSP, perché invece di restituire false se l'ordine non ha superato la validazione, il nostro metodo lancia un'eccezione IllegalStateException. I client di questo codice non si aspettano questo: si aspettano che venga restituito true o false . Ciò potrebbe causare errori nel programma.

Principio di suddivisione dell'interfaccia (ISP)

Caratterizzato dalla seguente dichiarazione: i client non dovrebbero essere costretti a implementare metodi che non utilizzeranno . Il principio della separazione delle interfacce suggerisce che le interfacce troppo “spesse” debbano essere divise in interfacce più piccole e più specifiche, in modo che i client di interfacce piccole conoscano solo i metodi necessari per il loro lavoro. Di conseguenza, quando si modifica un metodo di interfaccia, i client che non utilizzano questo metodo non dovrebbero cambiare. Diamo un'occhiata a un esempio. Lo sviluppatore Alex ha creato l'interfaccia "report" e ha aggiunto due metodi: generateExcel()e generatedPdf(). Ora il cliente A desidera utilizzare questa interfaccia, ma intende utilizzare solo report PDF e non Excel. Sarà soddisfatto di questa funzionalità? NO. Dovrà implementare due metodi, uno dei quali è in gran parte inutile ed esiste solo grazie ad Alex, il progettista del software. Il client utilizzerà un'interfaccia diversa o lascerà vuoto il campo Excel. Allora qual è la soluzione? Consiste nel dividere l'interfaccia esistente in due più piccole. Uno è un report in formato PDF, il secondo è un report in formato Excel. Ciò darà all'utente la possibilità di utilizzare solo le funzionalità a lui necessarie.

Principio di inversione delle dipendenze (DIP)

Questo principio SOLIDO in Java è descritto come segue: le dipendenze all'interno del sistema sono costruite sulla base di astrazioni . I moduli di livello superiore sono indipendenti dai moduli di livello inferiore. Le astrazioni non dovrebbero dipendere dai dettagli. I dettagli devono dipendere dalle astrazioni. Il software deve essere progettato in modo che i vari moduli siano autonomi e si connettano tra loro utilizzando l'astrazione. Una classica applicazione di questo principio è il framework Spring. All'interno del framework Spring, tutti i moduli sono implementati come componenti separati che possono funzionare insieme. Sono così autonomi che possono essere utilizzati altrettanto facilmente in altri moduli software oltre al framework Spring. Ciò si ottiene attraverso la dipendenza di principi chiusi e aperti. Tutti i moduli forniscono accesso solo a un'astrazione che può essere utilizzata in un altro modulo. Proviamo a dimostrarlo con un esempio. Parlando del principio di esclusiva responsabilità, ne abbiamo considerati alcuni OrderProcessor. Diamo un'altro sguardo 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, il nostro OrderProcessordipende da due classi specifiche MySQLOrderRepositorye ConfirmationEmailSender. Presentiamo anche il codice per queste classi:
public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // save the order to the database

        return true;
    }
}

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

        // Sending a letter to the client
    }
}
Queste classi sono lungi dall'essere chiamate astrazioni. E dal punto di vista del principio DIP, sarebbe più corretto iniziare creando alcune astrazioni che ci permetteranno di operare con esse in futuro, piuttosto che con implementazioni specifiche. Creiamo due interfacce MailSendere OrderRepository, che diventeranno le nostre astrazioni:
public interface MailSender {
    void sendConfirmationEmail(Order order);
}

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

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

        // Sending a letter to the client
    }

}

public class MySQLOrderRepository implements OrderRepository {

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

        return true;
    }
}
Abbiamo svolto il lavoro preparatorio in modo che la nostra lezione OrderProcessornon dipenda da dettagli concreti, ma da astrazioni. Apportiamo modifiche introducendo le nostre dipendenze nel 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);
        }
    }
}
La nostra classe ora dipende da astrazioni piuttosto che da implementazioni concrete. Puoi facilmente modificarne il comportamento inserendo la dipendenza desiderata al momento della creazione dell'istanza OrderProcessor. Abbiamo esaminato SOLID: i principi di progettazione in Java. Maggiori informazioni sull'OOP in generale, le basi di questo linguaggio di programmazione - non noioso e con centinaia di ore di pratica - nel corso JavaRush. È ora di risolvere alcuni problemi :) Cinque principi di base della progettazione di classi (SOLID) in Java - 2
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION