Tot el que heu de saber sobre principis sòlids a Java

En aquest article aprendreu detalladament què són els principis sòlids a Java amb exemples i la seva importància amb els exemples de la vida real.

Al món de (OOP), hi ha moltes pautes, patrons o principis de disseny. Cinc d'aquests principis solen agrupar-se i es coneixen amb l'acrònim SOLID. Tot i que cadascun d’aquests cinc principis descriu quelcom específic, també es superposen de manera que adoptar un d’ells implica o condueix a adoptar-ne un altre. En aquest article coneixerem els principis de SOLID a Java.



Història dels principis SOLID a Java

Robert C. Martin va donar cinc principis de disseny orientats a objectes i s’utilitza l’acrònim “S.O.L.I.D”. Quan utilitzeu tots els principis de S.O.L.I.D d’una manera combinada, us serà més fàcil desenvolupar programari que es pugui gestionar fàcilment. Les altres característiques de l’ús de S.O.L.I.D són:



  • Evita les olors de codi.
  • Codi de refractor ràpidament.
  • Pot desenvolupar programari àgil o adaptatiu.

Quan utilitzeu el principi de S.O.L.I.D a la vostra codificació, comenceu a escriure el codi que sigui alhora eficaç i eficaç.



Quin significat té S.O.L.I.D?

Solid representa cinc principis de Java que són:

  • S : Principi de responsabilitat única
  • O : Principi obert-tancat
  • L : Principi de substitució de Liskov
  • Jo : Principi de segregació de la interfície
  • D : Principi d'inversió de dependència

En aquest bloc, parlarem de tots els cinc principis SOLID de Java en detall.



Principi de responsabilitat única a Java

Què diu?

Robert C. Martin ho descriu com una classe només ha de tenir una única responsabilitat.

Segons el principi de responsabilitat única, només hi hauria d’haver un motiu pel qual s’hagi de canviar una classe. Vol dir que una classe ha de tenir una tasca a fer. Aquest principi sovint es denomina subjectiu.

El principi es pot entendre bé amb un exemple. Imagineu que hi ha una classe que realitza les següents operacions.

  • Connectat a una base de dades

  • Llegiu algunes dades de taules de bases de dades

  • Finalment, escriviu-lo a un fitxer.

Us heu imaginat l’escenari? Aquí la classe té múltiples raons per canviar, i poques d'elles són la modificació de la sortida de fitxers, l'adopció d'una nova base de dades. Quan parlem de responsabilitat de principi únic, diríem, hi ha massa motius perquè la classe canviï, per tant, no s’adapta correctament al principi de responsabilitat única.

Per exemple, una classe Automobile pot iniciar-se o aturar-se, però la tasca de rentar-la pertany a la classe CarWash. En un altre exemple, una classe de llibres té propietats per emmagatzemar el seu propi nom i text. Però la tasca d'impressió del llibre ha de pertànyer a la classe d'impressores de llibres. La classe de la impressora de llibres pot imprimir-se a la consola o un altre suport, però aquestes dependències s’eliminen de la classe de llibres

Per què es requereix aquest principi?

Quan es segueix el principi de responsabilitat única, les proves són més fàcils. Amb una única responsabilitat, la classe tindrà menys casos de prova. Menys funcionalitat també significa menys dependències respecte a altres classes. Condueix a una millor organització del codi, ja que són més fàcils de cercar classes més petites i ben proposades.

Un exemple per aclarir aquest principi:

Suposem que se us demana que implementeu un servei de configuració d'usuaris on l'usuari pugui canviar la configuració, però abans s'haurà d'autenticar l'usuari. Una manera d’implementar-ho seria:

