Czasami mam dość pisania switchów, tylko po to by wywołać odpowiednią metodę – przeważnie o takiej samej sygnaturze, dla przykładu:

switch(command)
{
    case "R":
        TurnRight();
        break;
    case "L":
        TurnLeft();
        break;
    case "M":
        Move();
        break;
    default:
        // error
        Die();
        break;
}

Niby fajnie, ale jakoś nie za fajnie, ja chciałbym móc zrobić coś w stylu:

Wywołaj polecenie dla Wartości x;

Jedna linijka, proste i przyjemne w użyciu. Kiedy zmieniłem tok myślenia zacząłem się zastanawiać nad rozwiązaniem tego małego problemu.

W ten sposób napisałem sobie mała pomocniczą klasę:

public class CallRegister<T, TFuncOrAction>
{
    private readonly Dictionary<T, TFuncOrAction> _callDict = new Dictionary<T, TFuncOrAction>();

    public CallRegister<T, TFuncOrAction> Registry(T value, TFuncOrAction funcOrAction)
    {
        if (_callDict.ContainsKey(value))
        {
            _callDict[value] = funcOrAction;
        }
        else
        {
            _callDict.Add(value, funcOrAction);
        }

        return this;
    }

    /// <exception cref="KeyNotFoundException"></exception>
    public TFuncOrAction Resolve(T value)
    {
        return _callDict[value];
    }
}

Dzięki temu za pomocą inversion of control mogę sobie zdefiniować jaką metodę chcę uruchomić kiedy poproszę o X:

var callReg = new CallRegister<int, Action>();
callReg.Register(x, SayHello);
callReg.Resolve(x)();

albo:

CallRegister<int, Func<int, int, bool>> reg = new CallRegister<int, Func<int, int, bool>>();

reg.Registry(0, Test);
reg.Registry(1, (a, b) => a + b == 0);

Console.WriteLine(reg.Resolve(1)(10, 10));
Console.WriteLine(reg.Resolve(0)(10, 10));

Oczywiście rozwiązanie to nie jest dobre dla wszystkich możliwych zastosować switch – jednak załatwia problem, który mnie dręczył. Minusem jest wyjątek, który może zostać wyrzucony przy pobieraniu elementu ze słownika jak. Jednakże analizą parametrów zajmują się na przykład kontrakty, wyjątki zaś powinny być obsłużone w aplikacji a nie połykane.

