Open closed principle

Ovaj članak je „slobodni“ prevod dijela 9. poglavlja iz knjige „Agile Principles, Patterns, and Practices in C#“ – „Open closed principles“ (autori Robert Martin i Micah Martin). Članak je fokusiran na upotrebu C++ jezika i objektno orjentisanog dizajna (OOD) kao i prema softverskom inžinjeringu. Nadam se da je članak pragmatičan i da će pomoći softverskim inžinjerima u njihovom radu. U ovom članku će se upotrebljavati Boochova notacija za označavanje u objektno orjentisanom dizajnu.Postoji mnogo usmjerenja i stavova u vezi sa OOD koji se uzimaju zdravo za gotovo. Npr. „sve member varijable treba da budu privatne“ ili “treba izbjegavati globalne varijable”, ili “upotreba dinamičkih indikatora je opasna”. Šta je uzrok ovim stavovima? Šta je to što ih je učinilo istinama? Da li su oni baš uvijek i neprikosnoveno istiniti? Ovaj članak postavlja princip dizajna na način da pokuša da potkrijepi i na dublji način obrazloži ove stavove – ovaj članak predstavlja osnovu tzv „open-closed principle“.

Ivar Jacobson kaže: “Svi sistemi se mjenjaju tokom svog životnog vijeka. Oni se moraju razvijati na način da ostanu stabilni duže od svojih prethodnika.” Kako možemo uraditi dizajn sistema tako da je on stabilan u odnosu na promjene i tako da opstane duže u odnosu na svoju prethodnu inkarnaciju? Bertrand Meyer nam je još 1988 godine dao uputstvo, tada je utemeljen sada omiljeni open-closed princip. Da ga parafraziramo:

PROGRAMSKI ENTITETI (KLASE, MODULI, FUNKCIJE ITD) BI TREBALI BITI OTVORENI ZA PROŠIRENJE ALI ZATVORENI ZA MODIFIKACIJE.

Kada jedna izmjena u programu rezultira u kaskadnim izmjenama zavisnih modula, tada takav program dobija nepoželjne atribute koje obično povezujemo sa pojmom „loš“ dizajn. Program postaje krut, lomljiv, nepredvidiv u ponašanju i bez mogućnosti višestrukog korištenja. „Open-closed“ princip napada ovakve programe „direktno“. Ovaj princip kaže da bi se dizajn programa trebao realizovati tako da se on nikada ne mijenja. Kada se zahtjevi korisnika promjene, treba proširiti ponašanje programa dodajući novi kod a ne mjenjajući postojeći kod koji već radi.

Objašnjenje

Moduli koji poštuju open-closed princip imaju 2 osnovna svojstva

1. Oni su “Otvoreni za proširenja”. Ovo znači da se ponašanje modula može proširiti. To možemo napraviti dodavanjem novog dijela programskog koda ili dodavanjem novog podmodula – aplikacije.

2. Oni su“Zatvoreni za izmjene”. Izvorni kod takvog modula je „nedodirljiv“. Nikome nije dozvoljeno da uzima izvorni kod da bi ga promjenio.

Izgleda kao da su ova dva svojstva suprotna jedno drugome. Normalan način da proširite ponašanje modula jeste da nešta promjenite u samom modulu. Moduli koji se ne mogu promjeniti se uobičajeno zamišljaju kao moduli sa fiksnim ponašanjem. Kako da razrješimo ova dva suprotna stava?

Apstrakcija je ključ

U C++, koristeći principe OOD, moguće je da se kreiraju apstrakcije koje su fiksne ali koje ipak predstavljaju skup neograničene grupe mogućih ponašanja. Apstrakcije su apstraktne osnovne klase, a neograničeni skup mogućih ponašanja je reprezentovan skupom svih mogućih izvedenih klasa. Moguće je napraviti module koji upravljaju apstrakcijama. Ovi moduli mogu biti „zatvoreni“ za izmjene pošto zavise od apstraktnih klasa koje su fiksne. Ipak, ponašanje ovih modula se može proširiti kreiranjem novih izvedenih klasa iz apstraktnih osnovnih klasa.

Naredna slika prikazuje jednostavni dizajn koji ne zadovoljava „open-closed principle“. Obadva modula, klijent i server su konkretni. Ne postoji garancija da su member funkcije serverske klase virtuelne. Klijentske klase upotrebljavaju serverske klase. Ukoliko želimo za klijentske objekte da upotrebimo drugačije serverske objekte tada se klijentske klase moraju promjeniti bar toliko da se pozove novo ime serverske klase.

