Tutto quello che devi sapere sui principi solidi in Java



In questo articolo imparerai in dettaglio cosa sono i principi Solid in java con esempi e la loro importanza con esempi di vita reale.

Nel mondo di (OOP), ci sono molte linee guida, modelli o principi di progettazione. Cinque di questi principi sono solitamente raggruppati insieme e sono noti con l'acronimo SOLID. Sebbene ciascuno di questi cinque principi descriva qualcosa di specifico, si sovrappongono anche in modo tale che l'adozione di uno di essi implica o porta ad adottarne un altro. In questo articolo capiremo i principi SOLID in Java.

Storia dei principi SOLID in Java

Robert C. Martin ha fornito cinque principi di progettazione orientati agli oggetti e viene utilizzato l'acronimo 'S.O.L.I.D'. Quando utilizzi tutti i principi di S.O.L.I.D in modo combinato, diventa più facile per te sviluppare software che può essere gestito facilmente. Le altre caratteristiche dell'utilizzo di S.O.L.I.D sono:





  • Evita gli odori di codice.
  • Codice rifrattore rapido.
  • Può eseguire lo sviluppo di software adattivo o agile.

Quando utilizzi il principio S.O.L.I.D nella tua codifica, inizi a scrivere il codice che è sia efficiente che efficace.



Qual è il significato di S.O.L.I.D?

Solid rappresenta cinque principi di java che sono:

  • S : Principio di responsabilità unica
  • O : Principio aperto-chiuso
  • L : Principio di sostituzione di Liskov
  • io : Principio di segregazione dell'interfaccia
  • D : Principio di inversione delle dipendenze

In questo blog, discuteremo in dettaglio tutti i cinque principi SOLID di Java.



Principio di responsabilità unica in Java

Cosa dice?

Robert C. Martin lo descrive come una classe dovrebbe avere una sola e unica responsabilità.

Secondo il principio di responsabilità unica, dovrebbe esserci un solo motivo per cui una classe deve essere cambiata. Significa che una classe dovrebbe avere un compito da svolgere. Questo principio è spesso definito soggettivo.

Il principio può essere ben compreso con un esempio. Immagina che ci sia una classe che esegue le seguenti operazioni.

  • Collegato a un database

  • Legge alcuni dati dalle tabelle del database

  • Infine, scrivilo su un file.

Hai immaginato lo scenario? Qui la classe ha più motivi per cambiare, e pochi di questi sono la modifica dell'output del file, l'adozione di nuovi database. Quando parliamo di responsabilità del principio unico, diremmo, ci sono troppe ragioni per cui la classe cambia, quindi non si adatta adeguatamente al principio di responsabilità unica.

Ad esempio, una classe Automobile può avviarsi o arrestarsi ma il compito di lavarla appartiene alla classe CarWash. In un altro esempio, una classe Book ha proprietà per memorizzare il proprio nome e testo. Ma il compito di stampare il libro deve appartenere alla classe Book Printer. La classe Book Printer potrebbe stampare sulla console o su un altro supporto, ma tali dipendenze vengono rimosse dalla classe Book

Perché è necessario questo principio?

Quando si segue il principio di responsabilità unica, il test è più facile. Con un'unica responsabilità, la classe avrà meno casi di test. Meno funzionalità significa anche meno dipendenze da altre classi. Porta a una migliore organizzazione del codice poiché le classi più piccole e ben definite sono più facili da cercare.

Un esempio per chiarire questo principio:

Supponiamo che ti venga chiesto di implementare un servizio di UserSetting in cui l'utente può modificare le impostazioni ma prima di questo deve essere autenticato. Un modo per implementarlo sarebbe:

public class UserSettingService {public void changeEmail (User user) {if (checkAccess (user)) {// Grant option to change}} public boolean checkAccess (User user) {// Verifica se l'utente è valido. }}

Tutto sembra a posto finché non si desidera riutilizzare il codice checkAccess in qualche altro posto OPPURE si desidera apportare modifiche al modo in cui viene eseguito checkAccess. In tutti e 2 i casi finiresti per cambiare la stessa classe e nel primo caso dovresti usare UserSettingService per controllare anche l'accesso.
Un modo per correggere questo problema consiste nello scomporre UserSettingService in UserSettingService e SecurityService. E sposta il codice checkAccess in SecurityService.

public class UserSettingService {public void changeEmail (User user) {if (SecurityService.checkAccess (user)) {// Grant option to change}}} public class SecurityService {public static boolean checkAccess (User user) {// controlla l'accesso. }}

Principio aperto chiuso in Java

Robert C. Martin lo descrive come i componenti software dovrebbero essere aperti per l'estensione, ma chiusi per la modifica.

Per essere precisi, secondo questo principio, una classe dovrebbe essere scritta in modo tale da svolgere il suo lavoro in modo impeccabile senza il presupposto che le persone in futuro verranno semplicemente e la cambieranno. Quindi, la classe dovrebbe rimanere chiusa per la modifica, ma dovrebbe avere la possibilità di essere estesa. I modi per estendere la classe includono:

