Laden...

typeof/is/if-Ketten auch dann vermeiden, wenn dynamische Bindung nicht anwendbar ist

Erstellt von ModelViewPresenter vor 12 Jahren Letzter Beitrag vor 9 Jahren 2.926 Views
ModelViewPresenter Themenstarter:in
67 Beiträge seit 2011
vor 12 Jahren
typeof/is/if-Ketten auch dann vermeiden, wenn dynamische Bindung nicht anwendbar ist

Hallo,

sicherlich kennt ihr das: ihr stoßt auf Code, bei dem mittels typeof/is und vielen if/else und switch/case Anweisungen Typ-Überprüfungen vorgenommen werden.

if (obj is Foo) {
   doFoo((Foo) obj);
} else if (obj is Bar) {
   doBar((Bar) obj);
} else if (...) {
   // even more instanceofs switching
} else {
   // not always but I've seen it a lot

}

Dass das schlechter Code ist, steht außer Frage. Nun fallen einem natürlich mehrere Möglichkeiten ein, diesen Code zu refaktorieren. An erster Stelle stehen dabei:
*Methodenüberladung *Polymorphie

Punkt 2 ist aus meiner Sicht der Beste. Leider ist dies nicht immer möglich. Was kann man also tun, wenn man von Polymorphie keinen Gebrauch machen kann?
*Dictionary mit Type-Mapping *Strategy Pattern mit Generics *Visitor Pattern

Ich wollte gern mal wissen, was aus eurer Sicht der beste Approach ist. Ich tendiere eigentlich zu Punkten 1 und 2 aus der zweiten Liste, weil das Visitor-Pattern doch zu stark die Kaspelung aufbricht.

3.170 Beiträge seit 2006
vor 12 Jahren

Hallo,

hier handelt es sich ja um einen klassischen Dispatcher, und dann muss auch irgendwo dispatcht werden. Wenn Du Dir die langen if-elses sparen willst, brauchst Du eine zentrale Instanz, die ein Mapping ermöglicht.

Ein Strategy Pattern bringt Dich IMO nur weiter, wenn das Objekt die konkrete Startegy kennt, die angewendet werden soll, da helfen auch Generics nix, weil der Typ zur Kompilierzeit noch nicht bekannt ist (wäre er es, würden es auch überladene Methoden tun) -> dann musst Du sämtliche Klassen, die in Deinem Beispielcode geswitcht werden, anpassen, und sie müssen ggf. über Dinge Bescheid wissen, die sie nix angehen.

Ein Visitor wäre möglich. Dann müssen die Klassen lediglich wissen, dass sie besucht werden können. Allerdings müssten auch dafür sämtliche Klassen geändert werden, um den Visitor zu akzeptieren.

Daher würde ich von Deinen angegbenen Vorschlägen eindeutig zum Dictionary mit Type-Mapping tendieren. Was Du Da über den Typen mappst, bleibt natürlich Dir überlassen, da kämen Delegate, Action<object>, Strategieklassen oder sonstiges in Frage, aber um das Mapping kommst Du IMO nicht herum. Klingt für mich auch irgendwie nach ServiceLocator.

Wenn Du nun den Fall hast, dass mehrere solche Dispatcher auf die gleichen Typen, aber mit unterschidlicher anzuwendender Funktionalität vorhanden sind, würde ich aber - je nach Anwendungsfall - sogar den Visitor vorziehen, weil Du sonst mehrere Dictionaries (ServiceLocator?) brauchst, die ja auch erst mal befüllt werden wollen, beim Visitor Pattern aber einfach weitere Visitors implementiert werden könnten.

Gruß, MarsStein

Non quia difficilia sunt, non audemus, sed quia non audemus, difficilia sunt! - Seneca

ModelViewPresenter Themenstarter:in
67 Beiträge seit 2011
vor 12 Jahren

Richtig, beim Strategy Pattern muss natürlich irgendwo eine Zuweisung der konkreten Strategie erfolgen. Hier kann natürlich das Problem auftreten, dass eine Methode nur einen Typ zurück gibt und man doch wieder abfragen muss. Allerdings wäre es auch schon eine Verbesserung, dies zu extrahieren und ähnlich wie bei einer Fabrik auszulagern.

