Sådan bruges et enkelt ansvarsprincip i Swift

Nogle gange, når vi lærer at kode, er det svært at forstå klassernes ansvarsbegreb. Det er grunden til, at vores første projekter bliver uholdbare, der indeholder mange linjer efter klasse i vores kode og, meget mere kritiske, mange ansvarsområder.

En god måde at virkelig kende ansvaret for en klasse er at tænke på skalerbarhed.

Lad os se et eksempel på, hvordan man bruger det. Men før vil jeg gerne introducere en smule teori og forklare, hvorfor dette koncept er vigtigt i software-design.

Introduktion til princippet om enkeltansvar

Ideen om, at hver klasse har et enkelt ansvar i et softwareprojekt, og at ansvaret, der er indkapslet i en unik klasse, har et navn: Enkeltansvarsprincip

Dette er et af de 5 hovedprincipper for softwaredesign SOLID, hvor de i Objektorienteret programmering forsøger at definere en guide til at have en mere forståelig, fleksibel og vedligeholdelig software.

Disse principper er:

  • Princippet om et enkelt ansvar
  • Åbent / lukket princip
  • Liskov-substitutionsprincip
  • Grænsefladesegregeringsprincip
  • Afhængighedsinversionsprincippet

Forfatteren af ​​disse koncepter, Robert C. Martin, (han skrev en af ​​de vigtigste bøger inden for softwarearkitektur, Clean Code) taler om ”En klasse skal kun have en grund til at ændre sig”, så han definerer et ansvar som en grund til at lave om.

Lad os f.eks. Overveje et modul, der udarbejder og udskriver en rapport. Forestil dig, at et sådant modul kan ændres af to grunde. For det første kan indholdet af rapporten ændres. For det andet kan rapportens format ændres. Disse to ting ændres af meget forskellige årsager; en substantiv og en kosmetik.

Princippet om enkelt ansvarlighed siger, at disse to aspekter af problemet virkelig er to separate ansvarsområder og derfor bør være i separate klasser eller moduler. Det ville være et dårligt design at parre to ting, der ændrer sig af forskellige grunde på forskellige tidspunkter.

Årsagen til, at det er vigtigt at holde en klasse fokuseret på en enkelt bekymring, er, at den gør klassen mere robust. Fortsæt med det foregående eksempel, hvis der er en ændring af rapportsamlingsprocessen, er der større fare for, at udskrivningskoden går i stykker, hvis den er en del af samme klasse. (Eksempel udvundet fra Wikipedia)

Hvorfor er det vigtigt at definere hvert klasseansvar korrekt

Hvis vi definerer vores klasser ved at vide, hvilket er deres ansvar i vores projekt, kan vi:

  • Forstå let, hvilken funktionalitet den gør i hver del af koden.
  • Rediger eksisterende logik hurtigere og detaljeret.
  • Find med mindre problemer oprindelsen af ​​bugs eller uønsket opførsel.
  • Abstrakt logik i forskellige klasser eller moduler.
  • Opdel uden større problemer implementeringer, så de kan udskiftes helt senere.
  • Definer enhedstest efter klasse eller modul på en mere effektiv måde, så vi kan teste et lille stykke af koden, og ikke mere, at vi virkelig ønsker at teste.

Tænker i muligheden for at definere ansvar

Som jeg sagde før, kan du i klasserne tænke på at definere ansvar. Dette er så simpelt som at tænke, hvis der i vores projekt kan ske, at kravene vil blive ændret, og se i vores arkitektur, hvordan disse ændringer ville finde sted.

Hvis vi ser, at for en lille visningsændring, vi er nødt til at ændre eller røre ved forretningslogik, definerer vi for eksempel ikke korrekt ansvaret i vores projekt.

Lad os se et konkret eksempel i Swift.

Hurtigt eksempel

Lad os sige, at jeg har en app, som den viser en liste over varer fra en butik. På nuværende tidspunkt har jeg kun en ItemsViewController, der er ansvarlig for al logikken i denne flow, dataene og præsentationen af ​​det. Det udskriver også en log, når brugeren vælger et element.

ItemsViewController: En simpel liste over varer

Du kan se koden på https://github.com/fedejordan/SRPExample

For at gøre det bruger ItemsViewController en UITableView til at vise elementerne på en liste. Vi bruger også en UITableViewCell-underklasse kaldet ItemTableViewCell til at vise disse elementer.

