Wzorzec dla wielokrotnego if-else

0

Witam, czy da sie jakos zastapic taki brzydki kod:

        if(obj instanceof A) {
            ...
        } else if(obj instanceof B) {
            ...
        }
        } else if(obj instanceof C) {
            ...
        }
        } else if(obj instanceof D) {
            ...
        }

na cos czytelniejszego i elastyczniejszego? obj jest zwracane przez pewna metode, warunek sprawdza czy obj jest jednego z wymienionych typow i wykonuje odpowiednie akcje.

0

Polimorfizm?

0

Może taka konstrukcja

interface Foo
{
    //tu jakaś abstrakcyjna metoda doSomething()
}
class A implements Foo
{
    //to implementacja metody doSomething()
}
....
class Z implements Foo
{
    //to implementacja metody doSomething()
}
...
    obj.doSomething();
0

Moze nie opisalem problemu dosc dokladnie.
Moja metoda przeszukuje liste obiektow typu Akcja. Ale mam zdefiniowane pare typow akcji dziedziczacych po interfejsie Akcja. Przypuscmy, ze metoda zwrocila obiekt Klikniecie. Potrzebuje podac x i y miejsca gdzie kliknac, oraz odstep czasu po jakim powinno nastapic klikniecie. Ale metoda moze mi zwrocic tez obiekt Dzwiek ktory przyjmuje zupelnie inne parametry (plik dzwiekowy, czy powtarzac odtwazanie itp.).
Teraz gdy chce sprawdzic wlasciwosci jakiegos obiektu, przepuszczam go przez tego nieszczesnego if-else i dopiero wtedy wiem jakie dane konkretnie moge wyciagnac z obiektu. Dlatego zastanawiam sie czy jest jakies lepsze rozwiazanie mojego problemu.

0

Przekazuj zawsze wszystkie parametry, odpowiednia metoda doSomething() "wie" które z nich są jej niepotrzebne i powinna je zignorować.

0

Albo opakuj parametry w obiekt, tak jak sie to dzieje przy zdarzeniach, np. DoSomething(Params params).

0

ja bym użył switch - case.
Wiem ze niektorzy by namnozyli klas, ale bez przesady....

0

Napisz dokładniej jak sobie to wyobrażasz.
switch(co tu ma być?)

0

Switch pójdzie, tyle że nie w Javie, a w Scali:

obj match {
  case a: A => a.doSomethingA
  case b: B => b.doSomethingB
  case c: C => c.doSomethingC
  case _    => doSomethingElse
}

Skompilujesz do normalnego bajtkodu Java, więc nie powinno być problemu połączyć z resztą projektu :)

Ogólnie rozwiązanie nazywa się "pattern matching" i wiele innych języków też to ma, tyle że Java pechowo nie.

@egon:
Czasem taki switch jest lepszy niż polimorfizm. Polimorfizm ma tę wadę, że bardzo łatwo skończyć z zestawem klas, które śpiewają, grają, wyprowadzają psa na spacer i parzą kawę. Każdy przypadek jednak należy rozpatrywać osobno.

@Swietowit (niżej): no co ja poradzę, że w Scali dokładnie jest rozwiązanie problemu postawionego w pierwszym poście. Gdyby było w Javie, podałbym w Javie. :D

0

Scalowy masturbant...

0

Uzylem switch-case, ale nie przeczytalem informacji o opakowaniu parametrow w osobna klase. Zobacze jak to bedzie dzialac.
Dzieki za wszystkie podpowiedzi

0
Krolik napisał(a)

@egon:
Czasem taki switch jest lepszy niż polimorfizm. Polimorfizm ma tę wadę, że bardzo łatwo skończyć z zestawem klas, które śpiewają, grają, wyprowadzają psa na spacer i parzą kawę. Każdy przypadek jednak należy rozpatrywać osobno.

Czasem może... widziałem kiedyś kolesia który dostał task, pisał 2 miesiące, własnie takiego if()elsa, po 2 miesiącach się zwolnił bez podania przyczyny. Obczailiśmy jego kod, ten if()else ciągnał się przez 700+ lini! A wystarczyło dodać jedną klasę bazową i w niej jedną metodę virtualną, nasz teamowy n00b zrobił ten task w niecałe 2 dni. Well, jak kto woli.

PS. A jak ktoś doda nową klasę to znowu będzie grzebanie w tym if()elsie?

0

Switch oraz dziedziczenie skalują się tak samo dobrze, tyle że pod innym względem.

Switch skaluje się dobrze względem liczby operacji na obiektach.
Dziedziczenie skaluje się dobrze względem liczby klas.
Switch skaluje się źle względem liczby klas.
Dziedziczenie skaluje się źle względem liczby operacji na obiektach.

