Autor Wątek: Zwracanie stanu z funkcji  (Przeczytany 2408 razy)

Offline ison

  • Użytkownik

# Styczeń 09, 2013, 20:53:19
Witam,
pytanie dotyczy projektowania kodu.

Załóżmy sytuację gdzie mamy obsługę frakcji dla graczy w grze.
Jak obsługujecie coś w stylu 'pseudo wyjątków' w takim przypadku - tzn. metoda odpowiedzialna za dołączenie gracza do frakcji może się nie udać - gracz może być we frakcji, frakcja może być pełna, itd. jak wtedy przekazać 'wyżej' wiadomość o tym?

1) funkcja zwraca jakąś wartość - wtedy dla wielu funkcji/metod musiałbym mieć mnóstwo stałych określających co zwróciła funkcja. Lepszym pomysłem byłby wtedy chyba enum, tylko, że wtedy miałbym całą klasę zawaloną różnymi typami zwracanych wartości, co więcej enumy jako takie zasyfiają scope'a, więc warto by je ująć w namespace'y. Wtedy to już jest totalna masakra.
class Faction
{
    public:
        namespace AddPlayerStatus
        {
            enum AddPlayerStatus
            {
                  OK,
                  FactionFull,
                  itd
            }
        }
        AddPlayerStatus::AddPlayerStatus addPlayer(Player *player);
};
i tak dla każdej metody

2) do funkcji przekazany zostaje string przez referencję do którego zapisywany jest ewentualny komunikat o niepowodzeniu. Brzydkie i niepraktyczne.

3) true, false + getLastError()? Też nie jest to najlepsze rozwiązanie, zwłaszcza, że to error jako taki nie jest.

samo true, false odpada

Offline Mr. Spam

  • Miłośnik przetworów mięsnych

Offline Avaj

  • Użytkownik

# Styczeń 09, 2013, 20:57:40
w wyższej warstwie możesz sprawdzać czy będzie ok:

if (!player->hasFaction())
{
  if (!targetFaction->isFull())
  {
    player->joinFaction(targetFaction);
  }
  else
  {
    message("Target faction is full");
  }
}
else
{
  message("Player already has faction");
}

Offline Kos

  • Użytkownik
    • kos.gd

# Styczeń 09, 2013, 21:01:24
Jeżeli funkcja ma logicznie X możliwych rezultatów, to gdzieś w kodzie musisz mieć wypisane je wszystkie. Mała różnica, czy wypiszesz je i zwracasz jako stringi, czy jako enumy, czy jako jeszcze coś.

Enum jest najprostszy, czemu się przed nim bronisz?

Offline Xirdus

  • Redaktor

# Styczeń 09, 2013, 21:03:32
Załóżmy sytuację gdzie mamy obsługę frakcji dla graczy w grze.
Jak obsługujecie coś w stylu 'pseudo wyjątków' w takim przypadku - tzn. metoda odpowiedzialna za dołączenie gracza do frakcji może się nie udać - gracz może być we frakcji, frakcja może być pełna, itd. jak wtedy przekazać 'wyżej' wiadomość o tym?

Ja bym zrobił tak:

if (faction.find (player) != null) // albo player.factions.find (faction)
    // wywal blad ze gracz jest juz we frakcji
else if (faction.cap == faction.count)
    // wywal blad ze frakcja pelna
else
    faction.add (player);

Czyli wszystkie warunki sprawdzasz wcześniej, na własną rękę.

Edit: oj, Avaj mnie ubiegł.

Edit 2: to rozwiązanie ma też tę zaletę, że możesz stosować różne zestawy warunków na różne okazje.
« Ostatnia zmiana: Styczeń 09, 2013, 21:06:31 wysłana przez Xirdus »

Offline ison

  • Użytkownik

# Styczeń 09, 2013, 21:24:37
Cytuj
Enum jest najprostszy, czemu się przed nim bronisz?
Enumy silnie typowane z C++0x jeszcze ok, ale te zwykłe trochę zaciemniają kod, trzeba tworzyć dodatkowe namespace'y, nie wiadomo jak je nazwać, żeby zadeklarować zmienną typu enum trzeba pisać ABC::ABC - aczkolwiek aby tego uniknąć można zawsze zrobić typedef ABC ABC::ABC, ale to jeszcze bardziej zaciemnia imo kod.
Poza tym tych enumów wyjdzie mnóstwo w każdej z klas, i teraz nie wiadomo jak je nazwać:
dla metod addPlayer, removePlayer by musiały być AddPlayerStatus, RemovePlayerStatus.