  • Ereditando dalla classe

  • Sovrascrivere i comportamenti richiesti dalla classe

  • Estendere alcuni comportamenti della classe

Un eccellente esempio di principio aperto-chiuso può essere compreso con l'aiuto dei browser. Ti ricordi di aver installato estensioni nel tuo browser Chrome?

La funzione di base del browser Chrome è quella di navigare in diversi siti. Vuoi controllare la grammatica quando scrivi un'e-mail utilizzando il browser Chrome? Se sì, puoi semplicemente usare l'estensione Grammarly, ti fornisce il controllo grammaticale sul contenuto.

Questo meccanismo in cui stai aggiungendo cose per aumentare la funzionalità del browser è un'estensione. Quindi, il browser è un perfetto esempio di funzionalità che è aperto per l'estensione ma è chiuso per la modifica. In parole semplici, puoi migliorare la funzionalità aggiungendo / installando plugin sul tuo browser, ma non puoi creare nulla di nuovo.

Perché è richiesto questo principio?

OCP è importante poiché le classi possono arrivare a noi tramite librerie di terze parti. Dovremmo essere in grado di estendere quelle classi senza preoccuparci se quelle classi base possono supportare le nostre estensioni. Ma l'ereditarietà può portare a sottoclassi che dipendono dall'implementazione della classe base. Per evitare ciò, si consiglia l'uso di interfacce. Questa ulteriore astrazione porta a un accoppiamento lento.

Diciamo che dobbiamo calcolare aree di varie forme. Iniziamo con la creazione di una classe per la nostra prima forma Rettangoloche ha 2 attributi di lunghezza& larghezza.

public class Rectangle {public double length public double width}

Successivamente creiamo una classe per calcolare l'area di questo rettangoloche ha un metodo calcolaRettangoloAreache prende il rettangolocome parametro di input e calcola la sua area.

public class AreaCalculator {public double calcolaRectangleArea (Rectangle rectangle) {return rectangle.length * rectangle.width}}

Fin qui tutto bene. Supponiamo ora di ottenere il nostro secondo cerchio di forma. Quindi creiamo prontamente un nuovo Cerchio di classecon un unico raggio di attributo.

public class Circle {public double radius}

Quindi modifichiamo Areacalculatorclasse per aggiungere i calcoli del cerchio attraverso un nuovo metodo calcolaCircleaArea ()

public class AreaCalculator {public double calcolaRectangleArea (Rectangle rectangle) {return rectangle.length * rectangle.width} public double calcolaCircleArea (Circle circle) {return (22/7) * circle.radius * circle.radius}}

Tuttavia, tieni presente che c'erano dei difetti nel modo in cui abbiamo progettato la nostra soluzione sopra.

Diciamo che abbiamo un nuovo pentagono di forma. In tal caso, finiremo di nuovo per modificare la classe AreaCalculator. Man mano che i tipi di forme crescono, questo diventa più complicato poiché AreaCalculator continua a cambiare e tutti i consumatori di questa classe dovranno continuare ad aggiornare le proprie librerie che contengono AreaCalculator. Di conseguenza, la classe AreaCalculator non sarà definita (finalizzata) con garanzia poiché ogni volta che viene creata una nuova forma, verrà modificata. Quindi, questo design non è chiuso per la modifica.

AreaCalculator dovrà continuare ad aggiungere la propria logica di calcolo nei metodi più recenti. Non stiamo davvero ampliando la portata delle forme, piuttosto stiamo semplicemente facendo una soluzione a pezzi (bit per bit) per ogni forma aggiunta.

Modifica del progetto di cui sopra per conformarsi al principio aperto / chiuso:

Vediamo ora un design più elegante che risolve i difetti del design di cui sopra aderendo al Principio Aperto / Chiuso. Innanzitutto renderemo estensibile il design. Per questo dobbiamo prima definire un tipo di base Shape e avere Circle & Rectangle implementare l'interfaccia Shape.

interfaccia pubblica Forma {public double calcolaArea ()} public class Rectangle implementa Shape {double length double width public double calcolaArea () {return length * width}} public class Circle implementa Shape {public double radius public double calcolaArea () {return (22 / 7) * raggio * raggio}}

C'è un'interfaccia di base Shape. Tutte le forme ora implementano l'interfaccia di base Shape. L'interfaccia Shape ha un metodo astratto calcolaArea (). Sia il cerchio che il rettangolo forniscono la propria implementazione sovrascritta del metodo calcolareArea () utilizzando i propri attributi.
Abbiamo introdotto un certo grado di estensibilità poiché le forme sono ora un'istanza delle interfacce di Shape. Questo ci permette di usare Shape invece di singole classi
L'ultimo punto sopra menzionato consumatore di queste forme. Nel nostro caso, il consumatore sarà la classe AreaCalculator che ora sarebbe simile a questa.

public class AreaCalculator {public double calcolaShapeArea (Shape shape) {return shape.calculateArea ()}}

Questo AreaCalculatorclass ora rimuove completamente i nostri difetti di progettazione sopra indicati e fornisce una soluzione pulita che aderisce al principio aperto-chiuso. Andiamo avanti con altri principi SOLID in Java

Principio di sostituzione di Liskov in Java

Robert C. Martin lo descrive come i tipi derivati ​​devono essere completamente sostituibili con i loro tipi di base.

Il principio di sostituzione di Liskov assume che q (x) sia una proprietà, dimostrabile su entità di x che appartengono al tipo T. Ora, secondo questo principio, q (y) dovrebbe ora essere dimostrabili per oggetti y che appartengono al tipo S, e la S è in realtà un sottotipo di T. Ora sei confuso e non sai cosa significhi effettivamente il principio di sostituzione di Liskov? La definizione potrebbe essere un po 'complessa, ma in realtà è abbastanza semplice. L'unica cosa è che ogni sottoclasse o classe derivata dovrebbe essere sostituibile con la propria classe genitore o base.

Puoi dire che è un principio orientato agli oggetti unico. Il principio può essere ulteriormente semplificato da un tipo di bambino di un particolare tipo di genitore senza complicazioni o far saltare le cose dovrebbe avere la capacità di sostituire quel genitore.Questo principio è strettamente correlato al principio di sostituzione di Liskov.

Perché è richiesto questo principio?

Ciò evita l'abuso dell'eredità. Ci aiuta a conformarci alla relazione 'è-un'. Possiamo anche dire che le sottoclassi devono soddisfare un contratto definito dalla classe base. In questo senso, è correlato aDesign per contrattoche è stato descritto per la prima volta da Bertrand Meyer. Ad esempio, si è tentati di dire che un cerchio è un tipo di ellisse ma i cerchi non hanno due fuochi o assi maggiore / minore.

L'LSP è comunemente spiegato usando l'esempio quadrato e rettangolo. se assumiamo una relazione ISA tra Square e Rectangle. Pertanto, chiamiamo 'Square is a Rectangle'. Il codice seguente rappresenta la relazione.

public class Rectangle {private int length private int breadth public int getLength () {return length} public void setLength (int length) {this.length = length} public int getBreadth () {return breadth} public void setBreadth (int breadth) { this.breadth = breadth} public int getArea () {return this.length * this.breadth}}

Di seguito è riportato il codice per Square. Notare che Square estende Rectangle.

public class Square extends Rectangle {public void setBreadth (int breadth) {super.setBreadth (breadth) super.setLength (breadth)} public void setLength (int length) {super.setLength (length) super.setBreadth (length)}}

In questo caso, proviamo a stabilire una relazione ISA tra Square e Rectangle in modo tale che la chiamata 'Square is a Rectangle' nel codice seguente inizi a comportarsi in modo imprevisto se viene passata un'istanza di Square. Verrà generato un errore di asserzione nel caso di controllo per 'Area' e controllo per 'Larghezza', sebbene il programma terminerà poiché l'errore di asserzione viene generato a causa del fallimento del controllo dell'area.

public class LSPDemo {public void calculateArea (Rectangle r) {r.setBreadth (2) r.setLength (3) assert r.getArea () == 6: printError ('area', r) assert r.getLength () == 3: printError ('length', r) assert r.getBreadth () == 2: printError ('breadth', r)} private String printError (String errorIdentifer, Rectangle r) {return 'Unexpected value of' + errorIdentifer + ' per esempio di '+ r.getClass (). getName ()} public static void main (String [] args) {LSPDemo lsp = new LSPDemo () // Viene passata un'istanza di Rectangle lsp.calculateArea (new Rectangle ()) // Viene passata un'istanza di Square lsp.calculateArea (new Square ())}}

La classe dimostra il principio di sostituzione di Liskov (LSP) Secondo il principio, le funzioni che utilizzano riferimenti alle classi base devono essere in grado di utilizzare oggetti di classe derivata senza saperlo.

Pertanto, nell'esempio mostrato di seguito, la funzione calcolaArea che utilizza il riferimento di 'Rettangolo' dovrebbe essere in grado di utilizzare gli oggetti della classe derivata come Square e soddisfare il requisito posto dalla definizione di Rectangle. Si dovrebbe notare che secondo la definizione di Rettangolo, quanto segue deve sempre essere vero dato i dati seguenti:

  1. La lunghezza deve sempre essere uguale alla lunghezza passata come input al metodo, setLength
  2. L'ampiezza deve sempre essere uguale all'ampiezza passata come input al metodo, setBreadth
  3. L'area deve essere sempre uguale al prodotto di lunghezza e larghezza

Nel caso in cui proviamo a stabilire una relazione ISA tra Square e Rectangle in modo tale da chiamare 'Square is a Rectangle', il codice sopra inizierebbe a comportarsi in modo imprevisto se viene passata un'istanza di Square L'errore di asserzione verrà generato in caso di controllo dell'area e controllo per l'ampiezza, anche se il programma verrà terminato poiché l'errore di asserzione viene generato a causa del mancato controllo dell'area.

La classe Square non necessita di metodi come setBreadth o setLength. La classe LSPDemo avrebbe bisogno di conoscere i dettagli delle classi derivate di Rectangle (come Square) per codificare in modo appropriato per evitare di generare errori. La modifica del codice esistente rompe in primo luogo il principio di aperto-chiuso.

Principio di segregazione dell'interfaccia

Robert C. Martin lo descrive come i client non dovrebbero essere costretti a implementare metodi non necessari che non useranno.

SecondoPrincipio di segregazione dell'interfacciaun client, indipendentemente da ciò che non dovrebbe mai essere costretto a implementare un'interfaccia che non utilizza o il client non dovrebbe mai essere obbligato a dipendere da alcun metodo, che non viene utilizzato da loro.Quindi fondamentalmente, i principi di segregazione dell'interfaccia come preferisci interfacce, che sono piccole ma specifiche del cliente invece che monolitiche e più grandi. In breve, sarebbe un male per te costringere il cliente a dipendere da una certa cosa, di cui non ha bisogno.

Ad esempio, una singola interfaccia di registrazione per la scrittura e la lettura dei registri è utile per un database ma non per una console. La lettura dei log non ha senso per un logger della console. Andando avanti con questo articolo sui principi SOLID in Java.

Perché è richiesto questo principio?

Supponiamo che esista un'interfaccia Ristorante che contiene metodi per accettare ordini da clienti in linea, clienti dial-in o telefonici e clienti walk-in. Contiene anche metodi per la gestione dei pagamenti online (per i clienti online) e di persona (per i clienti walk-in e per i clienti telefonici quando il loro ordine viene consegnato a casa).

Ora creiamo un'interfaccia Java per Restaurant e chiamiamola RestaurantInterface.java.

interfaccia pubblica RestaurantInterface {public void acceptOnlineOrder () public void takeTelephoneOrder () public void payOnline () public void walkInCustomerOrder () public void payInPerson ()}

Ci sono 5 metodi definiti in RestaurantInterface che sono per accettare ordini online, prendere ordini telefonici, accettare ordini da un cliente walk-in, accettare pagamenti online e accettare pagamenti di persona.

Iniziamo implementando RestaurantInterface per i clienti online come OnlineClientImpl.java

public class OnlineClientImpl implementa RestaurantInterface {public void acceptOnlineOrder () {// logic for place online order} public void takeTelephoneOrder () {// Not Applicable for Online Order throw new UnsupportedOperationException ()} public void payOnline () {// logic for payment online} public void walkInCustomerOrder () {// Not Applicable for Online Order throw new UnsupportedOperationException ()} public void payInPerson () {// Not Applicable for Online Order throw new UnsupportedOperationException ()}}
  • Poiché il codice precedente (OnlineClientImpl.java) è per gli ordini in linea, lancia UnsupportedOperationException.