Wnioski są proste:

  1. Jeśli dodawanie nowych klas ma być możliwe bez ruszania reszty kodu => dziedziczenie + polimorfizm
  2. Jeśli dodawanie nowych operacji / funkcji ma być możliwe bez ruszania reszty kodu => switch
  3. Jeśli masz operacje działające równocześnie na różnych klasach => switch

W przypadku rozbudowanej hierarchii klas jak musisz dodać nową metodę do interfejsu, to masz przerąbane. Podobnie w kodzie, który opiera się na switchach, masz przerąbane jak chcesz dodać nową klasę.

Ot, trzeba decyzje podejmować świadomie, znając wady i zalety jednego i drugiego.

PS. Istnieją mechanizmy, które nie mają powyższych ograniczeń i skalują się dobrze w obu wymiarach.

0

Nie chcę się wykłócać ale czy masz jakieś dowody na to wszystko?

0

Java, niejava, mialem nadzieje, ze jest jakis uniwersalny patent na tego typu problem. Teraz dla kazdego obiektu albo tworze osobne okno dialogowe konfiguracji, albo tworze uniwersalne z if-else/switch-case. Moga powstawac kolejne klasy implementujace interfejs, dlatego troche to frustrujace, ale widocznie tak trzeba...

0

Nie chcę się wykłócać ale czy masz jakieś dowody na to wszystko?

Jakie dowody, przecież to jest tak oczywiste, że nie trzeba nic udowadniać.
Jeśli dodajesz nową operację do interfejsu, to musisz zaimplementować ją we wszystkich klasach implementujących interfejs, czyli w różnych miejscach programu. W przypadku switcha nie musisz zmieniać nic, prócz dopisania jednej metody ze switchem w JEDNYM MIEJSCU. Więc switch w tej sytuacji skaluje się lepiej.

Poza tym jest kilka przypadków, których nie da się sensownie zrobić przez interfejs - wszystkie przypadki, gdzie musisz na raz przetwarzać więcej niż jeden obiekt. Typowym zastosowaniem jest upraszczanie drzewa wyrażenia algebraicznego. Wstawienie tego kodu jako element klas reprezentujących operacje (np. dodawanie, odejmowanie, mnożenie itp) i liście (np. liczby), to IMHO skopanie projektu. Bo w ten sposób uzależniłbyś np. klasę Dodawanie od klasy Mnożenie i na dodatek cały kod do upraszczania rozstrzelony byłby po różnych klasach. Łatwiej przeanalizować jeden duży switch (zresztą można go rozbić zawsze na kilka metod znajdujących się blisko siebie).

@koru:
Istnieje uniwersalny patent na to i robi się to w ten sposób, że klasy implementują interfejs dający dwie metody: jedna służy do odpytania obiektu, o konfigurowalne własności (wraz z opisami), druga do ustawienia własności. Wtedy możesz mieć jedno uniwersalne okienko dialogowe do konfiguracji wszystkich klas, dostosowujące się do każdej klasy bez używania switcha i uzależniania go od innych klas.
Zamiast metody do odpytywania o właściwości możesz też w Javie użyć refleksji i odpowiednich adnotacji.

0
Krolik napisał(a)

Nie chcę się wykłócać ale czy masz jakieś dowody na to wszystko?

Jakie dowody, przecież to jest tak oczywiste, że nie trzeba nic udowadniać.

Jeżeli to jest taka oczywista ocywistość to nawet dalej nie czytam. bo po co?

0

Żądanie dowodów w dziedzinie, która nie jest dziedziną ścisłą (jak inżynieria oprogramowania), to tak jak żądanie dowodów teorii ekonomicznych albo socjologicznych :P

Ale jak byś wyjrzał conieco poza OOP, to by było dla Ciebie oczywiste, to co napisałem wcześniej i nie musiałbym dokładniej tłumaczyć.

0
Krolik napisał(a)

Żądanie dowodów w dziedzinie, która nie jest dziedziną ścisłą (jak inżynieria oprogramowania), to tak jak żądanie dowodów teorii ekonomicznych albo socjologicznych :P

... albo urojen...

0

@Krolik:
@EgonOlsen:

Krolik napisał(a)

Żądanie dowodów w dziedzinie, która nie jest dziedziną ścisłą (jak inżynieria oprogramowania), to tak jak żądanie dowodów teorii ekonomicznych albo socjologicznych

Ale dodajmy: to nie znaczy, że żądanie dowodów w potocznym znaczeniu tego słowa jest tu bezcelowe. Bo nie musi chodzić o dowody formalne. Potocznie, dowodem może być zwykłe wyjaśnienie. Choćby takie jakie przedstawiłeś w dalszej części posta. Dowodem mogą być wyniki jakichś badań.