Slijedeća slika prikazuje dizajn koji zadovoljava „open-closed“ princip. U ovom slučaju, klasa AbstractServer je apstraktna klasa sa čisto virtuelnim member funkcijama. Klijentske klase upotrebljavaju ove apstrakcije. Posredno, objekti klijentske klase će u stvari upotrebljavati objekte izvedene serverske klase. Ukoliko želimo da klijentski objekti upotrebljavaju drugačije serverske klase, tada se treba napraviti nova izvedena klasa u AbstractServer klasi.

Primjer apstrakcija

Razmotrimo sljedeći primjer. Imamo aplikaciju koja mora biti sposobna da nacrta krug i kvadrat na standardnom GUI-u. Krugovi i kvadrati se trebaju nacrtati po odredjenim pravilima. Lista krugova i kvadrata se formira po odredjenom redosljedu a program mora biti sposoban da prodje kroz tu listu u tom redosljedu i da nacrta svaki krug ili kvadrat.

U programskom jeziku C, koristeći proceduralne tehnike nismo mogli ispoštovati „open-closed principle“, problem smo mogli rješiti na način prikazan u listingu 1. U listingu možemo primjetiti skup struktura podataka čiji je prvi elemenat identičan, ali se razlikuju u onome što je ispod njega. Taj prvi elemenat je tip koji identifikuje strukturu podataka – da li je to krug ili kvadrat. Funkcija DrawAllShapes prolazi kroz polje pokazivača za ovako izabrane strukture, birajući tip objekta za crtanje i tada poziva odgovarajuću funkciju koja realizuje konkretan zadatak (ili DrawCircle ili DrawSquare).

Listing 1

Procedural Solution to the Square/Circle Problem

Funkcija DrawAllShapes ne poštuje „open-closed principle“ jer ne može biti zaštićena u slučaju zahtjeva za crtanjem neke nove vrste objekta. Ukoliko želimo da proširimo funkciju na način da je sposobna da nacrta listu objekata koja u sebi sadrži i trouglove, ja bih morao da modifikujem funkciju. U stvari, ja bih funkciju morao modifikovati kad god se poželi nacrtati neki novi tip grafičkog objekta.

Naravno, ovaj program je samo jednostavni primjer. U realnom životu CASE struktura u DrawAllShapes funkciji bi se morala ponavljati na mnogim mjestima u nekoj hipotetičnoj aplikaciji; svaka od njih koristi ovu funkciju na neki „malo drugačiji način“. Dodavanjem novog grafičkog objekta za crtanje u ovakvu aplikaciju podrazumjeva traganje za svim pozivima ove funkcije u svim djelovima programskog koda gdje je ona pozvana i dodavanje novog dijela programskog koda. Čak šta više, veoma je vjerovatno da su u konkretnoj aplikaciji switch naredbe i lanci if/else naredbi mnogo manje pregledno struktuirani kako je to uradjeno sada u primjeru DrawAllShapes. Mnogo je vjerovatnije da su predikati u if naredbama kombinovani sa nekim drugim operatorima, ili da su case klauzule switch naredbe uvezane sa nekim pojednostavljenim mehanizmima za odlučivanje. Na taj način, problem pronalaženja i razumjevanja svih mjesta gdje treba intervenisati radi promjene postaje jako komplikovan.

Listing 2 prikazuje kod koji bi mogao biti odgovor na kvadrat/krug problem a koji zadovoljava „open-closed“ princip“. U ovom slučaju je kreirana apstraktna klasa Shape. Ova apstraktna klasa ima jednu čistu virtuelnu funkciju koju smo nazvali Draw. Obadvije funkcije Circle i Square su izvedene iz klase Shape.

 

Primjetite da ukoliko želimo da proširimo ponašanje DrawAllShapes funkcije u Listingu 2 na način da crta novu vrstu grafičkog objekta, sve što treba da uradimo je da napravimo novu izvedenu klasu iz klase Shape. DrawAllShapes funkcija se ne treba mijenjati. Na taj način DrawAllShapes zadovoljava „open-closed“ princip. Njeno ponašanje se proširuje a da se ona sama ne mijenja.

U realnom svijetu Shape klase bi trebalo da imaju mnogo više metoda. Ipak, dodavanje novog grafičkog objekta u aplikaciju je sasvim jednostavno, pošto jedino što se zahtjeva je da se napravi nova izvedena klasa i da se implementiraju sve njene funkcije. Nema potrebe da se ide kroz sve ostale aplikacije sistema i da se traga za eventualnim promjenama koje treba uraditi.

Na taj način, pošto su programi koji zadovoljavaju „open-closed“ princip promjenjeni na način što im je dodat novi programski kod, umjesto da je izmjenjen postojeći programski kod, oni ne prouzrokuju potrebu za kaskadnim izmjenama u ostalim djelovima programskog koda.

