2012-05-23 28 views
7

Mam implementację stanu State Pattern, gdzie każdy stan obsługuje zdarzenia, które otrzymuje z kolejki zdarzeń. W związku z tym klasa State ma czystą wirtualną metodę void handleEvent(const Event*). Zdarzenia dziedziczą klasę podstawową Event, ale każde zdarzenie zawiera dane, które mogą być innego typu (np. Int, string ... lub cokolwiek innego). handleEvent musi określić typ środowiska wykonanego zdarzenia, a następnie wykonać downcast w celu wyodrębnienia danych zdarzenia. Zdarzenia są tworzone dynamicznie i przechowywane w kolejce (w ten sposób odbywa się upcasting).Jak uniknąć downcast?

Wiem, że downcasting jest oznaką złego projektu, ale jest możliwe uniknięcie go w tym przypadku? Mam na myśli wzór użytkownika, w którym stan klasy podstawowej zawierałby wirtualne procedury obsługi dla każdego zdarzenia, ale następnie ponownie należy przeprowadzić downcast w kawałku kodu, który zapisuje zdarzenie z kolejki i przekazuje go do bieżącego stanu. (Przynajmniej w tym przypadku duży switch(eventID) będzie tylko w jednym miejscu ...). Czy wizytówka jest najlepszym sposobem (najlepsza praktyka), aby uniknąć downcastingu?

Oto pseudo-kod (ja przechodząc boost::shared_ptr w tym przykładzie, ale rzutowanie w dół dzieje się tak):

enum EventID 
{ 
    EVENT_1, 
    EVENT_2, 
    ... 
}; 

class Event 
{ 
    EventID id; 
public: 
    Event(EventID id):id(id){} 
    EventID id() const {return id;} 
    virtual ~Event() = 0; 
}; 

class Event1 : public Event 
{ 
    int n; 
public: 
    Event1(int n):Event(EVENT_1), n(n){} 
    int getN() const {return n;} 
}; 

class Event2 : public Event 
{ 
    std::string s; 
public: 
    Event2(std::string s):Event(EVENT_2), s(s){} 
    std::string getS() const {return s;} 
}; 

typedef boost::shared_ptr<Event> EventPtr; 

class State 
{ 
    ... 
public: 
    ... 
    virtual ~State() = 0; 
    virtual void handleEvent(const EventPtr& pEvent) = 0; 
}; 

class StateA : public State 
{ 
    ... 
public: 
    void handleEvent(const EventPtr& pEvent) 
    { 
     switch(pEvent->id()) 
     { 
     case EVENT_1:   
      int n = boost::static_pointer_cast<Event1>(pEvent)->getN(); 
      ... 
      break; 
     case EVENT_2: 
      std::string s = boost::static_pointer_cast<Event2>(pEvent)->getS(); 
      ... 
      break; 
     ... 

     } 
    } 
} 

Odpowiedz

6

Typowa odwiedzający wykonuje nie przygnębiony, dzięki strategii dwukrotnie wysyłkowy:

// Visitor.hpp 
class EventBar; 
class EventFoo; 

class Visitor { 
public: 
    virtual void handle(EventBar const&) = 0; 
    virtual void handle(EventFoo const&) = 0; 
}; 

// Event.hpp 
class Visitor; 

class Event { 
public: 
    virtual void accept(Visitor&) const = 0; 
}; 

I implementacje:

// EventBar.hpp 
#include <Event.hpp> 

class EventBar: public Event { 
public: 
    virtual void accept(Visitor& v); 
}; 

// EventBar.cpp 
#include <EventBar.hpp> 
#include <Visitor.hpp> 

void EventBar::accept(Visitor& v) { 
    v.handle(*this); 
} 

Kluczową kwestią jest to, że w v.handle(*this) typ statyczny *this to EventBar const&, który wybiera prawidłowe przeciążenie w Visitor.

+1

Opuszczenie wywoływane jest w wywołaniu '' Event :: accept''. Jest rozwiązany poprzez vtable do '' EventBar :: accept'' i '' this'' jest rzutowany z '' Event'' na '' EventBar'' w procesie. –

+0

Więc nie ma innych magicznych wzorów/idiomów, aby uniknąć downcastingu? Moją jedyną troską o odwiedzających jest ilość powtarzającego się kodu, który należy zapisać. Wydaje się jednak, że to cena braku zniżek. Nie miałbym tego problemu, gdybym miał tylko jedną klasę Event i nie trzeba przechowywać pochodnych klas zdarzeń w kolejce, ale jest to nieuniknione. –

+0

@BojanKomazec: co najmniej jednym innym sposobem jest użycie 'boost :: variant':' typedef boost :: variant Event; '. Usuwając hierarchię usuwasz downcasty :) –

2

Ideą zdarzeń jest przekazywanie szczegółowych obiektów poprzez uogólniony (i agnostyczny) interfejs. Spuszczenie jest nieuniknione i stanowi część projektu. Zły lub dobry, jest dyskusyjny.

Wzór użytkownika ukrywa tylko odlew. Wciąż jest wykonywany za kulisami, typy rozwiązywane za pomocą adresu metody wirtualnej.

Ponieważ Twój Event ma już identyfikator, nie jest całkowicie agnostyczny dla typu, więc odlewanie jest całkowicie bezpieczne. Tutaj oglądasz typ osobiście, we wzorcu odwiedzającego kompilator się tym zajmuje.

"Cokolwiek pójdzie w górę, musi spaść".

+0

Nie zgadzam się z użyciem * bezpiecznego *. Może działać, ale jest bardzo podatny na błędy, ponieważ jest ręczny. Korzystanie z mechanizmów językowych gwarantuje, że a) nie wystąpi błędne przypisanie downcastu i b) wszystkie typy "docelowe" będą obsługiwane poprawnie (włączanie ID, w którym można zapomnieć o nowym ID ...) –

+0

@MatthieuM. Im mniej musisz zrobić ręcznie, tym mniej miejsca na błędy, zgadzam się. Jednak kwestionowanie kompetencji programisty jest prostą drogą do argumentu "Jesteś głupi, by pisać w C++". C i C++ są przeznaczone dla osób, które wiedzą, co robią. Takie jest moje założenie. Wykrywanie cukru syntaktycznego z funkcjonalnością jest zbyt częste. –

+0

* Im mniej musisz zrobić ręcznie, tym mniej miejsca na błędy * - to moje credo :) Wolę taki projekt. A w przypadku Gościa nie ma potrzeby "identyfikatora zdarzenia". –