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

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

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

Ovaj obrazac koristimo kada želimo kreirati objekt polazeći od danog prototipa.

Struktura

Prototype.png

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

factoryMethod.png

Učesnici

  • 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.
  • Kod klijent će uzeti 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.

Primjer:

// 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 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;
}

Posljedice i varijante

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

singleton.png

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

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

adapter.png

Učesnici

  • Klijent ima referencu na Target i koristi sučelje klase Target;
  • 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 klase Target;
    • privatno nasljeđuje klasu Adaptee koju koristi za implementaciju svog sučelja.

Varijanta

Umjesto privatnog nasljeđivanja možemo uvijek koristiti agregaciju:

adapter-1.png

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.

adapter-2.png

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

composite.png

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:

composite-obj.png

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);
}
composite-1.png

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

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

templateMeth.png

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

Strategy.png

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.

command-example.png

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

command-struct.png

Učesnici

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

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-1.png

Iterator i agregacije su vezani: ListIterator opslužuje agregaciju List. Neovisnost klijenta o tipu iteratora postiže se pomoću polimorfnog iteratora:

iterator-2.png

Metode.

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

iterator-3.png

Posljedice

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.

composite-folderIter.png

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.

stablo.png

Moguće rješenje

iterator-4.png

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:

Kako bismo neovisno mogli varirati tip Subjekta i tip Promatrača formiramo stabla nasljeđivanja za obje vrste objekata.

Struktura

observer.png

Sequence dijagram

observer-sequence.png

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

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:

visitor-ex1-0.png

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:

visitor-ex1-1.png

Zbog velike konkurencije možda je potrebno dodati još koje sezonsko sniženje!?

visitor-ex1-2.png

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.

visitor-ex1-3.png

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);

Struktura

visitor-ex1-4.png

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

Double dispatch

Programiramo računalnu igru u kojoj imamo sljedeće objekte:

gameObjects.png

Ž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):

gameObjects-1.png
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:

gameObjects-2.png
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);

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.