Man könnte natürlich auch - wie du bereits angemerkt hast - mit einer StrategyMap arbeiten (ist Java-Code):

public interface StrategyHolder<D, S extends Strategy<D>> {

    public S getStrategy(D type);
}

public abstract class AbstractStrategyHolder<D, S extends Strategy<D>>
    implements StrategyHolder<D, S> {

    private final Map<D, S> strategies = new HashMap<D, S>();

    protected abstract S getDefaultStrategy(D type);

    protected AbstractStrategyHolder(final S... strategies) {
        for (final S strategy : strategies) {
            final D discriminator = strategy.getDiscriminator();
            this.strategies.put(discriminator, strategy);
        }
    }

    protected final S findByKey(final Object key) {
        return strategies.get(key);
    }

    @Override
    public final S getStrategy(final D type) {
        S result = findByKey(type);
        if (result == null) {
            result = getDefaultStrategy(type);
        }
        return result;
    }

}

Ich gebe dir recht, dass es unangenehm werden kann, wenn man viele solcher Dictionaries einrichten muss.

Vielen Dank für die Hinweise

49.485 Beiträge seit 2005
vor 12 Jahren

Hallo ModelViewPresenter,

Nun fallen einem natürlich mehrere Möglichkeiten ein, diesen Code zu refaktorieren.

ich wüsste nicht, wie da Methodenüber_ladung_ helfen soll, denn die passende Über_ladung_ wird (schon zur Compile-Zeit) aufgrund des statischen und nicht - wie im Beispiel angedeutet - aufgrund des dynamischen Types ermittelt. Vielleicht meinst du das Über_schreiben_ von Methoden, welches zusammen mit der Polymorphie (= Vielgestaltigkeit = der Fähigkeit einer Variablen, Objekte unterschiedlichen (Sub-)Typs zu enthalten) zur Laufzeit eine Auswahl der passenden Über_schreibung_ aufgrund des dynamischen Typs erlaubt (= dynamische Bindung).

Leider ist dies nicht immer möglich. Was kann man also tun, wenn man von Polymorphie keinen Gebrauch machen kann?

Das hängt doch vor allem davon ab, warum die Verwendung von dynamischer Bindung (also das, was du nicht ganz akkurat Polymorphie genannt hast) nicht möglich ist. Da die Gründe unterschiedlich sein können, kann es keine pauschale Antwort geben. Welche konkreten Fälle hast du im Sinn? Im Normalfall kommt man mit dynamischer Bindung hin. Im gezeigten Beispiel geht es auf jeden Fall und wäre gleichzeitig der bevorzugt Weg.

herbivore

ModelViewPresenter Themenstarter:in
67 Beiträge seit 2011
vor 12 Jahren

ich wüsste nicht, wie da Methodenüberladung helfen soll, denn die passende Überladung wird (schon zur Compile-Zeit) aufgrund des statischen und nicht - wie im Beispiel angedeutet - aufgrund des dynamischen Types ermittelt.

Was ich meinte, ist tatsächlich etwas in der Art wie unten zu sehen. Die Entscheidung darüber, welche Methode aufgerufen wird, geschieht zur Compilezeit anhand der statischen Typen der Argumente.

        
public void Do()
        {
            int param= 25;
            execute(param);
        }

        public void execute(String param)
        {
            Console.WriteLine(param);
        }

        public void execute(int param)
        {
            Console.WriteLine(param* 2);
        }

Welche konkreten Fälle hast du im Sinn? Im Normalfall kommt man mit dynamischer Bindung hin. Im gezeigten Beispiel geht es auf jeden Fall und wäre gleichzeitig der bevorzugt Weg.

Ich habe da Fälle im Sinn, wo der Code der eigenen Kontrolle nicht mehr unterliegt und nachträgliches implementieren nicht mehr möglich ist. Oder auch Fälle wie in How to eliminate If-Statements and InstanceOf with the Visitor-Pattern.

49.485 Beiträge seit 2005
vor 12 Jahren

Hallo ModelViewPresenter,

wo der Code der eigenen Kontrolle nicht mehr unterliegt und nachträgliches implementieren nicht mehr möglich ist.

mit einem Wrapper/Adapter kann man doch fast alles hinzufügen/ändern, selbst wenn man die Klasse selbst nicht ändern kann.

