Objektno programiranje je skup tehnika koje imaju za cilj pisanje kvalitetnog softvera.
Od 1 do 3 su kriteriji korisnika, a 4 i 5 su kriteriji programera.
Kako odrediti klase u aplikaciji i njihove funkcije članice? Kakva će biti interakcija među objektima?
U scenarijima korištenja aplikacije treba potražiti imenice i glagole.
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.
(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.
(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.
(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.
Treba identificirati dio programa koji je podložan promjeni i izoliraj ga od stabilnijeg koda kreiranjem nove apstrakcije (klase).
(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.
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.
Nasljeđivanje (specijalizacija) je statički odnos među klasama i manje je fleksibilan od delegacije, kompozicije ili agregacije.
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
Abstract Factory, Builder, Factory Method, Object Pool, Prototype, Singleton
Ovaj obrazac koristimo kada želimo kreirati objekt polazeći od danog prototipa.
clone()
.
clone()
koja
kreira objekt tipa klase.
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.
Product
je sučelje za niz tipova koje koristimo samo kroz sučelje. Konkretni tipovi
su ProductA
, ProductB
itd.
Creator
je sučelje za klase koje instanciraju Product
. Svakom konkretnom
produktu odgovara konkretan Creator
koji instancira Product
; na primjer,
CreatorA
instancira ProductA
itd.
Creator
pomoću koje će kreirati Product
.
Na taj način može biti napisan posve neovisno o stvarnom tipu Product
-a, što je cilj
obrasca.
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;
}
};
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; }
};
// 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;
}
};
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;
}
Product
objekta treba samo dodati odgovarajuću Creator
podklasu
i kod klijent može raditi s novim objektom bez izmjena.
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.
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.
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.
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;
}
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.
Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy
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.
Target
i koristi sučelje klase Target
;
Adaptee
, ali ona nema traženo sučelje.
Adapter
koja:
Target
i stoga ima sučelje klase Target
;
Adaptee
koju koristi za implementaciju svog sučelja.
Umjesto privatnog nasljeđivanja možemo uvijek koristiti agregaciju:
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.
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.
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.
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.
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);
}
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
Chain of responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template method, Visitor
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.
Iskorištavanje postojećeg koda u baznoj klasi.
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.
// apstraktna baza
class AbstractSort{
public:
void sort(int v[], int n) const; // nevirtualna metoda
virtual ~AbstractSort(){}
protected:
virtual bool doSwap(int, int) const = 0 ;
};
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]);
}
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; }
};
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;
}
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
Definiraj familiju algoritama istog sučelja kako bi bili zamjenjivi. Učini klijenta neovisnim o izabranom algoritmu.
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.
if
naredbi u kojima bi se birao algoritam.
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.
Button
dajući mu referencu/pokazivač na
odgovarajuću naredbu (command
).
Button
na klik miša reagira pozivom metodi clicked()
koja na
referenci command
poziva metodu execute()
.
Command
objekt pri konstrukciji dobiva referencu na aplikaciju
(koja ga je kreirala) i kroz nju može izvršiti potrebnu operaciju (na primjer
pozvati metodu showCopyrightInfo()
).
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
- apstraktna bazna klasa za konkretne operacije. Inicijator operacije
ima samo referencu tipa Command
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
- kreira ConcreteCommand
objekt i
postavlja njegovog primatelja operacije. U našem uvodnom primjeru Receiver
je ujedno bio i
Client
.
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();
// ....
}
Tko konstruira iterator? Svaka konkretna agregacijska klasa instancira svoj iterator pomoću metode createIterator()
(primjer Factory Method patterna).
Posljedice
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 obliku std::list<Component*>
.
Folder
treba povećati za dvije metode: child_begin()
i child_end()
koje vraćaju
iteratore na listu djece.
Component
neka implementira metodu bool isFolder()
koja u baznoj klasi vraća
false
, a prerađena u klasi Folder vraća true
.
std::string name()
koja vraća ime komponente.
createIterator()
koja dinamički alocira iterator i vraća pokazivač na njega.
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.
child_begin()
i child_end()
koje nudi klasa Folder
.
Memento
koja će čuvati dva iteratora i u konkretni iterator
(ovdje nazvan FolderIterator
)
dodati std::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).
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.
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:
notify()
.
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.
subject
) na konkretan subjekt. Klasa Observer
je apstraktna
i svaki konkretni Promatrač prerađuje metodu update()
.
Subject
je
implementacijska baza koja implementira obavještavanje promatrača. Ona ne mora poznavati konkretan tip
promatrača i dohvaća samo sučelje Observer
.
notify()
? Sam subjekt (kao ovdje) ili promatrač (dodatna odgovornost).
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
.
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() {}
};
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. Č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č.
*/
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;
}
#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;
}
notify()
.
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 zvati update()
dok se ne nakupi dovoljan broj izmjena subjekta. U tom slučaju subjekt može delegirati pozivanje metode
notify()
te registraciju i deregistraciju promatrača nekom objektu (tzv. ChangeManager)
koji je specijaliziran za tu funkciju. Time dobivamo mogućnost variranja ChangeManagera.
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?
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
.
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 metodu accept
prema dinamičkom tipu pokazivača pv
— na primjer,
na drugom elementu zove Mouse::accept
.
Mouse::accept(&visitor)
poziva visitor->visitMouse(this)
. Pri ovom se pozivu selektira metoda
visitMouse()
prema dinamičkom tipu pokazivača visitor
. U ovom slučaju pozivaju se metode klase
SummerDiscountPriceVisitor
.
SummerDiscountPriceVisitor::visitMouse()
koja je ovime pozvana dobiva pokazivač na Mouse
objekt
na kome može izvršiti operaciju s time da rezultat operacije ostaje u SummerDiscountPriceVisitor
objektu
(metoda je tipa void).
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: 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;
}
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;
}
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*.
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");
}
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:
SpaceShip::checkForCollision(SpaceStation*)
imaju informacije o tipovima i mogu predvidjeti
hoće li se kolizija desiti ili ne.
SpaceShip::checkForCollision(GameObject*)
definiramo na sljedeći način:
void SpaceShip::checkForCollision(GameObject* obj){
obj->checkForCollision(this);
}
U pozivu:
GameObject *po1 = new SpaceShip();
GameObject *po2 = new Asteroid();
po1->checkForCollision(po2);
SpaceShip::checkForCollision(po2)
SpaceShip::checkForCollision(po2)
zove funkciju po2->checkForCollision(this)
,
gdje se drugi puta koristi mehanizam virtualnih funkcija da bi se pozvala funkcija
Asteroid::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.