public class UserSettingService {public void changeEmail (usuari usuari) {if (checkAccess (usuari)) {// Opció de concessió per canviar}} public boolean checkAccess (usuari usuari) {// Verifiqueu si l'usuari és vàlid. }}

Tot queda bé fins que no vulgueu tornar a utilitzar el codi checkAccess en algun altre lloc O bé vulgueu fer canvis a la manera com es fa checkAccess. En els dos casos, acabareu canviant la mateixa classe i, en el primer cas, hauríeu d’utilitzar UserSettingService per comprovar-ne l’accés.
Una manera de corregir-ho és descompondre UserSettingService en UserSettingService i SecurityService. I moveu el codi checkAccess a SecurityService.

public class UserSettingService {public void changeEmail (usuari d'usuari) {if (SecurityService.checkAccess (usuari)) {// Concedeix l'opció per canviar}}} classe pública SecurityService {public boolean estàtic checkAccess (usuari d'usuari) {// comprova l'accés. }}

Principi obert tancat a Java

Robert C. Martin ho descriu com que els components del programari haurien d'estar oberts per a l'extensió, però tancats per modificar-los.

Per ser precisos, d'acord amb aquest principi, una classe s'hauria d'escriure de manera que realitzés el seu treball impecablement sense suposar que la gent en el futur simplement vindrà a canviar-la. Per tant, la classe hauria de romandre tancada per modificar-la, però hauria de tenir l’opció d’ampliar-se. Les maneres d’ampliar la classe inclouen:

  • Heretant de la classe

  • Sobreescrivint els comportaments necessaris de la classe

