Razvoj aplikacije
Objektno programiranje je skup tehnika koje imaju za cilj pisanje kvalitetnog softvera.
Što je kvalitetna aplikacija?
-
Aplikacija koja radi (ono što se od nje očekuje);
-
Aplikacija koja nastavlja raditi i kada se koristi na nestandardan ili krivi način;
-
Aplikacija koja je obnovljiva (korisnik očekuje nove verzije, eliminacije bugova itd.);
-
Kod koji je ponovo iskoristiv u novom kontekstu (code reuse);
-
Kod koji je fleksibilan - lako omogućava izmjene, dogradnju.
Od 1 do 3 su kriteriji korisnika, a 4 i 5 su kriteriji programera.
Etape razvoja
-
Analiza
-
Skupljanje i organizacija informacija iz aplikacijske domene; Analiza zahtjeva; Stvaranje scenarija korištenja softwarea (use cases).
-
-
Dizajn
-
Razbijanje zadaće na manje cjeline, globalna arhitektura aplikacije, klase, objekti i njihova komunikacija.
-
-
Programiranje
-
C++
-
-
Testiranje
-
Testirati glavne i sporedne scenarije korištenja, posebno one koji uključuju greške.
-
Dizajn aplikacije
Kako odrediti klase u aplikaciji i njihove funkcije članice? Kakva će biti interakcija među objektima?
Tekstualna analiza
U scenarijima korištenja aplikacije treba potražiti imenice i glagole.
-
Imenice odgovaraju klasama,
-
Glagoli odgovaraju funkcijama članicama.
Kada se shvati doslovno vodi do suviše velikog broja klasa. Tekstualna analiza daje indikaciju koje klase i metode treba implementirati.
Pri konstrukciji klasa treba uvažavati principe objektnog programiranja.
Razvoj aplikacije u tri koraka
-
Osigurati da software radi ono što korisnik od njega očekuje;
-
Primijeniti principe objektno-orijentiranog programiranja kako bi se povećala fleksibilnost koda;
-
Težiti prema održivom i ponovo iskoristivom dizajnu — koristiti oblikovne obrasce (design patterns).
Principi objektnog programiranja
Princip jedne odgovornosti
(The single responsibility principle) Svaki objekt u programu treba imati jednu odgovornost i sve njegove funkcije trebaju biti podređene toj odgovornosti.
Ovaj princip inzistira na koheziji klase. Sve odgovornosti klase su međusobno snažno povezane. Klasa visoke kohezije obavlja jednu zadaću i ne pokušava obavljati drugu, s prvom nevezanu zadaću.
Na primjer, klasa koja formatira i printa izvještaje narušava PJO i treba biti razdvojena na dvije klase. PJO traži da klasa ima samo jedan razlog za promjenu.
Princip otvorenosti/zatvorenosti
(The open/closed principle) Princip kaže da klasa (modul, funkcija) treba biti otvorena za proširenja i zatvorena za modifikacije.
To znači da klasa treba dozvoliti modifikaciju svog ponašanja bez izmjene svog koda. Postiže se proširivanjem klase putem nasljeđivanja — nasljeđivanjem sučelja i korekcijom implementacije.
Kodiranje kroz sučelje
(Coding to an interface) Kad god je moguće treba koristiti sučelja (apstraktne klase) umjesto konkretnih klasa. Na taj se način, kroz polimorfizam, postiže fleksibilniji kod koji može raditi s nizom različitih objekata.
Sučelje predstavlja ono što je zajedničko čitavoj porodici tipova.
Identificiraj dio koji varira i enkapsuliraj ga
Treba identificirati dio programa koji je podložan promjeni i izoliraj ga od stabilnijeg koda kreiranjem nove apstrakcije (klase).
Bez ponavljanja
(The don’t repeat yourself principle) Izbjegavaj dupliciranje koda apstrahiranjem zajedničkog koda i njegovim smješajem na posebno mjesto.
Pored izbjegavanja dupliciranja koda ovaj princip ima važniju posljedicu: svaka funkcionalnost u programu treba biti implementirana samo jednom i na jedinstvenom mjestu.
Liskovin princip supstitucije
Podtipovi moraju moći supstituirati svoj bazni tip.
Operacije nad podtipovima moraju biti neovisne o stvarnom tipu tako da bazni tip možemo uvijek zamijeniti podtipom.
Preferiraj delegaciju, kompoziciju i agregaciju pred nasljeđivanjem
-
Delegiranje: čin kojim jedan objekt proslijeđuje operaciju drugom objektu, koji ju je sposoban izvršiti.
-
Agregacija: objekt sadrži druge objekte koji imaju funkcionalnost koja mu treba.
-
Kompozicija: agregacija pri kojoj je kompozitni objekt vlasnik svojih dijelova.
Nasljeđivanje (specijalizacija) je statički odnos među klasama i manje je fleksibilan od delegacije, kompozicije ili agregacije.
Oblikovni obrasci (Design Patterns)
Oblikovni obrasci (eng. design patterns) naziv su za rješenja problema koji se često javljaju u konstrukciji koda. Svaki obrazac predstavlja rješenje za jedan oblikovni problem. Opis problema koji obrazac rješava objašnjava nam kada obrazac treba primijeniti; rješenje koje obrazac nudi nam objašnjava kako ćemo ga implementirati u danoj situaciji.
Svaki obrazac ima ime, što je važno za komunikaciju među programerima.
Temeljna literatura o o oblikovnim obrascima je
-
Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides: Design Patterns. Addison-Wesley; 1995.
Obrasci se obično dijele u tri grupe
-
Obrasci stvaranja [Creational Patterns]
-
Abstract Factory, Builder, Factory Method, Object Pool, Prototype, Singleton
-
-
Strukturni obrasci [Structural Patterns]
-
Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy
-
-
Obrasci ponašanja [Behavioral Patterns]
-
Chain of responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template method, Visitor
-
Obrasci stvaranja
Abstract Factory, Builder, Factory Method, Object Pool, Prototype, Singleton
-
Prototype
-
Factory Method
-
Singleton
Prototype
Ovaj obrazac koristimo kada želimo kreirati objekt polazeći od danog prototipa.
Struktura
Sudionici
-
Prototype. Bazna klasa koja deklarira virtualnu funkciju
clone()
. -
ConcretePrototype. Izvedene klase koje implementiraju funkciju
clone()
koja kreira objekt tipa klase. -
Client. Kreira novi objekt tražeći prototip da se klonira.
Factory method (virtualni konstruktor)
Namjera
Definirati sučelje za kreiranje objekata i pustiti podklasama da odluče koji će tip objekta instancirati.
Ovaj obrazac koristimo kada imamo različite tipove objekata koje koristimo kroz zajedničko sučelje. Cilj je da klijent tog sučelja može kreirati objekte ne vodeći računa o njihovom tipu.
Struktura
Učesnici
-
Product
je sučelje za niz tipova koje koristimo samo kroz sučelje. Konkretni tipovi suProductA
,ProductB
itd. -
Creator
je sučelje za klase koje instancirajuProduct
. Svakom konkretnom produktu odgovara konkretanCreator
koji instanciraProduct
; na primjer,CreatorA
instanciraProductA
itd. -
Kod klijent će uzeti referencu na
Creator
pomoću koje će kreiratiProduct
. Na taj način može biti napisan posve neovisno o stvarnom tipuProduct
-a, što je cilj obrasca.
Primjer:
-
1.
Product
klase:
// Formater je interface za različite objekte koji formatiraju
// text. Konkretni formateri prerađuju metodu format().
class Formater{
public:
virtual void format(std::string &) const = 0;
virtual ~Formater(){}
};
class FormaterA : public Formater{
public:
void format(std::string & str) const {
std::string tmp = "//: ";
str = tmp + str;
}
};
class FormaterB : public Formater{
public:
void format(std::string & str) const {
std::string tmp = "\?\?- ";
str = tmp + str;
}
};
-
2.
Creator
klase:
// FormaterCreator je sučelje za sve Creator klase.
// Konkretne Creator klase će instancirati konkretne Formater
// objekte.
class FormaterCreator{
public:
virtual Formater* make() const = 0;
virtual ~FormaterCreator(){}
};
class FormaterACreator : public FormaterCreator{
public:
virtual Formater* make() const { return new FormaterA; }
};
class FormaterBCreator : public FormaterCreator{
public:
virtual Formater* make() const { return new FormaterB; }
};
-
3. Kode klijent.
// Cilj obrasca je omogućiti pisanje klijenta neovisno o tipu konkretnog
// formatera s kojim radi. Klijent radi samo sa sučeljima
// Formater i FormaterCreator.
class Application{
public:
void work(FormaterCreator const * formater){
std::string text;
text = "bla bla bla";
// ...
Formater * pcc = formater->make();
pcc->format(text);
std::cout << text << std::endl;
delete pcc;
}
};
-
4. Primjena:
int main(){
// Odluka o tipu formatiranja se donosi izvan aplikacije.
FormaterCreator * f = new FormaterACreator;
Application app;
app.work(f);
delete f;
f = new FormaterBCreator;
app.work(f);
delete f;
return 0;
}
Posljedice i varijante
-
Nakon dodavanja novog
Product
objekta treba samo dodati odgovarajućuCreator
podklasu i kod klijent može raditi s novim objektom bez izmjena. -
Potrebno je imati dvije paralelne hijerarhije klasa. To obrazac čini najprirodnijim kada takve hijerarhije postoje i zbog drugih razloga.
-
FactoryMethod
može biti čista virtualna funkcija ali i ne mora ako postoji defaultna implementacija. -
FactoryMethod
može uzimati parametar i kreirati različite objekte ovisno o vrijednosti parametra. -
Broj konkretnih
Creator
klasa se može reducirati na jednu ako se klasa parametrizira s produktom. Na primjer:
template <typename TheFormater>
class TheCreator : public FormaterCreator{
public:
virtual Formater* make() const { return new TheFormater; }
};
TheCreator< ... >
zamijenjuje FormaterACreator
i FormaterBCreator
klase.
Singleton
Namjera
Osigurati da klasa ima samo jednu instancu i osigurati joj globalni pristup.
Može se implemetirati tako da sama klasa čuva svoju jedinstvenu instancu i dozvoljava joj pristup.
Struktura
Primjer:
class Singleton{
public:
// Metoda koja vraća jedinstvenu instancu klase
static Singleton & instance();
Singleton(const Singleton &) = delete;
Singleton(Singleton &&) = delete;
Singleton & operator=(const Singleton &) = delete;
Singleton & operator=(Singleton &&) = delete;
~Singleton() { delete _instance; }
// Ostale metode klase
int get_data() const { return _data; }
void set_data(int d){ _data = d; }
private:
// Konstruktor -- privatan da ne bi mogao biti pozvan
Singleton() : _data(0) {}
// Statička varijabla koja referira na jedinstvenu
// instancu klase
static Singleton* _instance;
// Ostale varijable klase
int _data;
};
// Inicijalitacija statičke varijable koja referiran na
// jedinstvenu instancu klase
Singleton* Singleton::_instance = nullptr;
Singleton & Singleton::instance()
{
if(_instance == nullptr){
_instance = new Singleton();
}
return *_instance;
}
int main()
{
Singleton & ps1 = Singleton::instance();
Singleton & ps2 = Singleton::instance();
// Ispisuju istu adresu
std::cout << &ps1 << std::endl;
std::cout << &ps2 << std::endl;
// Dohvat objekta može ići kroz instance() metodu.
Singleton::instance().set_data(3);
std::cout << Singleton::instance().get_data() << std::endl;
return 0;
}
Singleton - jednostavnija implementacija
Moguća je jednostavnija verzija bez dinamičke alokacije memorije.
class Singleton{
public:
// Metoda koja vraća jedinstvenu instancu klase
static Singleton & instance(){
static Singleton _instance;
return _instance;
}
Singleton(const Singleton &) = delete;
Singleton(Singleton &&) = delete;
Singleton & operator=(const Singleton &) = delete;
Singleton & operator=(Singleton &&) = delete;
// Ostale metode klase
int get_data() const { return _data; }
void set_data(int d){ _data = d; }
private:
// Konstruktor -- privatan da ne bi mogao biti pozvan
Singleton() : _data(0) {}
// Ostale varijable klase
int _data;
};
Uočite razliku između statičke varijable članice klase i statičke lokalne varijable unutar funkcije.
Strukturni obrasci
Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy
-
Adapter
-
Composite
Adapter
Namjera
Ponekad imamo klasu koja nam nudi funkcionalnost koja nam je potrebna, ali nema odgovarajuće sučelje. U tom slučaju trebamo konvertirati sučelje koje imamo u sučelje koje klijent očekuje.
Struktura
Učesnici
-
Klijent ima referencu na
Target
i koristi sučelje klaseTarget
; -
Traženu funkcionalnost daje klasa
Adaptee
, ali ona nema traženo sučelje. -
Konstruiramo klasu
Adapter
koja:-
javno nasljeđuje klasu
Target
i stoga ima sučelje klaseTarget
; -
privatno nasljeđuje klasu
Adaptee
koju koristi za implementaciju svog sučelja.
-
Varijanta
Umjesto privatnog nasljeđivanja možemo uvijek koristiti agregaciju:
Adapter - primjer
Imamo klasu Graphics
koja koristi SFML biblioteku za iscrtavanje ravninske triangulacije.
class Graphics{
public:
Graphics(SFMLGrid * pGrid, std::string const & fileName);
void run();
private:
sf::RenderWindow mWindow;
SFMLGrid * mGrid;
void processEvents();
void update();
void render();
};
Triangulaciju pamti klasa SFMLGrid
u obliku sf::VertexArray
objekta koji sadrži sve stranice triangulacije.
Objekti te klase se znaju iscrtati.
class SFMLGrid : public sf::Drawable, public sf::Transformable{
public:
SFMLGrid();
virtual void readFromFile(std::string const & fileName);
private:
sf::VertexArray mVA;
void draw(sf::RenderTarget & target, sf::RenderStates states) const override;
};
Imamo klasu GmshGrid
koja može pročitati mrežu generiranu pomoću gmsh alata za generiranje
triangulacije.
class GmshGrid{
public:
using Triangle = std::tuple<std::size_t, std::size_t, std::size_t>;
GmshGrid();
void read(const std::string & fileName);
// ...
private:
std::vector<double> mVertexXcoor;
std::vector<double> mVertexYcoor;
std::vector<Triangle> mElement;
};
Ova klasa koristi drugačije sučelje, drugačiju strukturu podataka za pamćenje mreže i ne zna se iscrtavati.
Konstruiramo klasu Adapter
koja adaptira GmshGrid
na sučelje a SFMLGrid
klase:
class Adapter: public SFMLGrid, private GmshGrid{
public:
Adapter();
void readFromFile(std::string const & fileName);
private:
// ...
};
Klasa Adapter
prerađuje metodu readFromFile()
koristeći GmshGrid::read()
te radi svu potrebnu adaptaciju.
Composite
Problem: Potrebno je strukturirati složeni objekt kao stablo koje predstavlja hijerarhiju sastavnih dijelova. Cilj je da klijent može jednako tretirati i dio i cjelinu.
Primjer: Folder na disku može sadržavati tekstualne datoteke, binarne datoteke linkove i druge foldere. Tu je evidentna struktura cjelina (floder) — dio (razne vrste datoteka).
Rješenje se bazira na tome da ista apstraktna klasa predstavlja i složeni i elementarni objekt.
Struktura
Učesnici:
-
Component
predstavlja sučelje svih komponenti u hijerarhiji. Nudi implementaciju
koja je zajednička komponentama listovima i složenim komponentama.
-
Leaf
je komponenta list koja nema djece. Određuje ponašanje primitivnih objekta u kompoziciji. -
Composite
je komponenta s djecom. Drži reference na djecu i implementira operacije vezane uz djecu.
Dijagram objekata
U našem primjeru je Composite
objekt folder, dok su Leaf
objekti tekstualna datoteka, binarna datoteka i link na
datoteku. Tipičan primjer dekompozicije složenog objekta je dan na ovoj slici:
Dijagram objekata prikazuje objekte. Strelice ukazuju na objekte koje dani objekt referira.
Varijanta
Metode za rad s djecom mogu se staviti u Composite
klasu jer djeca
(Leaf
objekti) nemaju potrebe za njima. Tada u svakom Composite
objektu koji dohvaćamo kroz Component
sučelje treba vratiti izgubljenu informaciju
o tipu. To se može napraviti na sljedeći način:
U Component
se doda metoda (koju Leaf
ne prerađuje):
virtual Composite* GetComposite() { return nullptr; }
U Composite
se metoda preradi u
virtual Composite* GetComposite() { return this; }
Klijent sada može ispitivati da li ima posla s Composite objektom:
if (test = aComponent->GetComposite()) {
test->addComponent(new Leaf);
}
Zadatak
Implementirati Composite predložak u kojem je Composite
objekt Folder
(direktorij),
dok je Leaf
objekt File
(datoteka). Ovi objekti pamte samo svoje ime i u svom sučelju imaju
samo metodu print
koja ispisuje ime na izlaznom streamu s odgovarajućim uvlačenjem radi preglednosti.
Sljedeći kod
#include "composite.hh"
int main()
{
Component * folder1 = new Folder("top");
Component * file1 = new TextFile("file1");
Component * file2 = new TextFile("file2");
Component * folder2 = new Folder("folder");
Component * file3 = new TextFile("file3");
Component * file4 = new TextFile("file4");
folder1->addComponent(file1);
folder1->addComponent(file2);
folder1->addComponent(folder2);
folder1->addComponent(file4);
folder2->addComponent(file3);
folder1->print(std::cout);
return 0;
}
mora dati ovaj ispis:
top
file1
file2
folder
file3
file4
Obrasci ponašanja
Chain of responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template method, Visitor
-
Template method
-
Strategy
-
Command
-
Iterator
-
Observer
-
Visitor
Template method
Namjera
Definiraj skeleton algoritma u baznoj klasi razbijanjem algoritma na niz bazičnih operacija. Dozvoli izvedenim klasama da prerade bazične operacije i tako alternira algoritam.
Struktura
Glavna posljedica
Iskorištavanje postojećeg koda u baznoj klasi.
Primjer
Shellov algoritam sortiranja može se implementirati na ovaj način:
void shell_sort(int v[], int n)
{
for (int g = n / 2; g > 0; g /= 2)
for (int i = g; i < n; i++)
for (int j = i - g; j >= 0; j -= g)
if (v[j] > v[j + g])
std::swap(v[j], v[j + g]);
}
On sortira u rastućem poretku. Shellsort koji sortira u padajućem poretku ima implementaciju:
void shell_sort_back(int v[], int n)
{
for (int g = n / 2; g > 0; g /= 2)
for (int i = g; i < n; i++)
for (int j = i - g; j >= 0; j -= g)
if (v[j] < v[j + g])
std::swap(v[j], v[j + g]);
}
To je puno ponavljanja koda.
Template method implementacija
-
1. Bazna klasa.
// apstraktna baza
class AbstractSort{
public:
void sort(int v[], int n) const; // nevirtualna metoda
virtual ~AbstractSort(){}
protected:
virtual bool doSwap(int, int) const = 0 ;
};
-
2. U implementaciji metode
sort()
faktoriziran je varijabilini dio:
void AbstractSort::sort(int v[], int n) const
{
for (int g = n / 2; g > 0; g /= 2)
for (int i = g; i < n; i++)
for (int j = i - g; j >= 0; j -= g)
if (doSwap(v[j], v[j + g]))
std::swap(v[j], v[j + g]);
}
-
3. U izvedenim klasama daje se odgovarajuća implementacija faktorizirane bazične operacije
doSwap()
:
class SortUp : public AbstractSort{
protected:
virtual bool doSwap(int a, int b) const { return a > b; }
};
class SortDown : public AbstractSort{
protected:
virtual bool doSwap(int a, int b) const { return a < b; }
};
-
4. Primjena:
int main()
{
const int NUM = 10;
int array[NUM];
std::random_device rd;
std::mt19937 mt(rd());
std::uniform_int_distribution<> dist(1, 10);
for (int i = 0; i < NUM; i++)
{
array[i] = dist(mt);
std::cout << array[i] << ' ';
}
std::cout << '\n';
SortUp su;
su.sort(array, NUM);
for (int i = 0; i < NUM; i++)
std::cout << array[i] << ' ';
std::cout << '\n';
SortDown sd;
sd.sort(array, NUM);
for (int i = 0; i < NUM; i++)
std::cout << array[i] << ' ';
std::cout << '\n';
return 0;
}
-
5. Primjer ispisa:
2 3 6 3 3 5 10 5 3 7
2 3 3 3 3 5 5 6 7 10
10 7 6 5 5 3 3 3 3 2
Strategy
Namjera
Definiraj familiju algoritama istog sučelja kako bi bili zamjenjivi. Učini klijenta neovisnim o izabranom algoritmu.
Struktura
Učesnici
-
Strategy
je apstraktna baza za konkretne algoritme; -
ConcreteStrategy
su klase koje implementiraju algoritam; -
Context
je klijent koji je parametriziran algoritmom. Može imati sučelje koje dozvoljava algoritmima da dohvate njegovo stanje.
Posljedice
-
Klijent se parametrizira jednim is skupine algoritama koji moraju biti srodni u smislu da mogu imati zajedničko sučelje.
-
Klijent je zaštićen od implementacijskih detalja algoritama i njihovih podataka.
-
Obrazac izbjegava stvaranje novih klasa nasljeđivanjem klijenta.
-
Obrazac izbjegava kodiranje
if
naredbi u kojima bi se birao algoritam.
Command
Problem. Ponekad je potrebno izvršiti operaciju na nekom objektu pri čemu niti o objektu niti o operaciji nemamo potrebnih informacija.
Rješenje. Zatvoriti traženu operaciju u objekt.
Primjer. GUI element, kao što je na primjer button, treba nakon primljenog klika mišem izvršiti određenu operaciju kao odgovor na korisnikovu akciju. Tražena akcija ne može biti implementirana u GUI elementu jer informaciju o njoj ima samo aplikacija koja element koristi.
-
Aplikacija insancira
Button
dajući mu referencu/pokazivač na odgovarajuću naredbu (command
). -
Button
na klik miša reagira pozivom metodiclicked()
koja na referencicommand
poziva metoduexecute()
. -
Command
objekt pri konstrukciji dobiva referencu na aplikaciju (koja ga je kreirala) i kroz nju može izvršiti potrebnu operaciju (na primjer pozvati metodushowCopyrightInfo()
).
Posljedica: Inicijator operacije (ovdje Button
) je parametriziran
operacijom (kroz command
) i stoga ne mora poznavati niti operaciju (prikaz
informacije o autorskim pravima) niti primatelja operacije (objekt na kome je operacija
izvršena, ovdje Application
objekt).
Command - struktura
Učesnici
-
Command
- apstraktna bazna klasa za konkretne operacije. Inicijator operacije ima samo referencu tipaCommand
i stoga ne treba poznavati tip konkretne komande. -
ConcreteCommand
- konkretna operacija. Klasa drži referencu na primatelja operacije (Receiver
objekt) i implementira samu operaciju (execute()
metoda). -
Invoker
- inicijator operacije (npr.Button
). Šalje zahtjev komandi za operacijom. -
Receiver
- primatelja operacije odnosno objekt koji izvršava operaciju. -
Client
- kreiraConcreteCommand
objekt i postavlja njegovog primatelja operacije. U našem uvodnom primjeruReceiver
je ujedno bio iClient
.
Iterator
Problem: Želimo iterirati kroz agregaciju objekata (kao što je npr. lista) bez oslanjanja na unutarnju strukturu agregacije. K tome možemo htjeti iterirati na više različitih načina.
Rješenje problema je u tome da se iteriranje kroz agregaciju prepusti posebnom objektu — iteratoru.
Iterator i agregacije su vezani: ListIterator
opslužuje agregaciju List
. Neovisnost
klijenta o tipu iteratora postiže se pomoću polimorfnog iteratora:
Metode.
-
currentItem()
— vraća trenutni element u listi; -
first()
— inicijalizira trenutni element prvim elementom; -
next()
— prelazi na sljedeći element, odnosno inicijalizira trenutni element sljedećim; -
isDone()
— testira jesmo li došli iza zadnjeg elementa.
Iterator se koristi na sljedeći način:
Iterator it;
for(it.first(); !it.isDone(); it.next()){
auto current = it.current();
// ....
}
Struktura
Tko konstruira iterator? Svaka konkretna agregacijska klasa instancira svoj iterator pomoću metode createIterator()
(primjer Factory Method patterna).
Posljedice
-
Mogući su različiti načini iteriranja kroz agregaciju.
-
Iterator pojednostavljuje sučelje Agregacije.
-
Više iteratora mogu prolaziti istom Agregacijom.
Iterator i Composite
Implementacija iteratora je dosta izravna. Složenija je zadaće iterirati kroz Composite objekt.
Zadatak: Napraviti iterator koji iterira kroz Folder
objekt iz Composite
zadatka.
Uputa: Promjene nužne u klasama Folder
, File
i Component
.
-
Folder
neka pamti svoju djecu (sve svoje datoteke i poddirektorije) u oblikustd::list<Component*>
. -
Implementaciju klase
Folder
treba povećati za dvije metode:child_begin()
ichild_end()
koje vraćaju iteratore na listu djece. -
Bazna klasa
Component
neka implementira metodubool isFolder()
koja u baznoj klasi vraćafalse
, a prerađena u klasi Folder vraćatrue
. -
U sve klase dodati još metodu
std::string name()
koja vraća ime komponente. -
Klasa Folder dobiva metodu
createIterator()
koja dinamički alocira iterator i vraća pokazivač na njega.
Iterator i Composite
Iterator koristi list_iterator
kako bi iterirao kroz listu djece. Kada naiđe na folder
mora nastaviti iterirati unutar njega. Nakon završetka vraća se u polazni folder i nastavlja iteriranje.
Primjer redosljeda obilaska dan je na sljedećoj slici.
-
Za iteriranje po jednoj razini stabla dovoljna su dva iteratora
child_begin()
ichild_end()
koje nudi klasaFolder
. -
Pri prijelazu na nižu razinu treba sačuvati stanje iteratora (tj. dva list_iteratora korištena na toj razini) i iteratore inicijalizirati s novim vrijednostima na nižoj razini.
-
Pri povratku na višu razinu treba vratiti staro stanje iteratora.
-
Implementacija ove operacije: napraviti klasu
Memento
koja će čuvati dva iteratora i u konkretni iterator (ovdje nazvanFolderIterator
) dodatistd::stack<Memento>
. Pri prijelazu na niži nivo stanje se gurne na stog (push), a pri povratku na viši nivo stanje iteratora se ukloni sa stoga (pop).
Moguće rješenje
Napomena: items : std::list<Component*>
u klasi FolderIterator
nam u stvari ne treba. Dovoljni su nam
iteratori. Tu se nameće problem uniformnosti koda u početnom trenutku (na najvišem nivou) kada imamo samo pokazivač na
root Folder
. Taj pokazivač možemo "zamotati" u std::list<Component*>
i time dobivamo
inicijalne begin
i end
iteratore koji nam nedostaju. Druga moguća rješenja su da se, na primjer,
promijeni tip spremnika.
Observer
Problem: Različiti objekti mogu ovisiti o trenutnom stanju nekog objekta i stoga moraju biti obaviješteni o promjeni stanja tog objekta istog trena kad se ona dogodi.
Primjer: Numerički podaci mogu biti prikazani na različite načine: tabularno i na razne grafičke načine. Stoga će podaci biti spremljeni u jedan objekt (Subjekt) dok će ih razni drugi objekti (Promatrači) prikazivati (tabularno, grafički itd.). Svaki Promatrač treba odmah biti obaviješten o promjeni Subjekta kako bi prikazivao aktualne podatke.
Rješenje:
-
Svaki Promatrač se registrira kod Subjekta. Subjekt dozvoljava da se kod njega registrira proizvoljno velik broj promatrača.
-
Subjekt signalizira svakom Promatraču kada je došlo do promjene njegovog stanja u svojoj metodi
notify()
. -
Promatrač ima javnu metodu
update()
koji Subjekt može pozvati kada želi signalizirati promjenu Promatraču.
Kako bismo neovisno mogli varirati tip Subjekta i tip Promatrača formiramo stabla nasljeđivanja za obje vrste objekata.
Struktura
-
Konkretan promatrač kod konstrukcije uzima referencu (
subject
) na konkretan subjekt. KlasaObserver
je apstraktna i svaki konkretni Promatrač prerađuje metoduupdate()
. -
Konkretan Subjekt postavlja svoje stanje i nudi promatračima metodu za očitavanje stanja. Klasa
Subject
je implementacijska baza koja implementira obavještavanje promatrača. Ona ne mora poznavati konkretan tip promatrača i dohvaća samo sučeljeObserver
.
Sequence dijagram
-
Početno stanje subjekta može postaviti neki od promatrača ili sam subjekt.
-
Tko zove
notify()
? Sam subjekt (kao ovdje) ili promatrač (dodatna odgovornost). -
Najveći problem: česte male promjene subjekta koje vode na učestalo modificiranje promatrača.
Primjer
Napisat ćemo klasu Timer
koja će biti konkretni Subjekt i čuvat će trenutno vrijeme.
Konkretni Promatrači su satovi koji prikazuju vrijeme. Mi ćemo napisati samo NormalClock
klasu
koja ispisuje vrijeme na std::cout
.
Apstraktna baza Observer
class Subject;
/** Design pattern: Observer
* Apstraktna bazna klasa za konkretne Promatrače.
*/
class Observer{
public:
/** Svaki konkretni Promatrač mora implementirati update().
* Ovdje update() uzima pokazivač na Subjekt radi identifikacije
* Subjekta. Tako jedan Promatrač može pratiti više Subjekata.
*/
virtual void update(Subject*) = 0;
virtual ~Observer() {}
};
Implementacijska baza Subject
/** Design pattern: Observer.
* Bazna klasa za konkretne Subjekte.
*/
class Subject{
public:
/** Dodaj pokazivač na zainteresiranog Promatrača. */
virtual void attach(Observer * o);
/** Ukloni pokazivač na Promatrača. */
virtual void dettach(Observer * o);
/** Obavijesti sve Promatrače o promjeni Subjekta. */
virtual void notify();
virtual ~Subject() {}
protected:
/** Lista pokazivača na Promatrače zainteresirane za Subjekt.
*/
std::list<Observer*> m_observers;
protected:
typedef std::list<Observer*>::iterator iterator;
};
/** observer.cc */
#include "observer.hh"
#include <algorithm>
#include <iostream>
#include <ctime>
void Subject::attach(Observer* o){
// Nemoj logirati više puta isti Promatrač
iterator it = std::find(m_observers.begin(), m_observers.end(), o);
if(it != m_observers.end())
throw "Subject:: Observer already attached.";
m_observers.push_back(o);
}
void Subject::dettach(Observer* o){
// Eliminiraj promatrač ako je logiran.
iterator it = std::find(m_observers.begin(), m_observers.end(), o);
if(it == m_observers.end())
throw "Subject:: Observer is not attached.";
m_observers.erase(it);
}
void Subject::notify(){
// Zovi update() na svim promatračima
iterator it = m_observers.begin();
for( ; it != m_observers.end(); ++it)
(*it)->update(this);
}
Konkretan Subjekt
/** Konkretan Subjekt. Čuva informaciju o trenutnom vremenu.
*/
class Timer : public Subject{
public:
Timer() : m_sec(0), m_min(0), m_hour(0) {}
virtual int get_sec() const { return m_sec;}
virtual int get_min() const { return m_min;}
virtual int get_hour() const { return m_hour;}
/** Postavlja stanje Timer-a, tj. trenutno vrijeme. */
virtual void set_state();
private:
int m_sec;
int m_min;
int m_hour;
};
/** observer.cc */
void Timer::set_state(){
// Uzmi trenutno vrijeme (zaglavlje <ctime>). time_t je integralni tip
time_t now = std::time(0);
// Konvertiraj dobiveno vrijeme u strukturu tm.
tm * now_tm = std::localtime(&now); // konverzija
// Iz strukture tm pročitaj sekunde, minute i sate.
m_sec = now_tm->tm_sec;
m_min = now_tm->tm_min;
m_hour= now_tm->tm_hour;
// Obavijesti sve Promatrače -- stanje Subjekta je promijenjeno.
notify();
}
Konkretan Promatrač
/** Konkretan Promatrač.
*/
class NormalClock : public Observer{
public:
NormalClock(Timer * pc) : m_pclock(pc) {m_pclock->attach(this); }
/** Prerađeni update(). Identificira Subjekt i ignorira sve osim
onog kod kojeg je logiran.
*/
virtual void update(Subject* ps){ if(m_pclock == ps) draw(); }
/** Iscrtavanje sata. */
virtual void draw() const;
private:
/** Pokazivač na konkretni Subjekt. */
Timer * m_pclock;
};
/** observer.cc */
void NormalClock::draw() const{
// "Iscrtavanje" sata.
std::cout << m_pclock->get_hour()<<":"
<< m_pclock->get_min()<<":"
<< m_pclock->get_sec()<< std::endl;
}
Glavni program
#include <iostream>
#include <chrono>
#include <thread>
#include "observer.hh"
int main() {
// Instanciramo konkretni subjekt
Timer sys_clock;
// Instanciramo konkretni promatrač
// kojeg promatra.
NormalClock normal(&sys_clock);
int delay = 1000; // ms
// Kod se dešava u beskonačnoj petlji. Izlazimo s Crtl-C.
while(true){
// Postavi stanje konkretnog subjekta
// sys_clock će obavijestiti sve svoje promatrače.
sys_clock.set_state();
// odspavaj delay milisekundi. (zaglavlja <thread> i <chrono>)
std::this_thread::sleep_for(std::chrono::milliseconds(delay));
}
return 0;
}
Varijante
-
Promatrač može biti taj koji ima odgovornost pozvati
notify()
. -
Promatrač može promatrati više subjekata.
-
Nekad se koriste složenije strategije pozivanja funkcije
notify()
, pogotovo kod subjekata koji se brzo mijenjaju, s ciljem da se smanji količina promjena kod promatrača. Na promatraču se neće zvatiupdate()
dok se ne nakupi dovoljan broj izmjena subjekta. U tom slučaju subjekt može delegirati pozivanje metodenotify()
te registraciju i deregistraciju promatrača nekom objektu (tzv. ChangeManager) koji je specijaliziran za tu funkciju. Time dobivamo mogućnost variranja ChangeManagera. -
Ako subjekt ima više razloga za promjenu onda se promatrač može pretplatiti na samo neki od njih. Pri tome pri registraciji mora subjektu dati tu dodatnu informaciju.
Visitor
Problem: Potrebno je dodati operaciju nizu objekata, općenito različitih tipova, bez mijenjanja klasa koje predstavljaju tipove tih objekata.
Primjer: Uzmimo da imamo trgovinu s dijelovima (računala) od kojih su neki složeni (sadrže druge dijelove). Svaki dio je reprezentiran u programu svojom klasom koja sadrži cijenu komada. Klase pojedinih dijelova ne moraju nužno biti dio iste hijerarhije. Na primjer:
Trgovina tokom ljeta može imati politiku ljetnog sniženja cijene - svaki dio dobiva neki popust (discount). U tom slučaju bi sistem klasa trebalo nadograditi na ovaj način:
Zbog velike konkurencije možda je potrebno dodati još koje sezonsko sniženje!?
Problem: Kako nizu klasa dodati novu metodu bez izmjena u samim klasama?
Rješenje
Grupirat ćemo sve srodne operacije u jednu klasu. Na primjer, sve metode za ljetno sniženje cijena ulaze u klasu
SummerDiscountPriceVisitor
koja implementira apstraktnu klasu (sučelje) Visitor
.
Sve komponente dobivaju metodu prihvaćanje visitora (accept) i time implementiraju sučelje Visitable
.
Kako funkcionira Visitor?
U programu (klijentu) imamo kolekciju komponenti na kojima želimo izvršiti danu operaciju — na primjer izračunati ukupnu cijenu svih komponenti s ljetnim sniženjem.
std::vector<Visitable*> equipment;
Visitable * pk = new Keyboard(80.99);
Visitable * pm = new Mouse(120.0);
// ...
equipment.push_back(pk);
equipment.push_back(pm);
// ...
Instanciramo konkretni visitor koji implementira željenu operaciju (na svim komponentama):
SummerDiscountPriceVisitor visitor;
Da bismo izvršili operaciju na svakom elementu kolekcije zovemo accept
metodu na svakom elementu:
for(Visitable * pv : equipment)
pv->accept(&visitor);
-
pv->accept(&visitor)
poziva metoduaccept
prema dinamičkom tipu pokazivačapv
— na primjer, na drugom elementu zoveMouse::accept
. -
Mouse::accept(&visitor)
pozivavisitor->visitMouse(this)
. Pri ovom se pozivu selektira metodavisitMouse()
prema dinamičkom tipu pokazivačavisitor
. U ovom slučaju pozivaju se metode klaseSummerDiscountPriceVisitor
. -
Metoda
SummerDiscountPriceVisitor::visitMouse()
koja je ovime pozvana dobiva pokazivač naMouse
objekt na kome može izvršiti operaciju s time da rezultat operacije ostaje uSummerDiscountPriceVisitor
objektu (metoda je tipa void).
Struktura
Implementacija Komponente
Komponente: samo Keyboard
i Mouse
.
class Visitor;
struct Visitable{
virtual void accept(Visitor *) = 0;
virtual ~Visitable() {}
};
class Keyboard : public Visitable {
double m_price;
public:
Keyboard(double price) : m_price(price) {}
double get_price() const { return m_price; }
virtual void accept(Visitor *v);
};
class Mouse : public Visitable {
double m_price;
public:
Mouse(double price) : m_price(price) {}
double get_price() const { return m_price; }
virtual void accept(Visitor *v);
};
// Implementacija accept metoda
void Keyboard::accept(Visitor *v) { v->visiteKeyboard(this); }
void Mouse::accept(Visitor *v) { v->visiteMouse(this); }
Implementacija visitora
Implementacija visitora: samo SummerDiscountPriceVisitor
. Visitor sumira cijenu pojedinih
komponenti (sa popustima) i vraća rezultat u metodi get_total_price()
.
struct Visitor{
virtual void visiteKeyboard(Keyboard *) = 0;
virtual void visiteMouse(Mouse *) = 0;
};
// Konkretan Visitor
class SummerDiscountPriceVisitor : public Visitor{
public:
SummerDiscountPriceVisitor() : m_sum(0.0) {}
virtual void visiteKeyboard(Keyboard *);
virtual void visiteMouse(Mouse *);
double get_total_price() const { return m_sum; }
private:
double m_sum;
const double m_discount_keyboard = 0.1; // 10 %
const double m_discount_mouse = 0.05; // 5 %
};
void SummerDiscountPriceVisitor::visiteKeyboard(Keyboard *p_keyboard){
double price = p_keyboard->get_price();
double discount_price = price * (1 - m_discount_keyboard);
m_sum += discount_price;
}
void SummerDiscountPriceVisitor::visiteMouse(Mouse *p_mouse){
double price = p_mouse->get_price();
double discount_price = price * (1 - m_discount_mouse);
m_sum += discount_price;
}
Klijent
Client
je glavni program. Agregacija (equipment
) je dana kao lista.
int main() {
std::vector<Visitable*> equipment;
Visitable * pk = new Keyboard(80.99);
Visitable * pm = new Mouse(120.0);
equipment.push_back(pk);
equipment.push_back(pm);
SummerDiscountPriceVisitor visitor;
for(Visitable * pv : equipment)
pv->accept(&visitor);
std::cout << "Total : " << visitor.get_total_price() << std::endl;
delete pk;
delete pm;
return 0;
}
Svojstva predloška Visitor
-
Visitor okuplja usko vezane operacije i razdvaja (u zasebne klase) nepovezane operacije. Strukture vezane za algoritme skrivene su u visitoru.
-
Dodavanje nove operacije je jednostavno — treba samo napisati novu Visitor klasu.
-
Dodavanje nove komponente je složeno jer tada treba mijenjati sve visitor klase. Sustav klasa komponenti stoga mora biti stabilan.
-
Visitor se oslanja na javno sučelje komponenti za implementaciju nove operacije.
-
Visitor često u sebi akumulira neko stanje (zbirna cijena komponenti).
Double dispatch
Programiramo računalnu igru u kojoj imamo sljedeće objekte:
Želimo implementirati funkciju
void checkForCollision(GameObject *o1, GameObject *o2){
// Detekcija kolizije ovisi o stvarnom obliku objekata te stoga
// naša funkcija mora biti "virtualna" po dva argumenta.
}
Tu nam treba virualna ovisnost o dva argumenta (= double dispatch) — što C++ jezik ne osigurava.
C++ osigurava single dispatch kroz mehanizam virtualnih funkcija. U sve klase bismo uveli virtualnu funkciju članicu
checkForCollision(GameObject *o)
:
GameObject *po1 = new SpaceShip();
GameObject *po2 = new Asteroid();
po1->checkForCollision(po2); // po1 poziva funkciju iz SpaceShip klase, ali
// po2 "ostaje" GameObject*.
1. Rješenje
Korištenjem RTTI sustava za rekonstrukciju izgubljenog tipa po2
objekta.
void SpaceShip::checkForCollision(GameObject* obj)
{
const type_info & objectType = typeid(*obj);
if(objectType == typeid(SpaceShip)){
// SpaceShip - SpaceShip kolizija
}
else if(objectType == typeid(SpaceStation)){
// SpaceShip - SpaceStation kolizija
}
else if(objectType == typeid(Asteroid)){
// SpaceShip - Asteroid kolizija
}
else
throw std::runtime_error("Collision with unknown object");
}
2. Rješenje.
Korištenjem virtualnih funkcija. Potrebna su nam dva virtualna poziva da bismo realizirali double dispatch. U baznu klasu stavljamo četiri preopterećene čiste virtualne funkcije koje prerađujemo u konkretnim klasama:
-
Funkcije tipa
SpaceShip::checkForCollision(SpaceStation*)
imaju informacije o tipovima i mogu predvidjeti hoće li se kolizija desiti ili ne. -
Funkcije tipa
SpaceShip::checkForCollision(GameObject*)
definiramo na sljedeći način:
void SpaceShip::checkForCollision(GameObject* obj){
obj->checkForCollision(this);
}
Kako taj kod funkcionira?
U pozivu:
GameObject *po1 = new SpaceShip();
GameObject *po2 = new Asteroid();
po1->checkForCollision(po2);
-
Koriste se mehanizam virtualnih funkcija da se pozove
SpaceShip::checkForCollision(po2)
-
SpaceShip::checkForCollision(po2)
zove funkcijupo2->checkForCollision(this)
, gdje se drugi puta koristi mehanizam virtualnih funkcija da bi se pozvala funkcijaAsteroid::checkForCollision(SpaceShip*)
.
Zadnja pozvana funkcija ima informacije o tipovima koje joj trebaju.
Nedostatak: Sve klase koje implementiraju GameObject
moraju znati za sve druge implementacijske klase.
Detaljnije vidi: Scott Meyers: More Effective C++, Addison-Wesley 1996.
Visitor oblikovni obrazac implementira double dispatch kako bi postigao svoj cilj.