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.
Observerclass 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.