  • Ampliació de certs comportaments de la classe

Un excel·lent exemple de principi obert-tancat es pot entendre amb l'ajuda dels navegadors. Recordeu haver instal·lat extensions al navegador Chrome?

La funció bàsica del navegador Chrome és navegar per diferents llocs. Voleu comprovar la gramàtica quan escriviu un missatge de correu electrònic amb el navegador Chrome? Si és així, només podeu utilitzar l'extensió Grammarly, que us proporciona una comprovació gramatical del contingut.

Aquest mecanisme on afegiu coses per augmentar la funcionalitat del navegador és una extensió. Per tant, el navegador és un exemple perfecte de funcionalitat oberta a l’extensió però tancada per modificar-la. En paraules simples, podeu millorar la funcionalitat afegint / instal·lant connectors al navegador, però no podeu crear res de nou.

Per què és necessari aquest principi?

OCP és important ja que les classes ens poden arribar a través de biblioteques de tercers. Hauríem de poder ampliar aquestes classes sense preocupar-nos si aquestes classes base poden donar suport a les nostres extensions. Però l’herència pot conduir a subclasses que depenen de la implementació de la classe base. Per evitar-ho, es recomana l'ús d'interfícies. Aquesta abstracció addicional condueix a un acoblament fluix.

Diguem que hem de calcular àrees de diverses formes. Comencem creant una classe per al nostre primer rectangle de formaque té 2 atributs de longitud& amplada.

public class Rectangle {public double length public double width}

A continuació, creem una classe per calcular l'àrea d'aquest rectangleque té un mètode calculateRectangleAreaque pren el Rectanglecom a paràmetre d’entrada i calcula la seva àrea.

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

Fins ara, tot bé. Ara diguem que obtenim el nostre segon cercle de forma. Així que immediatament creem un cercle de classe nouamb un únic radi d'atribut.

com executar atom python
Cercle de classe pública {doble radi públic}

A continuació, modificem Areacalculatorclass per afegir càlculs de cercles mitjançant un nou mètode calculateCircleaArea ()

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

Tanmateix, tingueu en compte que hi ha hagut defectes en la manera com hem dissenyat la nostra solució anteriorment.

Diguem que tenim un pentàgon de forma nova. En aquest cas, tornarem a modificar la classe AreaCalculator. A mesura que el tipus de formes creix, això es torna més complicat a mesura que AreaCalculator continua canviant i els consumidors d’aquesta classe hauran de continuar actualitzant les seves biblioteques que contenen AreaCalculator. Com a resultat, la classe AreaCalculator no es basarà (finalitzarà) amb seguretat, ja que cada vegada que aparegui una nova forma es modificarà. Per tant, aquest disseny no està tancat per modificació.

AreaCalculator haurà de continuar afegint la seva lògica de càlcul en mètodes més nous. Realment no estem ampliant l’abast de les formes, sinó que simplement estem fent una solució de menjar per peces (de mica en mica) per a cada forma que s’afegeix.

Modificació del disseny anterior per complir amb el principi obert / tancat:

Vegem ara un disseny més elegant que resol els defectes del disseny anterior adherint-se al principi obert / tancat. Primer de tot farem extensible el disseny. Per a això, primer hem de definir un tipus base Shape i fer que Circle & Rectangle implementi la interfície Shape.

interfície pública Shape {public double calculateArea ()} public class Rectangle implements Shape {double length double width public double calculateArea () {return length * width}} public class Circle implements Shape {public double radio public double calculateArea () {return (22 / 7) * radi * radi}}

Hi ha una interfície base Shape. Totes les formes ara implementen la forma base de la interfície. La interfície Shape té un mètode abstracte calculateArea (). Tant el cercle com el rectangle proporcionen la seva pròpia implementació anul·lada del mètode calculateArea () mitjançant els seus propis atributs.
Hem aportat un cert grau d’extensibilitat, ja que les formes són ara un exemple d’interfícies de formes. Això ens permet utilitzar Shape en lloc de classes individuals
L'últim punt esmentat consumidor d'aquestes formes. En el nostre cas, el consumidor serà la classe AreaCalculator que ara tindria aquest aspecte.

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

Aquest calculador d’àreaara la classe elimina completament els nostres defectes de disseny esmentats anteriorment i proporciona una solució neta que s’adhereix al principi de tancament obert. Continuem amb altres principis de SOLID a Java

Principi de substitució de Liskov a Java

Robert C. Martin ho descriu com que els tipus derivats han de ser completament substituïbles pels seus tipus base.

El principi de substitució de Liskov assumeix q (x) com una propietat, demostrable sobre entitats de x que pertany al tipus T. Ara, segons aquest principi, el q (y) hauria de ser ara demostrable per als objectes y que pertanyen al tipus S, i el S és en realitat un subtipus de T. Ara estàs confós i no saps què vol dir realment el principi de substitució de Liskov? La seva definició pot ser una mica complexa, però, de fet, és bastant fàcil. L'únic és que totes les subclasses o classes derivades haurien de ser substituïbles pel seu pare o classe base.

Es pot dir que és un principi únic orientat a objectes. El principi es pot simplificar encara més mitjançant un tipus de nen d’un tipus de pare determinat sense fer cap complicació o fer volar les coses que tinguin la capacitat de defensar-lo. Aquest principi està estretament relacionat amb el principi de substitució de Liskov.

Per què és necessari aquest principi?

Això evita un mal ús de l’herència. Ens ajuda a conformar-nos amb la relació 'és-a'. També podem dir que les subclasses han de complir un contracte definit per la classe base. En aquest sentit, està relacionat ambDisseny per contracteaixò va ser descrit per primera vegada per Bertrand Meyer. Per exemple, és temptador dir que un cercle és un tipus d’el·lipse, però els cercles no tenen dos focus ni eixos majors / menors.

El LSP s'explica popularment mitjançant l'exemple de quadrat i rectangle. si assumim una relació ISA entre quadrat i rectangle. Per tant, anomenem 'El quadrat és un rectangle'. El codi següent representa la relació.

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

A continuació es mostra el codi de Square. Tingueu en compte que el quadrat amplia el rectangle.

public class Square estén Rectangle {public void setBreadth (int wideth) {super.setBreadth (amplada) super.setLength (amplada)} public void setLength (int length) {super.setLength (length) super.setBreadth (length)}}

En aquest cas, intentem establir una relació ISA entre Square i Rectangle de manera que cridar 'Square és un Rectangle' al codi següent es comenci a comportar inesperadament si es passa una instància de Square. Es produirà un error d’afirmació en el cas de comprovar “Àrea” i comprovar “Amplada”, tot i que el programa finalitzarà quan es produeixi l’error d’afirmació a causa del fracàs de la comprovació d’Àrea.

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 ('broadth', r)} private String printError (String errorIdentifer, Rectangle r) {return 'Valor inesperat de' + errorIdentifer + ' per exemple de '+ r.getClass (). getName ()} public static void main (String [] args) {LSPDemo lsp = new LSPDemo () // Es passa una instància de Rectangle lsp.calculateArea (nou Rectangle ()) // Es passa una instància de Square lsp.calculateArea (nou Square ())}}

La classe demostra el principi de substitució de Liskov (LSP) Segons el principi, les funcions que fan servir referències a les classes base han de poder utilitzar objectes de classe derivada sense saber-ho.

Així, a l'exemple que es mostra a continuació, la funció calculateArea que utilitza la referència de 'Rectangle' hauria de ser capaç d'utilitzar els objectes de classe derivada com Square i complir el requisit proposat per la definició de Rectangle. Cal tenir en compte que, segons la definició de Rectangle, el següent ha de mantenir-se sempre en funció de les dades següents:

  1. La longitud sempre ha de ser igual a la longitud passada com a entrada al mètode, setLength
  2. L'amplada sempre ha de ser igual a l'amplada passada com a entrada al mètode, setBreadth
  3. La superfície sempre ha de ser igual al producte de llargada i amplada

En el cas que intentem establir una relació ISA entre Square i Rectangle de manera que anomenem 'Square és un Rectangle', el codi anterior començaria a comportar-se inesperadament si es passa una instància de Square Es produirà un error d'asserció en cas de comprovar l'àrea i comprovar per amplitud, tot i que el programa finalitzarà a mesura que es produeixi l'error d'afirmació a causa del fracàs de la comprovació d'àrea.

La classe Square no necessita mètodes com setBreadth o setLength. La classe LSPDemo hauria de conèixer els detalls de les classes derivades de Rectangle (com Square) per codificar adequadament per evitar errors de llançament. El canvi en el codi existent trenca el principi obert-tancat.

Principi de segregació d’interfícies

Robert C. Martin ho descriu com que els clients no haurien de ser obligats a implementar mètodes innecessaris que no utilitzaran.

D'acord ambPrincipi de segregació de la interfícieun client, independentment del que mai no s’hauria de forçar a implementar una interfície que no utilitza o el client mai no hauria d’estar obligat a dependre de cap mètode que no utilitzen. Així doncs, bàsicament, els principis de segregació de la interfície com preferiu interfícies petites, però específiques per al client en lloc d’interfícies monolítiques i més grans. En resum, seria dolent que obligueu el client a dependre d’una cosa determinada, que no necessita.

Per exemple, una única interfície de registre per escriure i llegir registres és útil per a una base de dades, però no per a una consola. La lectura de registres no té sentit per a una consola. Continuem amb aquest article de Principis sòlids a Java.

Per què és necessari aquest principi?

Diguem que hi ha una interfície de restaurant que conté mètodes per acceptar comandes de clients en línia, telèfons o clients telefònics i clients de sortida. També conté mètodes per gestionar els pagaments en línia (per a clients en línia) i els pagaments presencials (tant per als clients amb accés directe com per a clients de telefonia quan la seva comanda es lliura a casa).

Ara creem una interfície Java per a restaurant i l’anomenem RestaurantInterface.java.

interfície pública RestaurantInterface {public void acceptOnlineOrder () public void takeTelephoneOrder () public void payOnline () public void walkInCustomerOrder () public void payInPerson ()}

Hi ha cinc mètodes definits a RestaurantInterface que són per acceptar comandes en línia, fer comandes telefòniques, acceptar comandes d’un client que accedeix, acceptar pagaments en línia i acceptar pagaments en persona.

Comencem implementant la RestaurantInterface per a clients en línia com OnlineClientImpl.java

classe pública OnlineClientImpl implementa RestaurantInterface {public void acceptOnlineOrder () {// lògica per fer una comanda en línia} public void takeTelephoneOrder () {// No és aplicable per a la comanda en línia llança UnsupportedOperationException ()} public void payOnline () {// lògica per pagar en línia} public void walkInCustomerOrder () {// No aplicable per a comandes en línia llança UnsupportedOperationException ()} public void payInPerson () {// No s’aplica per a comandes en línia llança UnsupportedOperationException ()}}
  • Com que el codi anterior (OnlineClientImpl.java) és per a comandes en línia, llanceu UnsupportedOperationException.

  • Els clients en línia, telefònics i accessoris utilitzen la implementació de RestaurantInterface específica per a cadascun d’ells.

  • Les classes d’implementació per al client de telefonia i el client de sortida tindran mètodes no compatibles.

  • Com que els 5 mètodes formen part de la RestaurantInterface, les classes d'implementació han d'implementar-ne tots cinc.

  • Els mètodes que cadascuna de les classes d'implementació llança UnsupportedOperationException. Com podeu veure clarament: la implementació de tots els mètodes és ineficient.

  • Qualsevol canvi en qualsevol dels mètodes de la RestaurantInterface es propagarà a totes les classes d'implementació. Aleshores, el manteniment del codi comença a ser molt pesat i els efectes de regressió dels canvis continuaran augmentant.

    passa per valor i passa per referència java
  • RestaurantInterface.java trenca el principi de responsabilitat única perquè la lògica dels pagaments i la de la col·locació de comandes s’agrupen en una única interfície.

Per superar els problemes esmentats anteriorment, apliquem el principi de segregació d’interfícies per refactoritzar el disseny anterior.

  1. Separeu les funcions de pagament i de col·locació de comandes en dues interfícies lean separades, PaymentInterface.java i OrderInterface.java.

  2. Cadascun dels clients utilitza una implementació de PaymentInterface i OrderInterface. Per exemple: OnlineClient.java utilitza OnlinePaymentImpl i OnlineOrderImpl, etc.

  3. Ara s’adjunta el principi de responsabilitat única com a interfície de pagament (PaymentInterface.java) i interfície de comanda (OrderInterface).

  4. El canvi en qualsevol de les interfícies de comanda o de pagament no afecta l’altra. Ara són independents. No hi haurà necessitat de fer cap implementació fictícia ni llançar una excepció UnsupportedOperationException, ja que cada interfície només té mètodes que sempre utilitzarà.

Després d’aplicar l’ISP

Principi d’inversió de la dependència

Robert C. Martin ho descriu ja que depèn de les abstraccions i no de les concrecions, segons el qual el mòdul d’alt nivell mai ha de confiar en cap mòdul de baix nivell. per exemple

Aneu a una botiga local per comprar alguna cosa i decidiu pagar-la mitjançant la targeta de dèbit. Per tant, quan doneu la vostra targeta al secretari per fer el pagament, el secretari no es molesta a comprovar quin tipus de targeta heu donat.

Fins i tot si heu donat una targeta Visa, no traurà cap màquina Visa per passar la targeta. El tipus de targeta de crèdit o de dèbit que tingueu per pagar ni tan sols importa, simplement la llisquen. Per tant, en aquest exemple, podeu veure que tant vosaltres com el secretari dependreu de l’abstracció de la targeta de crèdit i que no us preocupen els detalls de la targeta. Això és el que és un principi d'inversió de dependència.

Per què és necessari aquest principi?

Permet a un programador eliminar les dependències codificades de manera que l’aplicació quedi acoblada i extensible.

classe pública Estudiant {adreça privada adreça pública estudiant () {adreça = nova adreça ()}}

A l'exemple anterior, la classe Student requereix un objecte Address i és responsable d'inicialitzar i utilitzar l'objecte Address. Si en un futur es canvia la classe d’adreça, també haurem de fer canvis a la classe d’estudiants. Això fa que l’acoblament estret entre els objectes Student i Address. Podem resoldre aquest problema mitjançant el patró de disseny d’inversió de dependències. És a dir, l’objecte d’adreça s’implementarà de manera independent i es proporcionarà a Student quan s’instanci Student mitjançant la inversió de dependències basada en constructors o en setter.

Amb això, arribem al final d’aquests principis SOLID a Java.

Consulteu el per Edureka, una empresa d'aprenentatge en línia de confiança amb una xarxa de més de 250.000 estudiants satisfets repartits per tot el món. El curs de formació i certificació Java J2EE i SOA d’Edureka està dissenyat per a estudiants i professionals que vulguin ser desenvolupador de Java. El curs està dissenyat per donar-vos un avantatge en la programació de Java i formar-vos tant per conceptes bàsics com avançats de Java, juntament amb diversos marcs Java com Hibernate i Spring.

Tens alguna pregunta? Si us plau, mencioneu-ho a la secció de comentaris d’aquest bloc “Principis de SOLID a Java” i us respondrem el més aviat possible.