Java >> Programma Java >  >> Java

Principi di progettazione SOLID

Introduzione:

Robert C. Martin ha definito cinque principi di progettazione orientati agli oggetti:

  • S Principio di responsabilità unica
  • O Principio della penna chiusa
  • L Principio di sostituzione di iskov
  • Io Principio di segregazione dell'interfaccia e
  • D Principio di inversione di dipendenza

Questi insieme sono popolarmente conosciuti come i principi SOLID. Quando progettiamo un sistema orientato agli oggetti, dovremmo cercare di attenerci a questi principi ove possibile. Questi principi ci aiutano a progettare un sistema più estensibile, comprensibile e manutenibile.

L'utilizzo di questi principi può aiutarci a risparmiare un sacco di sforzi man mano che le dimensioni delle nostre applicazioni crescono.

Principio di responsabilità unica:

Come suggerisce il nome, il Single-Responsibility Principle (SRP) afferma che ogni classe deve fare esattamente una cosa sola. In altre parole, non dovrebbe esserci più di un motivo per modificare una classe.

Come sappiamo, i grandi sistemi di solito hanno migliaia di classi. Se per qualsiasi nuovo requisito è necessario modificare più classi, ci sono più possibilità che introduciamo bug interrompendo un'altra funzionalità.

Il principio di responsabilità unica ci offre i seguenti vantaggi:

  • Meno accoppiamento: Poiché ogni classe farebbe solo una cosa, ci saranno molte meno dipendenze
  • Più facile da testare: il codice sarà molto probabilmente più facile da testare con molti meno casi di test che coprono il sistema nella sua interezza

Le classi modello del nostro sistema di solito seguono sempre il principio SRP. Ad esempio, dobbiamo modificare lo stato degli utenti nel nostro sistema, toccheremo solo l'Utente classe:

12345678 public class User {        private int id;      private String name;      private List<Address> addresses;           //constructors, getters, setters }

E quindi segue il principio SRP.

Principio aperto-chiuso:

Il principio Open-Closed afferma che i componenti software devono essere aperti per l'estensione ma chiusi per la modifica. L'intenzione qui è quella di evitare di introdurre bug nel sistema interrompendo alcune funzionalità di lavoro esistenti a causa delle modifiche al codice. Dovremmo piuttosto estendere la classe esistente per supportare qualsiasi funzionalità aggiuntiva.

Questa regola si applica alle classi più stabili del nostro sistema che hanno superato le fasi di test e stanno lavorando bene in produzione . Vorremo evitare di rompere qualcosa in quel codice esistente e quindi dovremmo piuttosto estendere le sue funzionalità supportate per soddisfare i nuovi requisiti.

Supponiamo di avere un EventPlanner class nel nostro sistema che funziona a lungo sui nostri server di produzione:

010203040506070809101112 public class EventPlanner {        private List<String> participants;      private String organizer;        public void planEvent() {          System.out.println( "Planning a simple traditional event" );          ...      }        ... }

Ma ora stiamo pianificando di avere un ThemeEventPlanner invece, che pianificherà gli eventi utilizzando un tema casuale per renderli più interessanti. Invece di saltare direttamente nel codice esistente e aggiungere la logica per selezionare un tema evento e usarlo, è meglio estendere la nostra classe stabile di produzione:

12345 public class ThemeEventPlanner extends EventPlanner {      private String theme;        ... }

Per i sistemi di grandi dimensioni, non sarà molto semplice identificare per tutti gli scopi che una classe potrebbe essere stata utilizzata. E quindi estendendo solo la funzionalità, riduciamo le possibilità di affrontare le incognite del sistema.

Principio di sostituzione di Liskov:

Il principio di sostituzione di Liskov afferma che un tipo derivato deve essere in grado di completare la sostituzione del suo tipo base senza alterare il comportamento esistente. Quindi, se abbiamo due classi A e B tale che B estende A, dovremmo essere in grado di sostituire A con B nella nostra intera base di codice senza influire sul comportamento del sistema.

Per poter raggiungere questo obiettivo, gli oggetti delle nostre sottoclassi devono comportarsi esattamente allo stesso modo degli oggetti della superclasse.

Questo principio ci aiuta a evitare relazioni errate tra i tipi in quanto possono causare bug o effetti collaterali imprevisti.

Vediamo l'esempio seguente:

010203040506070809101112 public class Bird {      public void fly() {          System.out.println( "Bird is now flying" );      } }   public class Ostrich extends Bird {      @Override      public void fly() {         throw new IllegalStateException( "Ostrich can't fly" );      } }

Anche se struzzo è un Uccello , ancora non può volare e quindi questa è una chiara violazione del principio di sostituzione di Liskov (LSP). Inoltre, i codici che coinvolgono la logica sui controlli di tipo sono una chiara indicazione che sono state stabilite le relazioni errate.

Esistono due modi per eseguire il refactoring del codice per seguire LSP:

  • Elimina le relazioni errate tra gli oggetti
  • Utilizza "Dillo, non chiedere ” principio per eliminare il controllo del tipo e la fusione

Diciamo che abbiamo del codice che coinvolge i controlli di tipo:

1234567 //main method code for (User user : listOfUsers) {      if (user instanceof SubscribedUser) {          user.offerDiscounts();      }      user.makePurchases(); }

Utilizzando "Dillo, non chiedere" principio, faremo il refactoring del codice sopra in modo che assomigli a:

0102030405060708091011121314 public class SubscribedUser extends User {      @Override      public void makePurchases() {          this .offerDiscounts();          super .makePurchases();      }        public void offerDiscounts() {...} }   //main method code for (User user : listOfUsers) {      user.makePurchases(); }

Principio di segregazione dell'interfaccia:

Secondo il principio di segregazione dell'interfaccia, i client non dovrebbero essere costretti a gestire i metodi che non utilizzano. Dovremmo dividere l'interfaccia più grande in quelle più piccole, ove necessario.

Supponiamo di avere un Carrello interfaccia:

12345678 public interface ShoppingCart {        void addItem(Item item);      void removeItem(Item item);      void makePayment();      boolean checkItemAvailability(Item item);     }

Effettuare pagamenti e verificare la disponibilità di un articolo non è ciò che un carrello è destinato a fare. C'è un'alta probabilità che incontriamo implementazioni di questa interfaccia che non utilizzeranno quei metodi.

Quindi, è una buona idea rompere l'interfaccia sopra come:

010203040506070809101112 public interface BaseShoppingCart {      void addItem(Item item);      void removeItem(Item item); }   public interface PaymentProcessor {      void makePayment(); }   public interface StockVerifier {      boolean checkItemAvailability(Item item); }

Il principio di segregazione dell'interfaccia (ISP) rafforza anche altri principi:

  • Principio di responsabilità unica: Le classi che implementano interfacce più piccole sono generalmente più mirate e di solito hanno un unico scopo
  • Principio di sostituzione di Liskov: Con interfacce più piccole, ci sono più possibilità che le classi le implementino per sostituire completamente l'interfaccia

Inversione di dipendenza:

È uno dei principi di progettazione più popolari e utili in quanto promuove l'accoppiamento libero tra gli oggetti. Il principio dell'inversione delle dipendenze afferma che i moduli di alto livello non dovrebbero dipendere da moduli di basso livello; entrambi dovrebbero dipendere dalle astrazioni.

I moduli di alto livello ci dicono cosa dovrebbe fare il software . Autorizzazione utente e pagamento sono esempi di moduli di alto livello.

D'altra parte, i moduli di basso livello ci dicono come il software dovrebbe svolgere vari compiti vale a dire coinvolge dettagli di implementazione. Alcuni esempi di moduli di basso livello includono sicurezza (OAuth), networking, accesso al database, IO, ecc.

Scriviamo un UserRepository interfaccia e la sua classe di implementazione:

01020304050607080910 public interface UserRepository {      List<User> findAllUsers(); } public class UserRepository implements UserRepository {        public List<User> findAllUsers() {          //queries database and returns a list of users          ...      } }

Abbiamo qui estratto l'astrazione del modulo in un'interfaccia.

Ora supponiamo di avere un modulo di alto livello Autorizzazione utente che controlla se un utente è autorizzato ad accedere a un sistema o meno. Useremo solo il riferimento di UserRepository interfaccia:

123456789 public class UserAuthorization {        ...        public boolean isValidUser(User user) {          UserRepository repo = UserRepositoryFactory.create();          return repo.getAllUsers().stream().anyMatch(u -> u.equals(user));      } }

Inoltre, stiamo utilizzando una classe factory per creare un'istanza di un UserRepository .

Nota che ci affidiamo solo all'astrazione e non alla concrezione. E così, possiamo facilmente aggiungere più implementazioni di UserRepository senza molto impatto sul nostro modulo di alto livello.

Com'è elegante!

Conclusione:

In questo tutorial, abbiamo discusso i principi di progettazione SOLID. Abbiamo anche esaminato gli esempi di codice in Java per ciascuno di questi principi.

Etichetta Java