herbivore

ModelViewPresenter Themenstarter:in
67 Beiträge seit 2011
vor 12 Jahren

Da hast du natürlich recht. Solche Fälle sind schon stark konstruiert. In meinem konkreten Fall es einfach eine menschliche Entscheidungen (eines Architekten), dass keine Methoden nach implementiert werden sollten. Ich erhielt den Auftrag, die if-else-statements in "Methoden" oder "Objekte" auszulagern. Das macht zwar den Code etwas übersichtlicher und reduziert die NPath-Complexity, behebt aber nicht das substanzielle Problem. Ich jedenfalls wollte die Typprüfungen einfach loswerden.

C
34 Beiträge seit 2013
vor 9 Jahren

Hallo,

entschuldigt, dass ich diesen alten Post wiederbelebe. Aber ich habe mir auch diese Frage gestellt, wie kann ich möglichst elegant if-else Abfragen vermeiden.

Darüber bin ich in einem anderen Thema auch auf das visitor pattern gestoßen. Mich würde jetzt aber noch interessieren, was hier in diesem Thema und in diesem Zusammenhang mit

  1. Dictionary mit Type-Mapping und
  2. Wrapper/Adapter

gemeint ist?

Ich habe bisher immer die Methodenüberladung angewandt in diesen Fällen. Allerdings kann das auch sehr aufwändig, bzw. lang werden. Ist daran prinzipiell etwas auszusetzen? Oder geht das mit den beiden oben genannten Methoden besser?

VG
cpa87

49.485 Beiträge seit 2005
vor 9 Jahren

Hallo cpa87,

ich habe bisher immer die Methodenüberladung angewandt

du meinst das Über_schreiben_ von Methoden. Das Über_laden_ ist etwas anderes. Die Verwendung dynamischer Bindung (also das, was passiert, wenn eine überschriebene virtuelle Methode aufgerufen wird) ist schon der übliche und sinnvolle Weg, um if-Ketten über den Typ zu vermeiden.

zu 1. Was gemeint ist, hat MarsStein bereits oben beschrieben, sogar mit Beispiel, also Dictionary<Type,Action<T>>, um die zum Typ passende Methode (Action) aufzurufen. Das braucht man aber nur, wenn der Weg mit dem Überschreiben aus irgendeinem Grund verbaut ist. Insofern, nein, es geht damit nicht besser. Schon im Titel steht ja, dass es hier nur um Vorgehensweisen geht, die man einsetzt, wenn dynamische Bildung nicht anwendbar ist.

zu 2. Das hat nichts mit dem eigentlichen Thema zu tun, sondern bezog sich nur auf die Situation, dass man Fremdcode verwenden muss, den man nicht ändern kann. Gemeint ist Adapter (Entwurfsmuster).

herbivore

C
34 Beiträge seit 2013
vor 9 Jahren

Hey herbivore,

ich meinte natürlich überschreiben, sorry.

Vielen Dank, dass hilft mir sehr weiter. Ich meine man lernt viel über Vorgehensweisen und Funktionsweisen etc. Allerdings ist es schwierig zu unterscheiden, welches der idealere Weg bzw. der elegantere Weg ist. Ich finde oft eine Lösung zu einem Problem, aber dann habe ich immer das Gefühl, dass es einen besseren gibt.

So was steht leider oft nicht in den Büchern. Daher die Nachfragen hier im Forum.

Vielen Dank,

cpa87

16.842 Beiträge seit 2008
vor 9 Jahren

Oft gibt es auch nicht "den idealen Weg" oder "die ideale Vorgehensweise".
Es muss eben auch zum Umfeld und zur Anwendung passen.

Egal welchen Code Du schreibst: in einem Jahr würdest Du es völlig anders machen.
Das geht Dir aber nach einem Jahr mit C# genauso wie nach 15 Jahren.

C
34 Beiträge seit 2013
vor 9 Jahren

Ja richtig. Aber für bestimmte allgemein gültige Vorgänge, gibt es schon bevorzugte Vorgehensweisen. Aber dadurch dass es in der Regel so viele Möglichkeiten gibt, brauch man eine gewisse Erfahrung um einen eleganten Weg zu wählen.

Trotzdem Danke für den Hinweis.