Nie będą to formalne dowody pokazujące, że coś działa na 100% w 100% przypadków. Niemniej jednak takie wyjaśnienia, wskazówki, argumenty są bardzo przydatne.

Tutaj wystarczy chyba powiedzieć, że dobry kod jest skalowalny, łatwo poddaje się zmianom. Łatwość oznacza tutaj, że dodanie nowego elementu (metody, klasy) wymaga zmian w możliwie najmniejszej liczbie fragmentów kodu.

Można mieć nadzieję, że doświadczeni programiści rozumieją powyższe stwierdzenia i zgadzają się z nimi. A jeśli ktoś powie, że nie, to można mu któreś z tych stwierdzeń rozwinąć.

Zakładając więc prawdziwość powyższych stwierdzeń możemy powiedzieć, że użycie dziedziczenia powoduje, że łatwe jest dodawanie nowych typów, bo większość kodu operuje na abstrakcjach (nadklasach/interfejsach) i jest niezależna od konkretnego typu. Za to dodawanie nowych metod jest trudne, bo trzeba zmienić interfejs abstrakcji, a to wymaga zmian w wielu klasach konkretnych.

Po drugiej stronie mamy kod proceduralny, korzystający z klas bardziej jak ze struktur danych. Taki kod nie korzysta z możliwości, jakie daje OO. Bardziej zorientowany jest na wywoływanie funkcji. Być może metod, ale bez polegania na polimorfizmie. Zamiast tego kod taki polega bardziej na programowaniu strukturalnym -- przynajmniej w tym sensie, że dość często widzimy tam switche (które w programowaniu OO byłoby zastąpione dziedziczeniem, polimorfizmem).

W takim kodzie proceduralnym dodanie nowej funkcji jest łatwe. Trzeba dodać funkcję. W niej walnąć switcha, żeby wykonywała różną operację w zależności od tego, jaki obiekt dostała (można użyć instanceof zamiast switcha -- to to samo). I tyle.

Ale za to dodanie nowej struktury to prawdziwy ból. Trzeba zmienić wszystkie switche (ciągi instanceof) we wszystkich "procedurach"!

W kodzie polimorficznym, OO, jest odwrotnie. Dodaj nową funkcję, a musisz zmienić wszystkie klasy. Ale dodawanie nowego typu polega na napisaniu jednej podklasy i ew. podpięcie jej do jakiejś fabryki. I tyle. Nie trzeba skakać po procedurach, by dodać do każdego switcha nowy typ.

Także coś za coś. Kod OO nie jest idealny i trzeba to mieć na uwadze.

Co do samego problemu, jaki miał @koru, to czasem da się tych instanceofów jednak uniknąć. Trzeba by jednak przeanalizować sporą część systemu. Ustalić skąd pochodzą te klasy, które trzeba sprawdzać instanceofem. Dowiedzieć się, jakie dane są niezbędne do przetworzenia żądania i od jakich obiektów one pochodzą. Nieraz można w podobnych wypadkach użyć wzorca Odwiedzający (Visitor). Tutaj jednak, z tego co widzę, może on być zupełnie nieadekwatny.

0

Kod OO nie jest idealny i trzeba to mieć na uwadze.

Argumenty implicit rozwiązują problem raz na zawsze. Nigdy więcej modyfikacji istniejącego kodu aby dorzucić metodę do interfejsu albo dorzucić nową implementację.

0
Krolik napisał(a)

Kod OO nie jest idealny i trzeba to mieć na uwadze.

Argumenty implicit rozwiązują problem raz na zawsze. Nigdy więcej modyfikacji istniejącego kodu aby dorzucić metodę do interfejsu albo dorzucić nową implementację.

No ale to chyba nie w przypadku tego if elsa czy tam case?

0

Dla mnie ważnym argumentem na rzecz polimorfizmu (w sporze ze switchem) jest to, że po dodaniu metody do interfejsu łatwo znaleźć wszystkie klasy, które ten interfejs implementuja. Natomiast po dodaniu nowej klasy implementującej trudno znaleźć wszystkie switche.

0

Dla mnie argumentem jest to ze przy prawidlowo zaprojektowanej architekturze oop ten switch czy ifelse w ogóle nie jest potrzebny.

0

Tak? To jak rozwiążesz to?


abstract class Expression {
  abstract double evaluate();
}

class Add extends Expression {
  private Expression left;
  private Expression right;
  public Add(Expression left, Expression right) { ... /* wiadomo co */ }
  public double evaluate() { return left.evaluate() + right.evaluate(); }
}

class Subtract extends Expression { ... }
class Multiply extends Expression { ... }
class Divide extends Expression { ... }
class Constant extends Expression { ... }
class Variable extends Expression { ... }

