Bazirano na prezentaciji prof. Mladena Juraka, 2019.
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 dizajn treba biti otvorena za proširenja i zatvorena za izmjene.
To znači da kod treba dozvoliti proširenje svog ponašanja bez izmjene svog postojećeg koda koji radi. 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 (DRY) 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.
Kada se u bazni tip unese podtip, mora se ponašati kako se očekuje za bazni tip.
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 objašnjava nam 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.
Copy construktor radi sličnu stvar, kopira postojeći objekt. Međutim postoji problem sliceinga.
Neka klasa Dog nasljeđuje klasu Animal.
Dog d;
Animal a = d;
Dolazi do sliceinga: gubi se Dog dio objekta.
Ispravno kopiranje koristeći Prototype obazac:
Dog* d = new Dog();
Animal* a = d->clone();
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.
Klijent uzet će referencu na 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.
// 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;
}
};
// 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 Klijent{
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.
Klijent app;
FormaterCreator * f = new FormaterACreator;
app.work(f);
delete f;
f = new FormaterBCreator;
app.work(f);
delete f;
}
Product podklase treba samo dodati odgovarajuću Creator podklasu
i Klijent može raditi s novom klasom 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 može se 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;
}
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 složeni objekt tretirati istim sučeljem kao pojedinačni objekt.
Primjer: Folder na disku može sadržavati datoteke i druge foldere. Neke fukcionalnsti datoteke želimo da ima i folder, koji je skup 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.
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);
}
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 AbstractShellSort{
public:
void sort(int v[], int n) const; // nevirtualna metoda
virtual ~AbstractShellSort(){}
protected:
virtual bool doSwap(int, int) const = 0 ;
};
sort() faktoriziran je varijabilini dio:
void AbstractShellSort::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 AbstractShellSort{
protected:
virtual bool doSwap(int a, int b) const { return a > b; }
};
class SortDown : public AbstractShellSort{
protected:
virtual bool doSwap(int a, int b) const { return a < b; }
};
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.
class AbstractSort{
public:
virtual void sort(int v[], int n) const;
virtual ~AbstractSort(){}
};
Apstraktna klasa AbstractSort može biti bazna klasa raznim algoritmima za sortiranje, kao što su shellSort, boobleSort, mergeSort, quickSort itd.
Command pretvara operaciju u objekt. To omogućuje apstraktno bavljenje operacijama bez znanja što one konkretno rade. Moguće je i pamćenje operacija da se obave kasnije, undo, itd.
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) parametriziran je
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
Zadatak: Napraviti iterator koji iterira kroz Folder objekt iz Composite zadatka. Potrebno je iterirati kroz foldere, a kada se dođe do foldera i kroz sve u njemu.
Uputa: U klasu Folder dodajte dvije metode: child_begin() i child_end() koje vraćaju
iteratore na listu djece.
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.cpp */
#include "observer.h"
#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.cpp */
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.cpp */
void NormalClock::draw() const{
// "Iscrtavanje" sata.
std::cout << m_pclock->get_hour()<<":"
<< m_pclock->get_min()<<":"
<< m_pclock->get_sec()<< std::endl;
}
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;
}
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 dogoditi.
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.