Zatvorenost – generalni stav

Trebalo bi ipak biti svima jasno da ne postoje programi koji su 100% zatvoreni i koji u potpunosti i do kraja mogu ispoštovati ovaj princip. Npr., razmotrimo šta bi se desilo da treba da nacrtamo sve funkcije iz listinga br.2 ali pod uslovom da odlučimo da se prvo moraju iscrtati krugovi poslije njih kvadrati. „DrawAllShapes“ funkcija nije zatvorena prema ovoj vrsti izmjene. Generalno, kako god da je modul „zatvoren“, uvjek postoji neka vrsta izmjene protiv koje on nije zatvoren.

Pošto zatvorenost ne može biti kompletna, mora se strateški postaviti. To znači, da dizajner mora tokom razvoja sistema da odluči na kojim i protiv kojih izmjena će se boriti implementirajući svoj dizajn. Ovo dovodi do odredjenog stepena predikcije koja je zasnovana na prethodnom iskustvu dizajnera. Iskusni dizajneri imaju dovoljno saznanja iz praktičnog rada tako da mogu da predvide u kojem pravcu će se kretati izmjene modula na kojem rade. Oni u tom slučaju sa velikom sigurnošću mogu da implementiraju „open-closed“ princip u svoje rješenje i da ono u najvećem broj slučajeva sasvim zadovoljava uslove eksploatacije.

Principi i konvencije

Kao što je napomenuto na početku ovog članka, „open-closed“ princip je u osnovi mnogih konvencija i preporuka koje su bile publikovane kroz OOD tokom godina. Evo nekoliko osnovnih medju njima.

Realizujte sve member promjenljive kao privatne

Ovo je jedna od najrasprostranjenijih konvencija u OOD. Promjenljive članice (member varijable) klasa bi trebale biti poznate samo metodama klase koje ih definišu. Member variable nebi trebale biti nikada poznate ni jednoj drugoj klasi, čak ni izvedenoj klasi. Zbog toga bi se one trebale deklarisati kao private, a ne kao public ili protected.

Sa stanovišta „open-closed“ principa, ovaj rezon je sasvim jasan. Kada se promjene promjenljive članice klasa, sve funkcije koje zavise od tih promjenljivih bi se morale mijenjati. Postavljajući ih kao privatne, ne stvaramo funkcije koje zavise od njih osim onih koje se nalaze u samoj klasi.

U OOD, očekujemo da metode klasa ne budu zatvorene u odnosu na izmjene zbog postojanja privatnih promjenljivih članica u tim istim klasama. Ipak, očekujemo da bilo koja druga klasa, uključujući i izvedene klase budu zatvorene za izmjene zbog promjene neke member promjenljive neke podklase. Imamo opšteprihvaćeno ime za ovo očekivanje, ono glasi: encapsulation.

Sada, šta ako imate member promjenljivu za koju znate da se nikada ne bi trebala promjeniti? Da li postoji bilo kakav razlog da ju napravite privatnom? Na primjer, listing 3 prikazuje klasu Device koja ima dvije statusne bool promjenljive. Ova promjenljiva sadrži status poslednje operacije. Ako je operacija bila uspješna , status će biti true; u suprotnom će biti false

Mi znamo da tip ili značenje ove promjenljive nikada neće biti izmjenjeno. Pa, zašto je ne napravimo javnom i omogućimo klijentu da jasnije vidi sadržaj koda? Ako se ova promjenljiva zaista nikada neće mijenjati, i ako ostali klijenti zaista poštuju pravilo i samo vrše čitanje sadržaja ove promjenljive, tada činjenica da se ova promenljiva proglasi public nije opasna. Ipak, razmotrimo šta će se desiti ako čak samo jedan klijent iskoristi prednost mogućnosti da pristupi i mjenja vrijednost ove promjenljive. Iznenada, ovaj jedan klijent će imati uticaja na sve ostale klijente koji koriste device. Ovo znači da je nemoguće ovim putem da spriječimo bilo kojeg neodgovornog klijenta da pristupi Device-u na nepropisan način. Ovo je vjerovatno preveliki rizik koji se ne bi trebao dozvoljavati.

Sa druge strane, pretpostavimo da imamo klasu „Time“ kako je predstavljeno u listingu 4. Kakva opsanost postoji kod javnih promjenljivih članica ove klase? Svakako, prilično je nevjerovatno da se one mijenjaju. Takodje, nebitno je i ako bilo koji od klijentskih modula uradi neku promjenu na promjenljivim, promjenljive su tako definisane da se očekuje da budu promjenjene od strane klijenta. Takodje, prilično je nevjerovatno da izvedene klase proizvedu bug na definisanim promjenljivima članicama. Dakle, ima li u ovome nekog problema?