Zadanie: dodać kod upraszczający drzewa wyrażeń. Np. a + 2 * a ma zastąpić 3 * a.
Gdzie dodasz ten kod i w jaki sposób? :P

No ale to chyba nie w przypadku tego if elsa czy tam case?

Dokładnie w przypadku tego.
Definiujesz nowy interfejs na potrzeby tylko i wyłącznie tego kawałka kodu (realizowanego do tej pory przez switch) oraz dostarczasz konwersje implicit konwertujące przekazany obiekt na inny obiekt implementujący ten nowy interfejs. Więc masz rozwiązanie w pełni polimorficzne, jednak bez wymagania od twórcy oryginalnej hierarchii implementowania jakiś specjalnych interfejsów na Twoje potrzeby. Ba, nawet lepiej - twój kod może w ten sposób wspólnie obsługiwać wiele klas, których jedyną klasą bazową jest Object (tak jak switch). Stąd oryginalna hierarchia może pozostać bardzo prosta i nie ma ryzyka, że po 5 latach rozwoju będzie zmywać, sprzątać, robić kawę, śpiewać i buczeć.

Wada tego rozwiązania jest tylko taka, że jest to statyczny polimorfizm a nie dynamiczny. No, ale przypadki, kiedy musisz mieć dynamiczny można policzyć na palcach jednej ręki. Mam kumpla, który twardo koduje w C++ i po paru latach stwierdził, że virtual to w ogóle zbędne słowo kluczowe - wszystko można załatwić statycznym polimorfizmem. No, ja się z nim nie do końca zgadzam, ale coś w tym jest.
W ogóle OOP jest przereklamowane (a w szczególności dziedziczenie).

Natomiast po dodaniu nowej klasy implementującej trudno znaleźć wszystkie switche.

Jeśli hierarchia klas jest sealed, to bardzo łatwo. Switche zwyczajnie nie skompilują się po dodaniu nowej klasy, jeśli nie dodasz nowego przypadku, o ile nie było defaulta.

0
Krolik napisał(a)

Switche zwyczajnie nie skompilują się po dodaniu nowej klasy, jeśli nie dodasz nowego przypadku, o ile nie było defaulta.

Tak glupio troche przekopywac pol projektu tylko dlatego ze ktos dodal nowa klase.

0

Dokładnie tak samo głupio jak przekopywać pół projektu, bo ktoś potrzebuje nową operację. Nie raz miałem taki przypadek, że trzeba było coś dodać, to przy dużej liczbie implementujących klas było to dosyć denerwujące. Zresztą, kończy się to najczęściej tym, że wszelkie operacje dodatkowe trafiają później do jakiś utilsów statycznych jak Collections w Javie. Bo nie wolno zmienić interfejsu. IMHO paskudne rozwiązanie.

0
Krolik napisał(a)

Dokładnie tak samo głupio jak przekopywać pół projektu, bo ktoś potrzebuje nową operację. Nie raz miałem taki przypadek.

Macie tam w ogóle architekta na tym waszym "projekcie"? Czy to specyfika "języka"?

0

Zapomniałem, że Ty jesteś tak zaj***istym architektem, że z góry w dwuletnim projekcie na początku potrafisz od razu bezbłędnie określić, jakie będą interfejsy i jakie będą miały operacje, i to się nigdy nie zmienia. Gratuluję. :-D

Z mojej strony temat zamknięty, jeśli ktoś chce dyskutować dalej, to można przenieść do flame.

0
Krolik napisał(a)

Zapomniałem, że Ty jesteś tak zaj***istym architektem, że z góry w dwuletnim projekcie na początku potrafisz od razu bezbłędnie określić, jakie będą interfejsy i jakie będą miały operacje, i to się nigdy nie zmienia. Gratuluję. :-D

Rozumiem ze u was sie nie projektuje a planuje najwyzej bierzaca iteracje, natomiast przy koniecznosci dodania jakiejs klasy przekopuje sie pol projektu, fajnie macie, przynajmniej zawsze macie co robic.

Dla twojej wiadomosci powiem ze moj piewszy powazniejszy system jest rozwijany do dzisiaj, bez zadnych zmian w modulach/klasach ktore raz zostaly oznaczone jako ready. System jest tak zaprojektowany ze przy dodawaniu nowych obiektow/modulow/driverow nawet nie musi wiedziec ze cos sie dodalo/zmienilo. No ale ja przy projektowaniu uzylem tego wstretnego polimorfizmu no i calosc jest napisana w tym ohydnym C++.

Krolik napisał(a)

Gratuluję. :-D

Dzieki.

1 użytkowników online, w tym zalogowanych: 0, gości: 1