12 KOMENTARZE

  1. Z ciekawości: po co jest ten if w metodzie Registry (zamiast zwykłego _callDict[value] = funcOrAction; )

    Możesz napisać więcej o problemie, który ten kod rozwiązuje? W sensie dlaczego takich switch-ów jest tak dużo.

  2. szczerze z tym ifem, to czyste przyzwyczajenie i moja nie wiedza. dla mnie rozsadnym byloby gdyby get[] i set[] zwracal ten sam wyjatek ale jak patrze w reflectorze tak nie jest :) nauczylem sie dzis czegos nowego :) dzieki :)

    Co do miejsc, zadko sie zastanawiam nad takimi rzeczami – czasami tak po prostu jest. Przy stosowaniu stanow czesto sa proste metody ktore w zaleznosci od jakiegos parametru zmieniaja stan – pierwsze wykorzystanie. Drugim moze byc wykorzystanie wzorca CMP http://bit.ly/clIwnO a trzecim parametrow wejsciowych do command line. Moze nie koniecznie trzeba zastepowac switch, mozna tez ify. Czasami poprostu nie interesuje mnie metoda a wynik, taka kolejna abstrakcja ;) ktora moze prowadzic do "wyabstrakcjonowania" calego kodu, ale podobnie jest z DI czy IoC ;)

    Na pytanie dlaczego jest ich tak duzo – mozna roznie odpowiedziec, zly design (zly bo mamy 30 metod ktory trzeba za pomoca jakiegos warunku wywolac), dobry design (bo mamy rozbicie na stany i tylko trzy rozne metody do wywolania).

    Albo tak jak teraz znajomi wykorzystali ten fragment kodu by zarejestrowac wszystkie metody w danej klasie ktore spelniaja dana specyfikacje, kazda metoda miala atrybut okreslajacy jej ID, dzieki czemu mozna automatycznie zarejestrowac wszystkie metody przy tworzeniu instancji CallRegistry, a nastepnie wywoluja odpowiwiednia metode w zaleznosci od parametru wejsciowego.

  3. Nakladaja mi sie takie zastrzezenia:

    – Czy rzeczywiscie warto prosty i dla wszystkich zrozumialy switch statement zastapic czyms co nie jest proste (przynajmniej w tej sytuacji)? Jak napotkam keyword ‘switch’, wiem odrazu o co chodzi. Jak napotkam sie na ‘callregistry’, nie wiem o co choczi i musze czytac implementacje. I nawet wtedy nie byl bym pewny czy rzeczywiscie sie domysle ze autor mial namysli zastapic prosty switch. Czy kontrakt spelniony przez switch jest napewno ten sam co spelnia call registry? I czemu wogle musze tracic czas nad ta analiza. Jakby programista uzyl switch to juz dawno bym siedzial z chlopakami w pubie i pil piwo.

    – czemu zastepowac cos co dziala bezblednie przez custom kod ktory nie jest pewny.

    – compiler ma mozliwosci zoptymalizowac switch, czemu mu ja zabierac i zastapic przez cos co z gory nie bedzie na tyle efektywne co prosty switch.

    – keep it simple and don’t be too smart – to chyba juz kazdy slyszal – ale jak widac malo kto sie tego trzyma ;)

  4. @Misiek: Odpowiedź jest całkiem prosta, bo programiści to lubią robić ;-) A na dobrą sprawę, przykład i jedna linijka tłumaczenia po co i na co to załatwia sprawę i miejsc w pubie nie zajmą ;->

    Chociaż ja bym się jednak próbował zastanawiać nad innym rozwiązaniem tego problemu – ale, że nie do końca go rozumiem (problem) to sobie na razie odpuszczę ;-)

  5. :) no niestety programisci to lubia. Przpomina mi sie post Pana Procenta, ktory zajmuje sie pytaniem jakich programistow lepiej miec w zespole, maniakow, ktorzy kochaja programowanie i spedzaja kazda chwile nad wymyslaniem nowych rozwiazan (lub jak w tym wypadkow problemow) czy poprostu programistow pracownikow (procent uzyl inne slownictwo), ktorzy dostarczaja w tych swoich 8 godzinach solidny kod (w tym wypadku z klasycznym switchem).

    Naszczescie o takich problemach mozna tez i przy piwie porozmawiac ;)

  6. czy warto? jak switch ma 10 przypadkow, warto, 10 * 3 linijki + default 3 linijki + deklaracja 3 linijki = 36 linii kodu zamiast 11. Dodatkowo zmiana switcha (nie koniecznie ktory musi byc na int moze byc na char) i utrzymanie tego wymaga czasu a i zapomniec mozna.

    jak napotykasz DI czy IoC wiesz o co chodzi? wiesz wiec movmentRegistry.Resolve("Right") czy nawet zmiana movmentResigry.Call("Right")(); jest chyba bardziej przejrzyste niz: switch(movmentDirection) { case "Right": MoveRight(); break; default: Die(); break; }. Tak samo jak ServiceLocator.Instance.Resolve(IRepository); nie intersuja Cie pewne aspekty i nie ma co na nie tracic czasu.

    Nie koniecznie autor mial na mysli zastapienie prostego switcha tak samo jak nie koniecznie autor w IoC ma namysli abstrakcje danych. Moze autor chcial poprostu zebrac sobie metody ktore w zaleznosci od parametru maja byc wywolane. Na przyklad system logowania ma 10 metod w zaleznosci od tego jaki "system" uzytownik wybral: sms, kod, token, haslo, open id itp.

    W wypadku ktory pokazalem, tak kontrakt jest ten sam. Wywolanie odpowiedniej metody/zmiana stanu. Czy kontrakt tworzenia instancji w IoC jest taki sam jak new Instance();? takie samo pytanie

    Co do efektywanosci, skoro i tak piszemy w JAva/.NET to raczej mniej przykladamy uwagi do performance, skoro korzystamy z ORM to jeszcze mniej, a skoro korzystamy z IoC, AOP, DI, Proxy, AutoMapper, LINQ, expressions itp itd. to jeszcze mniej. wiec bez przesady :)

    Keep it simple – zgadzam sie, dla mnie 50 linijek kodu switcha nie jest simple, 10 linijek registryCall tak.

    Nie wiem o co Ci dokladnie chodzi Misiek ale zaraz pewnie uslysze ze i IoC nie stosujesz bo nie jest "simple", mockow nie stosujesz bo to tez nie jest "simple", no i to wszystko ma wplyw na wydajnosc – LINQu i expressions na pewno nie stosujesz bo nie macie tego w Java :)

    Jezeli switch ma miec 2-3 wywolania i jest oparty o enum to spoko, mam pewnosc ze jak zmienie wartosci enum to i on mi sie zmieni, a jezli dodan kolejna wartosc enum?. W przeciwnym wypadku nie mam zadnej gwarancji. A teraz ktos jeszcze w dwoch miejscach w roznych klasach umiesci danego switcha i zarzadzanie tym zaczyna byc "ciezkie" lub "uciazliwe". Ja nie chce sie meczyc i przeszukiwac kazdego wystepowania switch by cos zmienic, ja chce miec metoda Configure() i ja zmieniac a reszta niech sie zajmie callRegistry.

  7. E tam, jakich problemów ;-)

    Najlepiej to sobie zrównoważyć, chociaż to też zależy od branży – w gamedevie np. fajnie mieć takich którzy po wyjściu z domu stawiają średnik, a nie zamykają drzwi ;-) [no, przynajmniej od strony klienta, który dostanie czasami kilka zbędnych ale fajnych atrakcji]

  8. Misiek chyba zle zapamietales post Procenta :) ale tak czy siak osoba w 8h moze dostarczyc solidny kod i wcale ze switchow nie korzystac i na pohybel Tobie jeszcze z IoC skorzysta i pojdzie po 7h na browara bo zakonczy wczesniej swoj kodzik, gdyz jest solidny i nie bedzie marnowal czasu pracodawcy na pisanie zbednych linijek kodu :)

  9. zbiles mnie tym z tropu :) Maniek to osoba wykorzystujaca dostepne frameworki? moim zdaniem nie, ale moze o cos innego chodzilo :)

  10. Bez dokładnej znajomości kontekstu zastosowania tego mechanizmu też miałbym pewne zastrzeżenia co do jego wpasowywania wszędzie zamiast switcha – mały switch nikomu krzywdy nie zrobił, gorzej – jak pisał Gutek – gdy się rozrastają.
    "Niepewność" własnego kodu można wyeliminować pokrywając go odpowiednimi testami jednostkowymi. Odpada też wówczas aspekt "nieznajomości" mechanizmu przez innych, wystarczy zerknąć w testy i wiadomo o co chodzi.
    A żeby wyeliminować i konieczność przeglądania testów, istnieje rozwiązanie jeszcze prostsze – klasę CallRegister można nazwać Switch i będzie jasne co robi:).

Comments are closed.