Jedna primjedba koju mogu dati na prethodni listing je da izmjena „time“ nije „atomska“ (učaurena). To znači da klijent može da promjeni promjenljivu „minutes“ a da ne promjeni promjenljivu „hours“. To može dovesti do nekonzistentnog stanja cijelog „time“ objekta. Preferiram, da ukoliko negdje postoji jedna funkcija koja postavlja vrijeme da uzima 3 argumenta i na taj način učini izmjene ovog objekta učaurenim. Ali, ovo je veoma slab argument – preporuka ostavljena na volju developera.

Nije teško zamisliti druge okolnosti koje se mogu desiti zbog kojih javna priroda ovih promjenljivih može biti diskutabilna. Tokom nekog dužeg rada ipak nisu postojali razlozi da se ovo promjeni i da se promjenljive proglase privatnim. Samo smo razmotrili da je ovo loš stil – loše je da se one proglase javnim, ali u isto vrijeme ovo vjerovatno nije loš dizajn. Smatramo da je to loš stil jer je veoma mali trošak da se kreiraju odgovarajuće inline member funkcije.

Na taj način, u ovakvim (ipak rijetkim slučajevima) gdje „open-closed“ princip nije narušen, primjena javnih ili protect promjenljivih zavisi uglavnom od stila primjenjenog u realizaciji a ne od suštine rješenja.

Nikada ne koristite globalne promjenljive

Argumenat protiv globalnih promjenljivih je sličan argumentu protiv globalnih promjenljivih članica klase. Ne postoje moduli koji zavise od globalnih promjenljivih a koji se mogu zatvoriti i zaštititi protiv bilo kojeg drugog modula koji može da upiše nešta u tu globalnu promjenljivu. Bilo koji modul koji upotrebljava tu promjenljivu na način na koji to drugi moduli ne očekuju da će se ona upotrebljavati će izazvati bug u drugim modulima. Previše je riskantno da imate nekoliko modula koji zavise od ponašanje neke ovakve promjenljive.

Sa druge strane, u slučajevima u kojima globalna promjenljiva ima veoma malu zavisnost ili kada se ona ne može iskoristiti na nekonzistentan način, onda njena upotreba ne nosi veliki rizik. Dizajner sam mora presuditi koliko zatvorenosti je dovoljno prema globalnim promjenljivima i odlučiti da li upotreba globalne promjenljive zadovoljava trošak koji proizvodi.

Ponovo dolazimo do situacije iz priče o stilu. Alternativa za upotrebu globalnih promjenljivih je obično veoma jeftina. U ovim slučajevima loš stil rada je da se koriste tehnike koje su riskantne čak i kada sasvim mala količina zatvorenosti uz mali trošak proizvodi dobre rezultate. Ipak, postoje slučajevi gdje je konvencija za korištenje globalnih promjenljivih zanačajna. Globalne promjenljive cout i cin su jedan od primjera.

RTTI je opsano

Slijedeći, veoma rasprostranjeni stav je onaj protiv tzv. dynamic_cast (dinamičko vezivanje – late binding). generalni je stav da dynamic_cast, ili bilo kakav oblik run time identifikacije tipova (RTTI) je opasno i trebalo bi se izbjegavati. Slučajevi koji se često citiraju su slični onom prikazanom u Listingu 5 koji jasno narušava „open-closed“ princip. Ipak Listing 6 prikazuje slično rješenje koji koristi dynamic_cast, ali koji ne narušava „open-closed“ princip.

Razlika između ova dva programa je je taj da se Listing 5 mora promjeniti kada god se napravi novi tip grafičkog objekta koji želimo nacrtati.

Listing 5


 

 

 

 

Kako god, u listingu 6, kada se uvede novi tip grafičkog objekta, nije potrebno ništa dodatno raditi. Na taj način, Listing 6 ne narušava „open-closed“ princip.

 

Generalni stav je, ukoliko se upotrebljava RTTI na način da se ne narušava „open-closed“ princip, njegova upotreba je sigurna.

 

Zaključak

 

Postoji još mnogo toga što bi se moglo reći o „open-closed“ principu. Na mnoge načine ovaj princip se nalazi u jezgru objektno orjentisanog dizajna. Poštovanje ovog principa je ono što proizvodi najveće benefite u razvoju objektno orjentisane tehnologije. Ipak, postizanje ovog principa se ne realizuje jednostavnom upotrebom objektno orjentisanih programskih jezika. Umjesto toga, on zahtjeva odluke koje treba da donese dizajner da primjeni odredjene apstrakcije na one dijelove programa za koje pretpostavlja da će biti podložni izmjenama zahtjeva korisnika.

Leave a comment