Po głębszym zastanowieniu to sposoby Avaja i Xirdusa chyba są najlepsze. Tzn osobno metody dla sprawdzania czy gracz może się dołączyć i osobno metoda, która powoduje dołączenie - aczkolwiek ma to jedną wadę, aby zapobiec zniszczeniu całej struktury frakcji tak czy inaczej w addPlayer musiałbym sprawdzać te warunki. W efekcie warunki zostaną sprawdzone 2 razy, raz w miejscu gdzie muszę dać stosowny komunikat, a drugi w samym addPlayer. Zakładanie, że addPlayer zawsze będzie wywołane w dobrych warunkach odpada.

Chyba, że ktoś podrzuci jakiś ciekawy wzorzec projektowy, na którym robi się tego typu rzeczy, chociaż nie wiem czy takowy istnieje.

Drążę tak ten temat, bo czeka mnie refaktoryzacja sporej ilości linii kodu, a nie będzie mi się już chciało go 3 raz refaktoryzować.

Offline Kos

  • Użytkownik
    • kos.gd

# Styczeń 09, 2013, 21:31:48
A daj spokój z wzorcami projektowymi. :)

To co Ci zaproponowali Avaj i Xirdus to nie tyle sposób na implementację tego, co chciałeś, a coś więcej, czyli mały redesign - czyli przerzutka części odpowiedzialności z klasy Faction w inne miejsce (która też przy okazji rozwiązuje Twój techniczny problem doboru sygnatury funkcji). Jeśli ten redesign Ci odpowiada, to masz problem z głowy.

Offline Xirdus

  • Redaktor

# Styczeń 09, 2013, 21:38:42
aby zapobiec zniszczeniu całej struktury frakcji tak czy inaczej w addPlayer musiałbym sprawdzać te warunki
Pokaż mi scenariusz, gdzie to się opłaci. Jak warunki są spełnione bezpośrednio przed wywołaniem funkcji, to siłą rzeczy będą i w jej środku. To nie PHP, gdzie na każdym kroku masz potencjalny SQL Injection.

Edit: sory, nie doczytałem reszty posta:

Zakładanie, że addPlayer zawsze będzie wywołane w dobrych warunkach odpada.
To masz błąd projektowy jakiś, jeśli if (czyMożnaDodaćPlayera) dodajPlayera() może dodać playera kiedy nie można.
« Ostatnia zmiana: Styczeń 09, 2013, 21:41:17 wysłana przez Xirdus »

Offline Kurak

  • Użytkownik

  • +1
# Styczeń 09, 2013, 21:43:42
Enumy silnie typowane z C++0x jeszcze ok, ale te zwykłe trochę zaciemniają kod, trzeba tworzyć dodatkowe namespace'y, nie wiadomo jak je nazwać, żeby zadeklarować zmienną typu enum trzeba pisać ABC::ABC - aczkolwiek aby tego uniknąć można zawsze zrobić typedef ABC ABC::ABC, ale to jeszcze bardziej zaciemnia imo kod.
namespace NazwaEnuma
{
    enum TYPE
    {
        Dupa = 0,
        Cycki,
        //...
        _COUNT,
        _FIRST = Dupa
    };

    string ToString(TYPE val);
    TYPE FromString(const string& val);

    // Miejsce na jakieś dodatkowe funkcje zwracające informacje o wartości w rodzaju IsFirearm(TYPE arg)
}

Jedno z możliwych rozwiązań - z paroma prostymi makrami od razu załatwia kwestię konwersji z i do stringa, iteracji po enumach, dodawania enumów jako stałych do parsera skryptów, wyliczania ile bitów trzeba by zapisać wszystkie wartości enuma itp. Do NazwaEnuma::TYPE można się przyzwyczaić, a enum class z c++11 chyba nie pozwala na zamknięcie funkcji w tej samej przestrzeni nazw.

Offline Dab

  • Redaktor
    • blog

# Styczeń 09, 2013, 21:47:41
Po co każda funkcja ma własne enumy? 99% bibliotek ma maksymalnie -dziesiąt różnego rodzaju kodów. Dodajesz flagę debug która zwraca elegancko sformatowane błędy ze szczegółowym opisem i jest po bólu. Tylko nie ma co przesadzać w drugą stronę, OpenGL ma pięć flag i nie jest OK :D

