Jedna od važnijih osobina dobro dizajniranih objektno-orijentisanih sistema je da oni omogućavaju lako i jednostavno menjanje pojedinih karakteristika. Ponašanje takvih programa može se promeniti zamenom objekata ili klasa koje određuju ponašanje. Ponekad je potrebno da se slična fleksibilnost ostvari i na nižim nivoima u programu, kada na primer promena stanja nekog objekta treba da promeni njegovo ponašanje ili kada treba omogućiti jednostavnu zamenu nekog algoritma. Stanje i Strategija su elementi objektno-orijentisanih sistema koji omogućavaju takve promene na nivou objekata.
Stanje
Stanje omogućava objektu da promeni svoje ponašanje kada mu se njegovo unutrašnje stanje promeni. Objekat se na ovakav način ponaša kao da je promenio klasu kojoj pripada. Ideju ćemo ilustrovati na primeru: recimo da želimo da implementiramo objekat koji uspostavlja, održava i zatvara TCP mrežnu vezu između vašeg programa i nekog drugog kompjutera. Ovakav objekat može da se nalazi u nekoliko različitih stanja: uspostavljena veza, „osluškivanje“ zahteva za uspostavljanje veze i zatvorena veza.
Ovakav objekat treba da se ponaša drugačije u zavisnosti od stanja u kom se nalazi. Ako neki drugi objekat pošalje zahtev Open() za otvaranje veze, objekat treba da reaguje drugačije u zavisnosti od toga da li je veza zatvorena ili uspostavljena. Ako je veza zatvorena, zahtev Open() treba da otvori novu vezu. Ako je veza uspostavljena, zahtev Open() treba to da stavi do znanja objektu koji pokušava da otvori vezu.
Već vam verovatno pada na pamet jedno rešenje problema: kod koji izvršava operaciju Open() treba prvo da proveri u kom je stanju objekat, pa da onda zavisno od tog stanja izvrši zahtevanu operaciju. Ovo rešenje je zadovoljavajuće ako objekat može da ima samo par različitih stanja i ako je broj operacija koje zavise od tog stanja mali. Porastom broja stanja i operacija ovakvo rešenje se znatno komplikuje: svaka operacija će imati mnogo različitih „reakcija“ na stanje objekta koje u opštem slučaju nemaju nikakve veze jedna s drugom, i svaka operacija će morati da ispituje stanje objekta i na osnovu njega donosi odluke. Ovo vodi previše komplikovanom kodu, koji je „iseckan“ na delove koji nemaju veze jedan sa drugim, ponavljanju koda koji ispituje stanje objekta u svim operacijama, komplikovanju dodavanja novih stanja i većoj verovatnoći grešaka u programu.
U ovakvim slučajevima mnogo je bolje koristiti element objektno-orijentisanih sistema Stanje, koji omogućava elegantno, jednostavno i fleksibilno rešenje ovog problema.
Primena
| (kliknite za veću sliku) |
Osnovna ideja ovakvog prilaza problemu je da se uvede apstraktna klasa TCPState koja opisuje stanje objekta TCPConnection. Ova apstraktna klasa ima po jednu konkretnu supklasu za svako moguće stanje objekta, i te konkretne supklase implementiraju operacije koje su zavisne od stanja objekta, kao što je to prikazano na slici 1.
Apstraktna klasa TCPState služi za opisivanje stanja objekta TCPConnection. Konkretne klase TCPEstablished, TCPListen i TCPClosed opisuju stanje tog objekta kada je veza uspostavljena, kada objekat „osluškuje“ zahteve za uspostavljanje nove veze i kada je veza zatvorena, respektivno. Klase implementiraju operacije koje su zavisne od stanja. Na primer, operacija Open() u klasi TCPEstablished će objektu koji zahteva otvaranje veze da stavi do znanja da je veza već uspostavljena, dok će operacija Open() u klasi TCPClosed da otvori novu vezu.
Objekat TCPConnection sadrži pokazivač na objekat tipa TCPState. Sve operacije koje su zavisne od stanja objekta, ovaj objekat direktno prosleđuje objektu stanja. Na ovaj način je promena stanja izuzetno jednostavna: dovoljno je usmeriti ovaj pokazivač na objekat koji opisuje novo stanje. Na primer, posle otvaranja nove veze (tokom koje je pokazivač pokazivao na objekat tipa TCPClosed), pokazivač će pokazivati na objekat tipa TCPEstablished. Uočićete da je u ovakvom sistemu izuzetno jednostavno dodati novo stanje: samo kreirate klasu koja je potklasa klase TCPState i implementirate operacije koje su zavisne od stanja. Kompletan mehanizam za ubacivanje i korišćenje tog novog stanja je već na svom mestu.
Stanje treba upotrebljavati kada ponašanje nekog objekta zavisi od njegovog stanja, i pri tome se ponašanje mora menjati u toku izvršavanja programa, odnosno kada operacije u vašem objektu imaju velike, kondicionalne iskaze koji ispituju stanje objekta i izvršavaju različite zadatke u zavisnosti od tog stanja. Ovakvi kondicionalni izrazi često su u praksi skoro identični u vašim operacijama (switch izrazi sa istovetnim konstantama, na primer).
Treba uočiti nekoliko bitnih karakteristika Stanja i imati ih u vidu prilikom upotrebe ovog elementa objektno-orijentisanih sistema. Stanje lokalizuje ponašanje koje je specifično za pojedino stanje, dok razdeljuje ponašanje koje je specifično za pojedine operacije. Stanje stavlja sve što je specifično za pojedino stanje objekta unutar tog objekta. Pošto je sav kod koji zavisi od stanja lokalizovan unutar potklasa apstraktnog stanja, vrlo je jednostavno dodavati nova stanja. Alternativa ovakvom rešenju je da „ručno“ proveravate stanje unutar svake operacije i da izvršavate kod zavisno od rezultata, što vodi do koda koji je komplikovan, nefleksibilan i pun grešaka.
Promena stanja je eksplicitna. Umesto da je stanje objekta implicitno definisano kombinacijom sadržaja promenljivih unutar objekta, ono je sada definisano stanje samo jedne promenljive (pokazivača na objekat stanja). Bez ovakvog pristupa, promena stanja u objektu bi se implicitno dešavala svaki put kada dodelite novu vrednost nekoj od tih promenljivih, dok se sada to sasvim jasno uočava u programu jer je stanje „podignuto“ na nivo objekta. Takođe, ovaj pristup zaštićuje objekat od neusaglašenog stanja, jer se čitavo stanje menja odjednom (promenom pokazivača), dok se u originalnom pristupu promenljive menjaju jedna po jedna, što može da dovede do neusaglašenih stanja, naročito u distribuiranim sistemima. Ako je stanje objekta definisano isključivo tipom objekta stanja (to jest, objekti stanja nemaju nikakve promenljive unutar njih), onda se ovakvi objekti stanja mogu deliti između više objekata. U suštini, objekti stanja u ovom slučaju predstavljaju Flyweight dizajn (pogledajte tekst objavljen u „PC #73“) jer ne sadrže nikakve podatke već samo ponašanje.
Strategija
| (kliknite za veću sliku) |
Strategija određuje familiju algoritama koji se, iako su nezavisno definisani, mogu koristiti jedan umesto drugog. Strategija na ovaj način omogućava da se algoritmi definišu nezavisno od klijenta koji ih koristi. Na primer, postoje mnogi algoritmi koji služe za podelu teksta na linije. Ako je podela teksta na linije neophodna u vašem sistemu, morate razmisliti da li je direktna implementacija jednog takvog algoritma u vašem sistemu dobra ideja. Direktna implementacija vam neće uvek odgovarati jer, recimo, vaš kod postaje duži i komplikovaniji ako u njega uključite i kod za podelu teksta na linije. Samim tim vaš kod postaje teži za održavanje, pogotovo ako želite da podržite upotrebu više različitih algoritama za podelu teksta na linije.
Svi znamo da su različiti algoritmi primenljivi u različitim situacijama. Nema potrebe da podržavate sve algoritme za podelu teksta na linije ako ih vaš sistem sve ne upotrebljava, a vrlo je teško dodavati nove algoritme ili varirati postojeće ako su ti algoritmi direktno implementirani u vašem sistemu. Ove probleme možete izbeći ako definišete klase koje implementiraju različite algoritme za podelu teksta na linije. Ovakva dekompozicija algoritama naziva se Strategijom.
Na slici 2 prikazana je hijerarhija klasa koje definišu Strategiju za algoritme za podelu teksta na linije. Recimo da je klasa Composition zadužena za održavanje i ažuriranje krajeva linija u programu za prikaz i editovanje teksta. Algoritmi koji odlučuju na kojim mestima u tekstu treba staviti novu liniju nisu implementirani direktno u klasi Composition, već u konkretnim klasama koje su potklase apstraktne klase Compositor. Ove apstraktne klase implementiraju različite algoritme za podelu teksta na linije. SimpleCompositor implementira jednostavan algoritam za podelu teksta na linije. Svaka linija se obrađuje pojedinačno; kada reči više ne mogu da stanu u tu liniju, ubacuje se novi red. TeXCompositor je sofisticiraniji algoritam koji pokušava da optimizuje ubacivanje kraja linije na nivou paragrafa, tako da paragraf kao celina bolje izgleda. Ovo može da znači da se tekst prekida na različitim mestima nego kao kod SimpleCompositor algoritma, jer je cilj da se dobije paragraf koji izgleda podjednako „popunjen“ u svakoj liniji. Najzad, ArrayCompositor prekida linije tako da svaka linija ima podjednak broj elemenata. Ovakav algoritam nije naročito koristan za podelu teksta, ali može da se koristi za podelu kolekcije ikona u kolone.
Objekat tipa Composition ima pokazivač na objekat tipa Compositor. Kad god Composition reformatira tekst, on poziva Compositor da izdeli tekst na linije. Klijent koji koristi klasu Composition odlučuje o tome koji od navedenih algoritama će se koristiti. Jednostavnom instalacijom odgovarajućeg Compositor objekta klijent dobija željeno ponašanje.
Strategiju treba koristiti kada se više srodnih klasa razlikuje samo po svom ponašanju. Strategija vam omogućava da kreirate jednu klasu koja može da se konfiguriše tako da se ponaša kako vama odgovara. Primenićete je i kada su vam potrebne različite varijante nekog algoritma – algoritmi za rešavanje nekog problema mogu da imaju različite memorijske i procesorske zahteve. Strategija vam omogućava da na vrlo jednostavan način prilagodite vaš softver datoj mašini. Najzad, neki algoritam koristi podatke koji ne treba da budu dostupni klijentu; ako koristite Strategiju, vaše kompleksne i specifične strukture podataka će ostati sakrivene za klijenta.
Smisao Strategije primetićete i kod klasa koje definišu više različitih ponašanja, s tim što se ta ponašanja pojavljuju kao višestruki kondicionalni iskazi unutar metoda. Umesto da se jedno ponašanje pojavljuje unutar kondicionalnog iskaza, upotrebom Strategije to ponašanje možete da izdvojite u zasebnu klasu.
Prednosti i nedostaci
Kod kreiranja „familija“ srodnih algoritama možete koristiti nasleđivanje na više nivoa (a ne samo na jednom kao u našem primeru) da definišete nove algoritme koji variraju ponašanje njihovih roditelja. Nasleđivanje vam omogućava da izdvojite zajedničko ponašanje različitih algoritama i da to ponašanje smestite u klasu roditelja. Šablon („PC #82“) je vrlo koristan element objektno-orijentisanih sistema koji vam omogućava da ovo uradite na elegantan način;
U nekim situacijama, Strategija je bolja alternativa nasleđivanju. U našem primeru, mogli smo direktno da napravimo supklase klase Composition, i da nateramo te klase da implementiraju različite algoritme za podelu teksta na linije. Međutim, ovakav pristup primorava algoritam da bude „uklesan“ unutar klase Composition, što otežava razumevanje, održavanje i ažuriranje klase Composition. Takođe, na ovakav način nećete moći da menjate algoritam u toku izvršavanja programa. Korišćenje Strategije omogućava vam da algoritme menjate nezavisno od konteksta u kojem se koriste, što olakšava razumevanje, promenu i ažuriranje.
Strategija eliminiše kondicionalne iskaze – ponašanja koja su izdeljena korišćenjem kondicionalnih iskaza dobijaju svoje sopstvene klase, i veliki, komplikovani kondicionalni iskazi nestaju iz koda. Osim toga, Strategija vam daje elegantan izbor implementacije između više algoritama koji implementiraju isto ponašanje ali imaju različite memorijske ili procesorske zahteve.
Pre nego što klijent može da izabere algoritam koji želi, mora da zna koje su karakteristike algoritma. Ovo može da dovede to nepotrebnog izlaganja klijentu detalja implementacije pojedinih algoritama. Koristite Strategiju samo u slučajevima kada ima smisla da klijent zna takve detalje.
Pošto je interfejs svih konkretnih strategija ujedinjen (metod Compose() u našem primeru), može se desiti da neke jednostavnije Strategije uopšte ne koriste parametre koji su potrebni komplikovanijim Strategijama. Na primer, TeXCompositor-u je možda potreban opis paragrafa kojem treba da teži („gust“, „redak“, „novinski“, itd.), dok će SimpleCompositor ignorisati tu informaciju. Nažalost, Composition mora da pripremi i inicijalizuje tu informaciju bez obzira na to koji se Compositor poziva (jer Composition i ne zna koji se Compositor poziva), što može da traje dugo. Ako je ovo problem u vašem sistemu, onda morate razmisliti o intimnijem povezivanju strategije sa kontekstom.
Strategija uvodi nove objekte u sistem, što može da bude problem. Problem možete da ublažite ako pojedinačne strategije definišete tako da nemaju promenljivih već samo implementiraju ponašanje. Korišćenjem Flyweight dizajna („PC #73“) možete da smanjite broj objekata neophodnih za implementaciju ovakvih Strategija.
Kao što ste verovatno već uočili, Stanje i Strategija imaju vrlo sličnu strukturu (pogledajte slike 1 i 2), ali su njihov smisao i primena bitno različiti. Stanje sa koristi kada želite da izdvojite ponašanje koje zavisi od stanja objekta tako da se ponašanje menja sa eksplicitnom promenom stanja, dok se Strategija koristi kada želite da objedinite različite algoritme koji implementiraju isto ponašanje, i da omogućite jednostavnu i elegantnu promenu tih algoritama, čak i u vreme izvršavanja programa.
Ova dva elementa objektno-orijentisanog programiranja imaju niz prednosti u odnosu na „klasične“ metode i mogu vam pomoći u pisanju elegantnih, razumljivih i efikasnih programa. Naravno, kao i uvek do sad, morate voditi računa o tome da ih primenjujete samo u situacijama kada su njihove prednosti važnije od nedostataka. Nadajmo se da će vam ovaj članak bar malo pomoći u razumevanju ovih elemenata i situacija u kojima su primenljivi.
|