  • I clienti online, telefonici e walk-in utilizzano l'implementazione di RestaurantInterface specifica per ciascuno di loro.

  • Le classi di implementazione per il client telefonico e il client walk-in avranno metodi non supportati.

  • Poiché i 5 metodi fanno parte di RestaurantInterface, le classi di implementazione devono implementarli tutti e 5.

  • I metodi che ciascuna delle classi di implementazione genera UnsupportedOperationException. Come puoi vedere chiaramente, implementare tutti i metodi è inefficiente.

  • Qualsiasi modifica in uno qualsiasi dei metodi di RestaurantInterface verrà propagata a tutte le classi di implementazione. La manutenzione del codice inizia quindi a diventare davvero complicata e gli effetti di regressione delle modifiche continueranno ad aumentare.

  • RestaurantInterface.java infrange il principio di responsabilità unica perché la logica per i pagamenti e quella per l'inserimento degli ordini sono raggruppate in un'unica interfaccia.

Per superare i problemi sopra menzionati, applichiamo il principio di segregazione dell'interfaccia per eseguire il refactoring del progetto precedente.

  1. Separare le funzionalità di pagamento e di posizionamento degli ordini in due interfacce snelle separate, PaymentInterface.java e OrderInterface.java.

  2. Ciascun client utilizza un'implementazione ciascuno di PaymentInterface e OrderInterface. Ad esempio, OnlineClient.java utilizza OnlinePaymentImpl e OnlineOrderImpl e così via.