Ja ostatnio stosuję dużo funkcji typu "spróbuj" które zwracają wynik albo nil. Wtedy można tworzyć dużo prostsze konstrukcje niż drabinki ifów:
function dodajGracza(gracz)
  return spróbujDodaćDoFrakcji(gracz, frakcja_1) or spróbujDodaćDoFrakcji(gracz, frakcja_2) or błąd("nie ma wolnych frakcji")
end
Oczywiście w C++ będzie trudniej ale idea jest podobna.

Offline Kurak

  • Użytkownik

# Styczeń 09, 2013, 21:47:54
Pokaż mi scenariusz, gdzie to się opłaci. Jak warunki są spełnione bezpośrednio przed wywołaniem funkcji, to siłą rzeczy będą i w jej środku.
No nie, bo to zakładałoby że nic nie dzieje się równolegle. A w międzyczasie może np. dołączyć się inny gracz i zapełnić frakcję.

Offline ison

  • Użytkownik

# Styczeń 09, 2013, 21:54:54
@Xirdus, po prostu imo to jest dosyć ryzykowne, gdy wywołanie niektórych metod w nieodpowiednim momencie może doprowadzić do zagłady. Jeśli w którymś miejscu ktoś zapomni dać warunku sprawdzającego czy gracz może dołączyć do frakcji i go doda, to taki błąd bedzie prawie niewykrywalny, a może sporo namieszać.
Pomimo dwukrotnego sprawdzania warunków, to chyba i tak skorzystam z tego rozwiązania.

@Kurak, dobry motyw z funkcjami zamiany enuma na string (i na odwrót) w namespace.

Co do wielowątkowości to jeszcze się nie zastanawiałem nad tym, ale rzeczywiście jeśli miałbym z niej korzystać to jest to dosyć mocny argument. W pierwotnym rozwiązaniu (tzn funkcja zwraca czy się udało czy nie) wystarczy, że mutex będzie zakładany wewnątrz metody addPlayer.

Offline Xirdus

  • Redaktor

# Styczeń 09, 2013, 22:03:21
No nie, bo to zakładałoby że nic nie dzieje się równolegle. A w międzyczasie może np. dołączyć się inny gracz i zapełnić frakcję.
Prosty mutex wystarczy.

Jeśli w którymś miejscu ktoś zapomni dać warunku sprawdzającego czy gracz może dołączyć do frakcji i go doda, to taki błąd bedzie prawie niewykrywalny
O RLY? "Ty, pa, ta frakcja ma za dużo graczy. A ten gracz jest w obu naraz! Coś musiało pójść nie tak... Zbadajmy funkcję addPlayer()... Czysto... No to zobaczmy wszystkie wywołania... Ajć, tu zapomniałem o warunkach!". I wątpię, by tych wywołań było więcej jak trzy na frakcję.

Offline Kurak

  • Użytkownik

  • +1
# Styczeń 09, 2013, 22:13:02
Prosty mutex wystarczy.
Nie zawsze można zablokować zasób tak by zapewnić jego dostępność. Np. nie zmusisz gracza żeby nie wyciągnął karty pamięci dokładnie między checkiem a zakończeniem zapisu, nie zapewnisz stałej dostępności clouda. To są akurat sytuacje gdzie sprawdziłyby się wyjątki, ale czasem nie można ich użyć - i wtedy nie tylko check, ale i samo wykonanie musi umieć powiedzieć że coś poszło źle.

Offline Xirdus

  • Redaktor

# Styczeń 09, 2013, 22:24:25
Np. nie zmusisz gracza żeby nie wyciągnął karty pamięci dokładnie między checkiem a zakończeniem zapisu

nie zapewnisz stałej dostępności clouda
W takim (albo analogicznym) przypadku addPlayer() nawali niezależnie od spełnienia warunków.

Poza tym, myślałem że rozmawiamy o dołączaniu gracza do serwera gry FPS i wyborze drużyny, albo coś w ten deseń (na początku tematu to myślałem w ogóle że chodzi o frakcje w sensie "TES-owym", że tak się wyrażę).

Offline Avaj

  • Użytkownik

# Styczeń 09, 2013, 22:30:17
Nie zawsze można zablokować zasób tak by zapewnić jego dostępność. Np. nie zmusisz gracza żeby nie wyciągnął karty pamięci dokładnie między checkiem a zakończeniem zapisu, nie zapewnisz stałej dostępności clouda. To są akurat sytuacje gdzie sprawdziłyby się wyjątki, ale czasem nie można ich użyć - i wtedy nie tylko check, ale i samo wykonanie musi umieć powiedzieć że coś poszło źle.
no ale wtedy musisz transakcje implementować, żeby móc cofnąć coś co w połowie padło :)