Men lad os sige, at vi f.eks. Vil ændre visningen med en UICollectionView. Hvad ville der være problemet i denne situation?

Visningskoden er meget koblet til elementets datalogik. Hvis vi ændrer visningen, er det meget sandsynligt, at vi også ændrer klassen, der er ansvarlig for varerne.

Det virkelige problem er i disse linjer:

let item = items [indexPath.row]

Hvorfor er problemet her?

Fordi vi bruger UITableView-indekset for at få det specifikke element i matrixen. Vi skal abstraherer på en måde, at visningen bruger anUITableView.

For at undgå det, reflekterer vi ItemsViewController, og vi flytter modellogikken til en anden klasse, der hedder ItemsInteractor.

Interaktorkonceptet har sin oprindelse i VIPER-arkitektur. Som det siges i definitionen, indeholder en Interactor forretningslogikken til at manipulere modelobjekter (enheder) for at udføre en bestemt opgave. I dette tilfælde er vores ItemsInteractor ansvarlig for at indhente oplysninger om enhver vare.

Du kan få denne kode på https://github.com/fedejordan/SRPExample, i filialen interactor_refactor.

Som vi kan se, kender ItemsViewController ikke noget om datamodellen. Det spørger simpelthen interaktoren, hvad den har brug for for at få et synspunkt.

Med denne tilgang kan vi for eksempel ændre datatypen Element til enhver anden ting, og vi vil kun ændre ItemsInteractor. Det vil hente de samme oplysninger (vi ville ikke ændre de offentlige metoder) og til sidst kunne ItemsViewController fortsætte med at fungere som altid.

Så med den refaktor, hvis vi vil ændre netop layoutet af vores app, ændrer vi blot ItemsViewController til at bruge anUICollectionView:

ItemsViewController-skærmbillede ved hjælp af en UICollectionView

Du kan se det endelige resultat i https://github.com/fedejordan/SRPExample, collection_view_refactor branch.

Ændrede vi noget i ItemsInteractor? Bare intet. Vi ændrede simpelthen præsentationen af ​​funktionen.

Dette tillader os også at teste alle på en mere modulariseret måde. Vi kan først begynde at teste visningen og derefter forretningslogikken. Selvfølgelig, for at vi udfører tests korrekt, er vi nødt til at gøre noget mere injicerbart, for vi kan initialisere modulerne med hånede klasser, og vi er afhængige af grænseflader eller protokoller i Swift. Grundlæggende betyder det for eksempel ikke at oprette ItemsInteractor i ItemsViewController, fordi det er en afhængighed, og det skal abstraheres fra dens implementering. Men alt dette overstiger artikelformålet.

Konklusion

Ser ud til, at vi ikke gjorde så meget arbejde. Vi har simpelthen fjernet visningslogikken fra datamodellen. Var det virkelig det værd? I dette eksempel var det måske ikke alt for nyttigt, det kunne faktisk være, at vi simpelthen har brug for en liste og intet andet.

Men når vi har mere komplekse brugergrænseflader, er vi nødt til at tilpasse den samme visning til forskellige datakilder eller dele datamodellen med forskellige strømme. Ved at anvende dette koncept kan vi genbruge koden og skalere projektet på en mere korrekt måde. Og for at gøre det er det vigtigt at forstå, hvilket ansvar der skal have hver klasse, som vi opretholder.

Jeg vil gerne tilføje, at efter min mening er dette koncept en nødvendig betingelse for at have en god kode. Faktisk vil vi i enhver kode, der er dårligt skrevet, altid finde nogen klasse med mere end et ansvar. Det er på grund af det, at vi så vigtigheden af ​​princippet om et enkelt ansvar.

Vores Swift-eksempel gjorde det muligt for os at forstå, hvorfor det er vigtigt at definere ansvaret korrekt. Hvordan en korrekt modulariseret arkitektur kan være nøglen i skabbarheden af ​​vores projekt, simpelthen fordi vi ønsker at lave en layoutændring.

Som et resumé vil jeg gerne konkludere, at vi altid ser i vores projektbarhed og vil være i stand til at se, om vi definerer vores klasser eller modulers ansvar korrekt.

Mange tak for at have læst indlægget!

Jeg håber du nød det. Du kan sende enhver meddelelse eller forslag om emnerne i kommentarfeltet eller blot sende mig en mail til [email protected]