En solid guide til SOLID-principper

1. Introduktion

I denne vejledning diskuterer vi SOLID-principperne for objektorienteret design.

Først starter vi med udforske årsagerne til, at de opstod, og hvorfor vi skulle overveje dem når du designer software. Derefter skitserer vi hvert princip sammen med nogle eksempler på kode for at understrege pointen.

2. Årsagen til SOLID-principper

SOLID-principperne blev først konceptualiseret af Robert C. Martin i sit papir fra 2000, Designprincipper og designmønstre. Disse koncepter blev senere bygget på af Michael Feathers, der introducerede os til SOLID-akronymet. Og i de sidste 20 år har disse 5 principper revolutioneret en verden af ​​objektorienteret programmering og ændret den måde, vi skriver software på.

Så hvad er SOLID, og ​​hvordan hjælper det os med at skrive bedre kode? Kort sagt, Martin's and Feathers ' designprincipper tilskynder os til at skabe mere vedligeholdelig, forståelig og fleksibel software. Følgelig, som vores applikationer vokser i størrelse, kan vi reducere deres kompleksitet og spar os en masse hovedpine længere nede ad vejen!

Følgende 5 koncepter udgør vores SOLID-principper:

  1. Single Ansvar
  2. Open / Lukket
  3. Liskov udskiftning
  4. jegnterface segregering
  5. Dependency Inversion

Mens nogle af disse ord måske lyder skræmmende, kan de let forstås med nogle enkle kodeeksempler. I de følgende afsnit vil vi gå dybt ned i, hvad hver af disse principper betyder sammen med et hurtigt Java-eksempel for at illustrere hver enkelt.

3. Enkelt ansvar

Lad os starte tingene med det enkelte ansvarsprincip. Som vi kunne forvente, siger dette princip det en klasse skal kun have et ansvar. Desuden skal det kun have en grund til at ændre sig.

Hvordan hjælper dette princip os med at opbygge bedre software? Lad os se et par af fordelene:

  1. Testning - En klasse med ét ansvar vil have langt færre testsager
  2. Nedre kobling - Mindre funktionalitet i en enkelt klasse vil have færre afhængigheder
  3. Organisation - Mindre, velorganiserede klasser er lettere at søge end monolitiske

Tag for eksempel en klasse til at repræsentere en simpel bog:

public class Book {private String name; privat strengforfatter; privat strengtekst; // konstruktør, getters og setters}

I denne kode gemmer vi navnet, forfatteren og teksten, der er knyttet til en forekomst af en Bestil.

Lad os nu tilføje et par metoder til at forespørge teksten:

public class Book {private String name; privat strengforfatter; privat strengtekst; // constructor, getters and setters // metoder, der direkte relaterer til bogegenskaberne public String erstatte WordInText (String word) {return text.replaceAll (word, text); } offentlig boolsk isWordInText (strengord) {return text.contains (word); }}

Nu, vores Bestil klasse fungerer godt, og vi kan gemme så mange bøger, som vi vil i vores ansøgning. Men hvad nytter det at gemme oplysningerne, hvis vi ikke kan sende teksten til vores konsol og læse den?

Lad os kaste forsigtighed mod vinden og tilføje en udskrivningsmetode:

public class Book {// ... ugyldig printTextToConsole () {// vores kode til formatering og udskrivning af teksten}}

Denne kode er imidlertid i strid med princippet om et enkelt ansvar, som vi skitserede tidligere. For at løse vores rod skal vi implementere en separat klasse, der kun vedrører udskrivning af vores tekster:

public class BookPrinter {// metoder til udskrivning af tekst ugyldig printTextToConsole (strengtekst) {// vores kode til formatering og udskrivning af tekst} ugyldig printTextToAnotherMedium (strengtekst) {// kode til skrivning til et andet sted ..}}

Fantastisk. Ikke kun har vi udviklet en klasse, der aflaster Bestil af dets trykopgaver, men vi kan også udnytte vores BookPrinter klasse for at sende vores tekst til andre medier.

Uanset om det er e-mail, logning eller noget andet, har vi en separat klasse dedikeret til denne ene bekymring.

4. Åben for udvidelse, lukket for modifikation

Nu, tid til 'O' - mere formelt kendt som åbent-lukket princip. Kort fortalt, klasser skal være åbne for udvidelse, men lukkede for ændringer.Ved at gøre det, viforhindre os i at ændre eksisterende kode og forårsage potentielle nye fejl i en ellers glad applikation.

Selvfølgelig er det en undtagelse fra reglen er, når der rettes fejl i eksisterende kode.

Lad os udforske konceptet yderligere med et hurtigt kodeeksempel. Som en del af et nyt projekt, forestil dig, at vi har implementeret en Guitar klasse.

Det er fuldt udbygget og har endda en lydstyrkeknap:

offentlig klasse guitar {private streng mærke; privat streng model; privat int volumen; // Konstruktører, getters & settere}

Vi starter applikationen, og alle elsker det. Men efter et par måneder beslutter vi, at Guitar er lidt kedeligt og kunne gøre med et fantastisk flammemønster for at få det til at se lidt mere 'rock and roll' ud.

På dette tidspunkt kan det være fristende at bare åbne Guitar klasse og tilføj et flammemønster - men hvem ved, hvilke fejl der kan kaste op i vores ansøgning.

Lad os i stedet holde fast ved det åben-lukkede princip og blot udvide vores Guitar klasse:

offentlig klasse SuperCoolGuitarWithFlames udvider Guitar {private String flameColor; // konstruktør, getters + setters}

Ved at udvide Guitar klasse kan vi være sikre på, at vores eksisterende applikation ikke påvirkes.

5. Udskiftning af Liskov

Næste op på vores liste er Liskov-erstatning, som uden tvivl er den mest komplekse af de 5 principper. Kort fortalt, hvis klasse EN er en undertype af klasse B, så skal vi kunne erstatte B med EN uden at forstyrre vores programs opførsel.

Lad os bare springe direkte til koden for at hjælpe med at pakke hovedet rundt om dette koncept:

offentlig grænseflade Car {void turnOnEngine (); tomrum accelerere (); }

Ovenfor definerer vi en simpel Bil interface til et par metoder, som alle biler skal være i stand til at udføre - at tænde for motoren og accelerere fremad.

Lad os implementere vores grænseflade og give nogle kode til metoderne:

offentlig klasse MotorCar implementerer bil {privat motor motor; // Konstruktører, getters + settere offentlige ugyldige turnOnEngine () {// tænd motoren! engine.on (); } offentlig tomrum accelerere () {// gå videre! engine.powerOn (1000); }}

Som vores kode beskriver, har vi en motor, som vi kan tænde, og vi kan øge effekten. Men vent, det er 2019, og Elon Musk har været en travl mand.

Vi lever nu i æraen med elbiler:

offentlig klasse ElectricCar implementerer bil {public void turnOnEngine () {smid ny AssertionError ("Jeg har ikke en motor!"); } offentlig tomrum accelerere () {// denne acceleration er skør! }}

Ved at smide en bil uden motor i blandingen ændrer vi iboende opførelsen af ​​vores program. Dette er en åbenlys krænkelse af Liskov-udskiftning og er lidt sværere at rette end vores tidligere 2 principper.

En mulig løsning ville være at omarbejde vores model til grænseflader, der tager højde for vores motorløse tilstand Bil.

6. Adskillelse af grænseflade

'Jeg' i SOLID står for grænsefladeseparation, og det betyder simpelthen det større grænseflader skal opdeles i mindre. Ved at gøre dette kan vi sikre, at implementering af klasser kun behøver at være bekymret for de metoder, der er af interesse for dem.

Til dette eksempel vil vi prøve vores hænder som zooeepers. Og mere specifikt arbejder vi i bjørnekabinettet.

Lad os starte med en grænseflade, der skitserer vores roller som bjørnevogter:

offentlig grænseflade BearKeeper {void washTheBear (); ugyldig feedTheBear (); ugyldigt petTheBear (); }

Som ivrige zooeepers er vi mere end glade for at vaske og fodre vores elskede bjørne. Vi er dog alt for opmærksomme på farerne ved at klappe dem. Desværre er vores grænseflade ret stor, og vi har intet andet valg end at implementere koden for at klappe bjørnen.

Lad os ordne dette ved at opdele vores store interface i 3 separate:

offentlig grænseflade BearCleaner {void washTheBear (); } offentlig grænseflade BearFeeder {void feedTheBear (); } offentlig grænseflade BearPetter {ugyldigt petTheBear (); }

Takket være grænsefladeseparation er vi nu fri til kun at implementere de metoder, der betyder noget for os:

offentlig klasse BearCarer implementerer BearCleaner, BearFeeder {public void washTheBear () {// Jeg tror, ​​vi gik glip af et sted ...} public void feedTheBear () {// Tuna Tirsdage ...}}

Og endelig kan vi overlade de farlige ting til de skøre mennesker:

offentlig klasse CrazyPerson implementerer BearPetter {public void petTheBear () {// Held og lykke med det! }}

Gå videre kunne vi endda dele vores BookPrinter klasse fra vores eksempel tidligere for at bruge grænsefladeseparation på samme måde. Ved at implementere en Printer interface med en enkelt Print metode, kunne vi instantiere separat ConsoleBookPrinter og AndetMediaBookPrinter klasser.

7. Afhængighedsinversion

Princippet om afhængighedsinversion refererer til afkobling af softwaremoduler. På denne måde afhænger begge af abstraktioner i stedet for moduler på højt niveau afhængigt af moduler på lavt niveau.

For at demonstrere dette, lad os gå i old school og bringe en Windows 98-computer til live med kode:

offentlig klasse Windows98Machine {}

Men hvad nytter en computer uden skærm og tastatur? Lad os tilføje en af ​​hver til vores konstruktør, så hver Windows98Computer vi instantiate kommer færdigpakket med en Overvåge og en Standardtastatur:

offentlig klasse Windows98Machine {privat final StandardKeyboard keyboard; privat endelig Monitor monitor; offentlig Windows98Machine () {monitor = ny skærm (); tastatur = nyt StandardKeyboard (); }}

Denne kode fungerer, og vi kan bruge Standardtastatur og Overvåge frit inden for vores Windows98Computer klasse. Problem løst? Ikke helt. Ved at erklære Standardtastatur og Overvåge med ny nøgleord, vi har tæt koblet disse 3 klasser sammen.

Ikke kun gør dette vores Windows98Computer svært at teste, men vi har også mistet evnen til at slukke for vores Standardtastatur klasse med en anden, hvis behovet skulle opstå. Og vi sidder fast med vores Overvåge klasse også.

Lad os afkoble vores maskine fra Standardtastatur ved at tilføje en mere generel Tastatur interface og bruge dette i vores klasse:

offentligt interface-tastatur {}
offentlig klasse Windows98Machine {privat final Keyboard keyboard; privat endelig Monitor monitor; offentlig Windows98Machine (tastaturtastatur, skærmmonitor) {this.keyboard = tastatur; this.monitor = monitor; }}

Her bruger vi afhængighedsinjektionsmønsteret her for at gøre det lettere at tilføje Tastatur afhængighed af Windows98Maskine klasse.

Lad os også ændre vores Standardtastatur klasse til at gennemføre Tastatur interface, så det er egnet til injektion i Windows98Maskine klasse:

offentlig klasse StandardKeyboard implementerer tastatur {}

Nu er vores klasser afkoblet og kommunikerer gennem Tastatur abstraktion. Hvis vi vil, kan vi nemt skifte typen af ​​tastatur i vores maskine med en anden implementering af grænsefladen. Vi kan følge det samme princip for Overvåge klasse.

Fremragende! Vi har afkoblet afhængighederne og er fri til at teste vores Windows98Maskine med den testramme, vi vælger.

8. Konklusion

I denne vejledning har vi taget en dykk ned i SOLID-principperne for objektorienteret design.

Vi startede med en hurtig smule SOLID-historie og grundene til, at disse principper eksisterer.

Brev for brev har vi opdelt betydningen af ​​hvert princip med et hurtigt kodeeksempel, der overtræder det. Vi så derefter, hvordan vi fikser vores kode og få det til at overholde SOLID-principperne.

Som altid er koden tilgængelig på GitHub.


$config[zx-auto] not found$config[zx-overlay] not found