SOLID-printsiibid: objektorienteeritud programmeerimise viis kuldreeglit
SOLID-printsiibid koosnevad viiest tavast ja juhisest, mis aitavad objektipõhisel programmeerimisel kirjutada puhast, hooldatavat ja paindlikku koodi. Printsiipide rakendamine ja järgimine võimaldab hõlpsasti mõistetavat tarkvaradisaini ka pikkade arendusperioodide vältel. Nii ei kirjuta te mitte üksnes paremat koodi, vaid saate ka olemasolevat koodi paremini hooldada.
Mis on SOLID-printsiibid?
Hea lähtekoodi aluseks on reeglid, programmeerimisparadigmad ja sobiv programmeerimisstiil, mis tagavad tõhusa ja puhta koodi. Just selle eest hoolitsevad viis SOLID-printsiipi, mille kujundasid Robert C. Martin, Bertrand Meyer ja Barbara Liskov. Kui järgite objektorienteeritud programmeerimises (OOP) selliste keeltega nagu Python või Java teatud printsiipe, kirjutate mitte ainult paremat koodi, vaid tagate pikas perspektiivis ka tõhusama koodihoolduse, jätkusuutliku ja paindliku tarkvaradisaini ning suurema turvalisuse.
Nimetus SOLID tähistab ühelt poolt kindlat arendusalust kõigile, kes soovivad õppida programmeerima. Teiselt poolt koosneb lühend viie printsiibi esitähtedest (Michael Feathersi järgi):
- Single Responsibility Principle (ainuvastutuse printsiip): klassil tohib olla ainult üks vastutus (mitte ülesanne) ja ainult üks põhjus muutmiseks!
- Open Closed Principle (avatuse ja suletuse printsiip): klassid peavad olema avatud laiendustele ja suletud muudatustele!
- Liskov Substitution Principle (Liskovi asendusprintsiip): alamklassid peavad suutma pärida ja implementeerida kõik ülemklassi meetodid ja omadused!
- Interface Segregation Principle (liideste eraldamise printsiip): liidesed ei tohi sisaldada rohkem meetodeid, kui implementeerivad klassid vajavad!
- Dependency Inversion Principle (sõltuvuste pööramise printsiip): klassid ei tohi sõltuda teistest klassidest, vaid liidestest või abstraktsetest klassidest!
Millised on SOLID-printsiipide eelised?
Seal, kus reegleid ei ole, tekib kaos – programmeerimises ilmneb see eriti selgelt. Juba väikesed vead, ebatäpsused ja lüngad võivad „ravimata“ jäädes muuta hea lähtekoodi pikemas perspektiivis täiesti kasutuskõlbmatuks. Selleks ei ole vaja palju: piisab juba keerukatest klassidest, mis raskendavad implementeerimist, või alamklassidest, millel puuduvad mõned nende ülemklasside omadused. SOLID-printsiibid hoolitsevad selle eest, et koodi oleks vaja refaktoreerimise teel võimalikult vähe parandada.
SOLID-printsiipide rakendamise eelised on järgmised:
- Selge, puhas ja ilus: tarkvara ja kood on hõlpsamini mõistetav, paremini jälgitav, tõhusam ja lihtsalt ilusam.
- Lihtne hooldada: ülevaatliku ja selge struktuuri tõttu on nii uut kui ka aja jooksul kasvanud koodi lihtsam hooldada ka siis, kui sellega tegeleb mitu inimest.
- Kohandatav, laiendatav, taaskasutatav: parem loetavus, väiksem keerukus ja vastutuste hulk ning klasside vähendatud sõltuvused võimaldavad koodi paremini muuta, kohandada, liideste kaudu laiendada ja paindlikult taaskasutada.
- Vähem veaohtlik: lihtsa struktuuriga puhta koodi puhul ei mõjuta ühe osa muutmine soovimatult teisi valdkondi ega funktsioone.
- Turvalisem ja usaldusväärsem: väiksema arvu nõrkuste, ühildumatuste või vigade tõttu – või nende täieliku puudumise tõttu – paraneb süsteemide funktsionaalsus ja töökindlus ning seega ka turvalisus.
SOLID-printsiibid ülevaatlikult
SOLID-printsiibid kuuluvad hea programmeerimise kuldreeglite hulka ja peaksid seetõttu olema tuttavad kõigile, kes objektipõhise programmeerimisega tegelevad. Tutvustame viit printsiipi üksikasjalikult.
SRP: Single Responsibility Principle (ainuvastutuse printsiip)
Robert C. Martini algne määratlus teoses „Agile Software Development: Principles, Patterns and Practices“ ütleb:
„Klassi muutmiseks ei tohiks kunagi olla rohkem kui üks põhjus.“
SRP ütleb, et igal OOP-i klassil tohib olla ainult üks vastutus. Seega tohib klassi muutmiseks olla ainult üks põhjus. Vastupidiselt levinud väärarusaamadele ei tähenda see, et klassil või igal moodulil tohiks olla täpselt üks ülesanne. Küll aga peaks see kandma vastutust ainult kindlate ülesannete eest, mis võimaluse korral ei kattu teiste valdkondadega.
Vastutuste kattumine, näiteks funktsioonide pakkumine eri ärivaldkondadele, võib ühe valdkonna muutmisel mõjutada kogu klassi talitlust. Kui vastutusi on rohkem kui üks ja sõltuvusi liiga palju, võib muudatus ühes valdkonnas tuua kaasa mitu järgnevat muudatust või koodivigu.
Seega on SRP eesmärk arendada sidusaid mooduleid, mis vastutavad selgelt mõistetava ülesande või selgelt määratletud objektide eest. Selgelt struktureeritud kuju, vähendatud sõltuvuste ja lahtisidestatud implementatsioonide tõttu saab hilisemaid muudatusi ja kohandusi teha lihtsamalt, kiiremini ja tüsistusteta.
Klassid, tuntud ka objektitüüpidena, on objektorienteeritud programmeerimise (OOP) keskne element. Neid võib mõista ehitusplaanina, mis sisaldab atribuute reaalsete, sarnaste ja konstrueeritavate objektide kujutamiseks tarkvaraobjektidena. Seetõttu võrreldakse klasse, mida tuntakse ka moodulitena, sageli failitüüpidega.
OCP: Open Closed Principle (avatuse ja suletuse printsiip)
Bertrand Meyeri ja Robert C. Martini järgi teoses „Object Oriented Software Construction“ ütleb OCP:
„Tarkvaraüksused (klassid, moodulid, funktsioonid jne) peaksid olema avatud laiendustele, kuid suletud muudatustele.“
OCP hoolitseb selle eest, et teil ei oleks muudatuste rakendamiseks vaja tarkvara tuuma ümber kirjutada. Kui on vaja põhjalikke koodimuudatusi, tekib oht peente vigade ja code smell’i tekkeks. Hästi struktureeritud koodi peaks saama varustada liidestega, mille kaudu seda täiendavate funktsioonidega laiendada. Märksõna on siinkohal klasside pärimine.
Uusi funktsioone ja laiendusi koos ülevaatlike uute funktsioonide ja meetoditega, mis tuleb implementeerida, saab liidese kaudu hõlpsasti ülemklassi külge haakida alamklasside kujul. Nii ei pea te juba kirjutatud stabiilses koodis „sorima“. See lihtsustab programmide hooldust ja muudab stabiilsete koodielementide taaskasutamise liideste kaudu märksa tõhusamaks.
LSP: Liskov Substitution Principle (Liskovi asendusprintsiip)
Barbara H. Liskovi ja Jeannette M. Wingi järgi teoses „Behavioral Subtyping Using Invariants and Constraints“ ütleb LSP:
„Olgu q (x) tüüpi T objekti x omadus, siis peaks q (y) kehtima kõigi tüüpi S objektide kohta, kusjuures S on tüübi T alamtüüp.“
See, mis esmapilgul krüptiliselt kõlab, on tegelikult lihtsasti mõistetav: seotud või laiendatud alamklassid peavad töötama nagu nende ülem- või baasklassid. See tähendab, et iga alamklass peab pärimise teel saama oma ülemklassi omadused ja need omadused ei tohi alamklassis muutuda. Need peavad olema põhimõtteliselt asendatavad – sellest ka nimetus asendusprintsiip. Ülemklasse seevastu tohib muudatustega modifitseerida.
Näitlikustamiseks sobib Robert C. Martini klassikaline ristküliku ja ruudu näide. Geomeetriatunnis kehtib tõdemus: iga ruut on ristkülik, kuid mitte iga ristkülik ei ole ruut. Ruudul on ristkülikute ülemklassiga ühine omadus „täisnurksed küljed“, kuid lisaks on tal omadus „võrdse pikkusega küljed“.
Programmeerimises on olukord teine: siin viib eeldus, et sarnased või näiliselt identsed klassid on omavahel seotud või üksteisest sõltuvad, vigadeni, arusaamatusteni ja ebaselge koodini. Just seetõttu ei ole programmeerimises klass „ristkülik“ ruut ega klass „ruut“ ristkülik. Mõlemad „lahtisidestatakse“ ja implementeeritakse eraldi. Ilma klasside vahelise sisseehitatud seoseta ei saa arusaamatus põhjustada klassideüleseid vigu. See suurendab turvalisust ja stabiilsust implementatsioonide vahetamisel alam- või ülemklassides, ilma et sellel oleks kõrvalmõjusid.
ISP: Interface Segregation Principle (liideste eraldamise printsiip)
ISP on Robert C. Martini järgi teoses „The Interface Segregation Principle“ määratletud nii:
„Kliente ei tohiks sundida sõltuma liidestest, mida nad ei kasuta.“
ISP ütleb, et kasutajaid ei tohiks sundida kasutama liideseid, mida nad ei vaja. Teisisõnu: selleks et teha klientidele kättesaadavaks kindlate klasside funktsioonid, kohandatakse uued, väiksemad liidesed konkreetsetele nõuetele. Nii saab vältida liideste liiga suureks paisumist ja tugevate sõltuvuste teket klasside vahel. Eelis: lahtisidestatud klasside ja mitme väikese, konkreetsetele nõuetele kohandatud liidesega tarkvara on lihtsam hooldada.
DIP: Dependency Inversion Principle (sõltuvuste pööramise printsiip)
Viies ja viimane SOLID-printsiip kõlab Robert C. Martini järgi teoses „The Dependency Inversion Principle“ nii:
„A. Kõrgema taseme moodulid ei tohiks sõltuda madalama taseme moodulitest. Mõlemad peaksid sõltuma abstraktsioonidest. B. Abstraktsioonid ei tohiks sõltuda detailidest. Detailid peaksid sõltuma abstraktsioonidest.“
DIP hoolitseb selle eest, et konkreetsed funktsioonid ja sõltuvused lähtekoodi tasemetel toetuksid abstraktsetele liidestele, mitte üksteisele. Selgituseks: tarkvaraarhitektuure võib jämedalt jagada kõrgemateks kasutajatasemeteks ning madalamateks või sügavamateks abstraktseteks tasemeteks. Puhtloogiliselt võiks eeldada, et abstraktne alus määrab kõrgemate tasemete käitumise. DIP näeb selles aga veaohtu, kuna nii tekivad kõrgemate tasemete sõltuvused sügavamatest tasemetest.
Selle asemel et siduda kõrgemad tasemed madalamatega, peaksid nii kõrgete kui ka madalate tasemete klassid sõltuma abstraktsetest vahepealsetest liidestest. Liidesed pärivad madalamatelt tasemetelt funktsionaalsused, mida kõrgematel tasemetel vajatakse, ja teevad need kättesaadavaks. Nii saab vältida sõltuvustest koosnevat „alt-üles-hierarhiat“, mis võib aja jooksul koodis vigu põhjustada. See lihtsustab moodulite taaskasutamist ja võimaldab muuta madalamaid klasse ilma kõrgemaid tasemeid mõjutamata.
Mis juhtub siis, kui SOLID-printsiipe ei järgita?
Iga tarkvaraarenduse eesmärk peaks olema ilus, hõlpsasti loetav kood, mis teeb hoolduse võimalikult lihtsaks. Kui aga sellised juhised nagu SOLID-printsiibid ununevad, vananeb kood nõrkuste, liiasuste, kuhjunud vigade ja liiga paljude sõltuvuste tõttu väga halvasti. Halvimal juhul muutub kood aja jooksul kasutuskõlbmatuks. Eriti suur probleem on see agiilses tarkvaraarenduses, sest seal töötab keeruka koodiga tavaliselt palju inimesi.
Ebapuhta koodi või halva koodihoolduse tagajärgede hulka kuuluvad:
- Code smell: aja jooksul kuhjunud nõrkused ebapuhtas koodis, tuntud kui code smell ehk „haisev kood“, põhjustavad talitlusvigu ja ühildumatuid programme.
- Code rot: kui koodi ei hooldata ega parandata refaktoreerimise või kuluka koodiülevaatuse abil, võib kood ülekantud tähenduses „mädaneda“ ja kaotada täielikult oma talitluse. Loetamatu, segase koodi kohta kasutatakse ka nimetust spagetikood.
- Turvaaugud: tagajärjeks ei ole mitte ainult tõrked, keeruline hooldus või ühildumatused, vaid ka turvaaugud, mis annavad pahavarale võimaluse ärakasutusteks ja nullpäeva-rünneteks.
Kes töötas SOLID-printsiibid välja?
SOLID-printsiipide päritolu ulatub mitme printsiibini, mille tutvustas esmakordselt agiilse programmeerimise üks algatajaid Robert C. Martin („Uncle Bob“) oma 2000. aastal ilmunud essees „Design Principles and Design Patterns“. Viiest SOLID-printsiibist koosneva rühma kujundasid Robert C. Martin, Bertrand Meyer ja Barbara Liskov. Meeldejäävat, printsiipide viiest esitähest koosnevat akronüümi levitas omakorda Michael Feathers, kes järjestas viis olulist printsiipi ümber.
Millised sarnased programmeerimisprintsiibid veel on?
Tarkvaraarenduses tähistavad printsiibid üldisi või väga konkreetseid suuniseid ja tegevussoovitusi, nagu ka SOLID-printsiibid, mis pakuvad printsiipide komplekti objektorienteeritud programmeerimise paradigma jaoks. Teised puhta koodi programmeerimisprintsiibid on näiteks:
- DRY-printsiip (Don’t repeat yourself) – funktsioonid ühe ainsa, üheselt mõistetava esitusega
- KISS-printsiip (Keep it simple, stupid) – võimalikult lihtsalt üles ehitatud kood