  3. Il principio di responsabilità unica è ora allegato come interfaccia di pagamento (PaymentInterface.java) e interfaccia di ordinazione (OrderInterface).

  4. La modifica in una qualsiasi delle interfacce dell'ordine o di pagamento non influisce sull'altra. Sono indipendenti adesso. Non sarà necessario eseguire alcuna implementazione fittizia o generare un'eccezione UnsupportedOperationException poiché ogni interfaccia ha solo metodi che utilizzerà sempre.

Dopo aver applicato ISP

Principio di inversione delle dipendenze

Robert C. Martin lo descrive in quanto dipende dalle astrazioni e non dalle concrezioni. Secondo esso, il modulo di alto livello non deve mai fare affidamento su alcun modulo di basso livello. per esempio

Vai in un negozio locale per comprare qualcosa e decidi di pagarlo usando la tua carta di debito. Quindi, quando dai la tua carta all'impiegato per effettuare il pagamento, l'impiegato non si preoccupa di controllare che tipo di carta hai dato.

Anche se hai dato una carta Visa, non metterà fuori una macchina Visa per strisciare la tua carta. Il tipo di carta di credito o di debito che hai per pagare non ha nemmeno importanza, lo faranno semplicemente scorrere. Quindi, in questo esempio, puoi vedere che sia tu che l'impiegato dipendete dall'estrazione della carta di credito e non siete preoccupati per le specifiche della carta. Questo è il principio di inversione di dipendenza.

Perché è richiesto questo principio?

Consente a un programmatore di rimuovere le dipendenze hardcoded in modo che l'applicazione diventi liberamente accoppiata ed estendibile.

public class Studente {private Address address public Student () {address = new Address ()}}

Nell'esempio precedente, la classe Student richiede un oggetto Address ed è responsabile dell'inizializzazione e dell'utilizzo dell'oggetto Address. Se la classe dell'indirizzo viene modificata in futuro, dobbiamo apportare modifiche anche alla classe dello studente. Questo crea uno stretto accoppiamento tra gli oggetti Student e Address. Possiamo risolvere questo problema utilizzando il modello di progettazione dell'inversione delle dipendenze. Ad esempio, l'oggetto indirizzo verrà implementato in modo indipendente e verrà fornito a Student quando Student viene istanziato utilizzando l'inversione delle dipendenze basata su costruttore o setter.

Con questo, arriviamo alla fine di questi principi SOLID in Java.

quando usarlo in java

Controlla il da Edureka, una società di formazione online affidabile con una rete di oltre 250.000 studenti soddisfatti sparsi in tutto il mondo. Il corso di formazione e certificazione Java J2EE e SOA di Edureka è progettato per studenti e professionisti che desiderano diventare sviluppatori Java. Il corso è progettato per darti un vantaggio nella programmazione Java e formarti sia sui concetti di base che avanzati su Java insieme a vari framework Java come Hibernate e Spring.

Hai domande per noi? Si prega di menzionarlo nella sezione commenti di questo blog 'Principi SOLIDI in Java' e ti risponderemo il prima possibile.