Ist es in Ordnung, Objekte zu haben, die sich selbst umwandeln, selbst wenn dadurch die API ihrer Unterklassen verschmutzt wird?

33

Ich habe eine Basisklasse Base. Es hat zwei Unterklassen Sub1und Sub2. Jede Unterklasse verfügt über einige zusätzliche Methoden. Zum Beispiel Sub1hat Sandwich makeASandwich(Ingredients... ingredients)und Sub2hat boolean contactAliens(Frequency onFrequency).

Da diese Methoden unterschiedliche Parameter verwenden und völlig unterschiedliche Aufgaben ausführen, sind sie vollständig inkompatibel, und ich kann nicht einfach den Polymorphismus verwenden, um dieses Problem zu lösen.

Basebietet den größten Teil der Funktionalität, und ich habe eine große Sammlung von BaseObjekten. Alle BaseObjekte sind jedoch entweder ein Sub1oder ein Sub2, und manchmal muss ich wissen, um welche es sich handelt.

Es scheint eine schlechte Idee zu sein, Folgendes zu tun:

for (Base base : bases) {
    if (base instanceof Sub1) {
        ((Sub1) base).makeASandwich(getRandomIngredients());
        // ... etc.
    } else { // must be Sub2
        ((Sub2) base).contactAliens(getFrequency());
        // ... etc.
    }
}

Also habe ich mir eine Strategie ausgedacht, um dies zu vermeiden, ohne zu zaubern. BaseJetzt hat diese Methoden:

boolean isSub1();
Sub1 asSub1();
Sub2 asSub2();

Und natürlich Sub1implementiert diese Methoden als

boolean isSub1() { return true; }
Sub1 asSub1();   { return this; }
Sub2 asSub2();   { throw new IllegalStateException(); }

Und Sub2setzt sie umgekehrt um.

Leider haben jetzt Sub1und Sub2diese Methoden eine eigene API. So kann ich das zum Beispiel weiter machen Sub1.

/** no need to use this if object is known to be Sub1 */
@Deprecated
boolean isSub1() { return true; }

/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub1 asSub1();   { return this; }

/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub2 asSub2();   { throw new IllegalStateException(); }

Wenn bekannt ist, dass das Objekt nur ein Objekt ist Base, sind diese Methoden nicht veraltet und können verwendet werden, um sich selbst in einen anderen Typ umzuwandeln, sodass ich die Methoden der Unterklasse aufrufen kann. In gewisser Weise erscheint mir das elegant, aber andererseits missbrauche ich veraltete Annotationen, um Methoden aus einer Klasse zu "entfernen".

Da eine Sub1Instanz wirklich eine Basis ist, ist es sinnvoll, Vererbung anstelle von Kapselung zu verwenden. Geht es mir gut? Gibt es einen besseren Weg, um dieses Problem zu lösen?

Codeknacker
quelle
12
Alle 3 Klassen müssen jetzt voneinander wissen. Das Hinzufügen von Sub3 würde eine Menge Code-Änderungen mit sich bringen, und das Hinzufügen von Sub10 wäre geradezu schmerzhaft
Dan Pichelman,
15
Es wäre sehr hilfreich, wenn Sie uns echten Code geben würden. Es gibt Situationen, in denen es angebracht ist, Entscheidungen auf der Grundlage der jeweiligen Klasse von Dingen zu treffen, aber es ist unmöglich zu sagen, ob Sie in Bezug auf das, was Sie mit solchen erfundenen Beispielen tun, berechtigt sind. Was auch immer es wert ist, was Sie wollen, ist ein Besucher oder eine getaggte Gewerkschaft .
Doval
1
Entschuldigung, ich dachte, es würde die Sache einfacher machen, vereinfachte Beispiele zu nennen. Vielleicht poste ich eine neue Frage mit einem breiteren Spektrum von dem, was ich tun möchte.
Codebreaker
12
Sie implementieren Casting nur neu und instanceofsind auf eine Weise, die viel Eingabe erfordert, fehleranfällig und machen es schwierig, weitere Unterklassen hinzuzufügen.
user253751
5
Wenn Sub1und Sub2kann nicht austauschbar verwendet werden, warum behandeln Sie sie dann als solche? Warum nicht Ihre "Sandwichmacher" und "Außerirdischen" getrennt nachverfolgen?
Pieter Witvoet

Antworten:

27

Es ist nicht immer sinnvoll, der Basisklasse Funktionen hinzuzufügen, wie in einigen anderen Antworten vorgeschlagen. Das Hinzufügen zu vieler Sonderfallfunktionen kann dazu führen, dass ansonsten nicht miteinander verknüpfte Komponenten miteinander verbunden werden.

Zum Beispiel könnte ich eine AnimalKlasse mit Catund DogKomponenten haben. Wenn ich in der Lage sein wollen , dass sie zu drucken, oder zeigen Sie sie in der GUI könnte es übertrieben für mich sein , um hinzuzufügen renderToGUI(...)und sendToPrinter(...)zu der Basisklasse.

Der Ansatz, den Sie mit Hilfe von Typprüfungen und Abgüssen verfolgen, ist fragil - zumindest werden die Bedenken jedoch getrennt.

Wenn Sie jedoch feststellen, dass Sie diese Arten von Überprüfungen / Casts häufig durchführen, besteht eine Option darin, das Besucher- / Doppelversandmuster für diese zu implementieren. Es sieht ungefähr so ​​aus:

public abstract class Base {
  ...
  abstract void visit( BaseVisitor visitor );
}

public class Sub1 extends Base {
  ...
  void visit(BaseVisitor visitor) { visitor.onSub1(this); }
}

public class Sub2 extends Base {
  ...
  void visit(BaseVisitor visitor) { visitor.onSub2(this); }
}

public interface BaseVisitor {
   void onSub1(Sub1 that);
   void onSub2(Sub2 that);
}

Jetzt wird Ihr Code

public class ActOnBase implements BaseVisitor {
    void onSub1(Sub1 that) {
       that.makeASandwich(getRandomIngredients())
    }

    void onSub2(Sub2 that) {
       that.contactAliens(getFrequency());
    }
}

BaseVisitor visitor = new ActOnBase();
for (Base base : bases) {
    base.visit(visitor);
}

Der Hauptvorteil besteht darin, dass beim Hinzufügen einer Unterklasse Kompilierungsfehler auftreten und keine Fälle im Hintergrund fehlen. Die neue Besucherklasse wird auch zu einem netten Ziel, in das Funktionen hineingezogen werden. Zum Beispiel könnte es Sinn machen , sich zu bewegen getRandomIngredients()in ActOnBase.

Sie können auch die Schleifenlogik extrahieren: Beispielsweise könnte das obige Fragment zu werden

BaseVisitor.applyToArray(bases, new ActOnBase() );

Ein wenig weiter massieren und mit Java 8 Lambdas und Streaming können Sie bekommen

bases.stream()
     .forEach( BaseVisitor.forEach(
       Sub1 that -> that.makeASandwich(getRandomIngredients()),
       Sub2 that -> that.contactAliens(getFrequency())
     ));

Welche IMO ist so gut aussehend und prägnant, wie Sie bekommen können.

Hier ist ein vollständigeres Java 8-Beispiel:

public static abstract class Base {
    abstract void visit( BaseVisitor visitor );
}

public static class Sub1 extends Base {
    void visit(BaseVisitor visitor) { visitor.onSub1(this); }

    void makeASandwich() {
        System.out.println("making a sandwich");
    }
}

public static class Sub2 extends Base {
    void visit(BaseVisitor visitor) { visitor.onSub2(this); }

    void contactAliens() {
        System.out.println("contacting aliens");
    }
}

public interface BaseVisitor {
    void onSub1(Sub1 that);
    void onSub2(Sub2 that);

    static Consumer<Base> forEach(Consumer<Sub1> sub1, Consumer<Sub2> sub2) {

        return base -> {
            BaseVisitor baseVisitor = new BaseVisitor() {

                @Override
                public void onSub1(Sub1 that) {
                    sub1.accept(that);
                }

                @Override
                public void onSub2(Sub2 that) {
                    sub2.accept(that);
                }
            };
            base.visit(baseVisitor);
        };
    }
}

Collection<Base> bases = Arrays.asList(new Sub1(), new Sub2());

bases.stream()
     .forEach(BaseVisitor.forEach(
             Sub1::makeASandwich,
             Sub2::contactAliens));
Michael Anderson
quelle
+1: Diese Art von Dingen wird häufig in Sprachen verwendet, die erstklassige Unterstützung für Summentypen und Mustervergleiche bieten, und sie sind in Java gültig, obwohl sie syntaktisch sehr hässlich sind.
Mankarse
@Mankarse Ein bisschen syntaktischer Zucker kann es sehr hässlich machen - ich habe es mit einem Beispiel aktualisiert.
Michael Anderson
Nehmen wir an, das OP ändert hypothetisch seine Meinung und beschließt, a hinzuzufügen Sub3. Hat der Besucher nicht genau das gleiche Problem wie instanceof? Jetzt müssen Sie onSub3den Besucher hinzufügen und es bricht, wenn Sie vergessen.
Radiodef
1
@Radiodef Ein Vorteil der Verwendung des Besuchermusters gegenüber dem direkten Einschalten des Typs besteht darin, dass onSub3nach dem Hinzufügen zur Besucheroberfläche an jedem Ort, an dem Sie einen neuen Besucher erstellen, ein Kompilierungsfehler auftritt, der noch nicht aktualisiert wurde. Im Gegensatz dazu führt das Einschalten bestenfalls zu einem Laufzeitfehler - der schwieriger auszulösen sein kann.
Michael Anderson
1
@FiveNine, wenn Sie glücklich sind, das vollständige Beispiel am Ende der Antwort hinzuzufügen, das anderen helfen könnte.
Michael Anderson
83

Aus meiner Sicht: Ihr Design ist falsch .

In die natürliche Sprache übersetzt, sagen Sie Folgendes:

Vorausgesetzt wir haben animals, gibt es catsund fish. animalshaben Eigenschaften, die gemeinsam sind catsund fish. Aber das reicht nicht aus: Es gibt einige Eigenschaften, die sich catvon denen unterscheiden fish, daher müssen Sie Unterklassen bilden.

Jetzt haben Sie das Problem, dass Sie vergessen haben, Bewegung zu modellieren . Okay. Das ist relativ einfach:

for(Animal a : animals){
   if (a instanceof Fish) swim();
   if (a instanceof Cat) walk();
}

Das ist aber ein falsches Design. Der richtige Weg wäre:

for(Animal a : animals){
    animal.move()
}

Wo movewürde geteiltes Verhalten von jedem Tier anders umgesetzt werden.

Da diese Methoden unterschiedliche Parameter verwenden und völlig unterschiedliche Aufgaben ausführen, sind sie vollständig inkompatibel, und ich kann nicht einfach den Polymorphismus verwenden, um dieses Problem zu lösen.

Das heißt: Ihr Design ist kaputt.

Meine Empfehlung: Refactoring Base, Sub1und Sub2.

Thomas Junk
quelle
8
Wenn Sie tatsächlichen Code anbieten würden, könnte ich Empfehlungen abgeben.
Thomas Junk
9
@codebreaker Wenn Sie zustimmen, dass Ihr Beispiel nicht gut war, empfehle ich, diese Antwort zu akzeptieren und dann eine neue Frage zu schreiben. Überarbeiten Sie die Frage nicht, um ein anderes Beispiel zu erhalten, da die vorhandenen Antworten sonst keinen Sinn ergeben.
Moby Disk
16
@codebreaker Ich denke, das "löst" das Problem durch Beantwortung der eigentlichen Frage. Die eigentliche Frage lautet: Wie löse ich mein Problem? Refactor Ihren Code richtig. Umgestalten , so dass Sie .move()oder .attack()und richtig abstrakter thatTeil - anstelle von .swim(), .fly(), .slide(), .hop(), .jump(), .squirm(), .phone_home()etc. Wie Refactoring Sie richtig? Unbekannt ohne bessere Beispiele ... aber im Allgemeinen immer noch die richtige Antwort - es sei denn, Beispielcode und weitere Details legen etwas anderes nahe.
WernerCD
11
@codebreaker Wenn Ihre Klassenhierarchie Sie zu Situationen wie diesen führt, ist dies per Definition nicht sinnvoll
Patrick Collins
7
@codebreaker Bezogen auf Crisfoles letzten Kommentar gibt es eine andere Sichtweise, die hilfreich sein könnte. Zum Beispiel mit dem movevs fly/ run/ etc, das kann in einem Satz erklärt werden als: Sie sollten dem Objekt sagen, was zu tun ist, nicht wie es zu tun ist. In Bezug auf die Zugriffsmethoden sollten Sie, wie Sie in einem Kommentar an dieser Stelle bereits erwähnt haben, dem Objekt Fragen stellen und nicht dessen Status überprüfen.
Izkata
9

Es ist ein wenig schwierig, sich einen Umstand vorzustellen, in dem Sie eine Gruppe von Dingen haben und möchten, dass sie entweder ein Sandwich machen oder Aliens kontaktieren. In den meisten Fällen, in denen Sie ein solches Casting finden, werden Sie mit einem Typ arbeiten - z. B. filtern Sie in clang eine Reihe von Knoten nach Deklarationen, bei denen getAsFunction nicht null zurückgibt, anstatt für jeden Knoten in der Liste etwas anderes zu tun.

Möglicherweise müssen Sie eine Aktionssequenz ausführen, und es ist nicht relevant, ob die Objekte, die die Aktion ausführen, miteinander verknüpft sind.

BaseArbeiten Sie also anstelle einer Liste von an der Liste der Aktionen

for (RandomAction action : actions)
   action.act(context);

woher

interface RandomAction {
    void act(Context context);
} 

interface Context {
    Ingredients getRandomIngredients();
    double getFrequency();
}

Gegebenenfalls kann Base eine Methode implementieren, um die Aktion zurückzugeben, oder eine andere Methode, mit der Sie die Aktion aus den Instanzen in Ihrer Basisliste auswählen müssen (da Sie sagen, dass Sie keinen Polymorphismus verwenden können, vermutlich also die auszuführende Aktion) ist keine Funktion der Klasse, sondern eine andere Eigenschaft der Basen, ansonsten würden Sie Base nur die act (Context) -Methode geben.

Pete Kirkham
quelle
2
Man merkt, dass meine absurden Methoden darauf ausgelegt sind, die Unterschiede in der Funktionsweise der Klassen aufzuzeigen, sobald ihre Unterklasse bekannt ist (und dass sie nichts miteinander zu tun haben), aber ich denke, ein ContextObjekt macht die Sache nur noch schlimmer. Ich könnte genauso gut Objects an die Methoden weitergeben, sie besetzen und hoffen, dass sie der richtige Typ sind.
Codebreaker
1
@codebreaker Sie müssen Ihre Frage dann wirklich klären - in den meisten Systemen gibt es einen triftigen Grund, eine Reihe von Funktionen als agnostisch für das zu bezeichnen, was sie tun. Zum Beispiel, um Regeln für ein Ereignis zu verarbeiten oder einen Schritt in einer Simulation auszuführen. Normalerweise haben solche Systeme einen Kontext, in dem die Aktionen stattfinden. Wenn 'getRandomIngredients' und 'getFrequency' nicht miteinander verknüpft sind, sollten sie sich nicht im selben Objekt befinden, und Sie benötigen noch einen anderen Ansatz, z. B. die Erfassung der Quelle der Inhaltsstoffe oder der Häufigkeit durch die Aktionen.
Pete Kirkham
1
Sie haben Recht, und ich glaube nicht, dass ich diese Frage retten kann. Am Ende habe ich ein schlechtes Beispiel ausgewählt, also werde ich wahrscheinlich nur ein weiteres posten. GetFrequency () und getIngredients () waren nur Platzhalter. Ich hätte wahrscheinlich nur "..." als Argumente verwenden sollen, um klarer zu machen, dass ich nicht weiß, was sie sind.
Codebreaker
Das ist IMO die richtige Antwort. Verstehen Sie, dass Contextes sich nicht unbedingt um eine neue Klasse oder gar eine Schnittstelle handeln muss. Normalerweise ist der Kontext nur der Anrufer, sodass Anrufe in der Form erfolgen action.act(this). Wenn getFrequency()und getRandomIngredients()statische Methoden sind, benötigen Sie möglicherweise nicht einmal einen Kontext. So würde ich beispielsweise eine Job-Warteschlange implementieren, nach der sich Ihr Problem fürchterlich anhört.
QuestionC
Auch dies ist meist identisch mit der Besuchermusterlösung. Der Unterschied besteht darin, dass Sie die Methoden Visitor :: OnSub1 () / Visitor :: OnSub2 () als Sub1 :: act () / Sub2 :: act () implementieren
QuestionC
4

Wie wäre es, wenn Ihre Unterklassen eine oder mehrere Schnittstellen implementieren, die definieren, was sie tun können? Etwas wie das:

interface SandwichCook
{
    public void makeASandwich(String[] ingredients);
}

interface AlienRadioSignalAwarable
{
    public void contactAliens(int frequency);

}

Dann sehen deine Klassen so aus:

class Sub1 extends Base implements SandwichCook
{
    public void makeASandwich(String[] ingredients)
    {
        //some code here
    }
}

class Sub2 extends Base implements AlienRadioSignalAwarable
{
    public void contactAliens(int frequency)
    {
        //some code here
    }
}

Und Ihre for-Schleife wird:

for (Base base : bases) {
    if (base instanceof SandwichCook) {
        base.makeASandwich(getRandomIngredients());
    } else if (base instanceof AlienRadioSignalAwarable) {
        base.contactAliens(getFrequency());
    }
}

Zwei Hauptvorteile dieses Ansatzes:

  • kein Casting beteiligt
  • Sie können jede Unterklasse so viele Schnittstellen implementieren lassen, wie Sie möchten, was eine gewisse Flexibilität für zukünftige Änderungen bietet.

PS: Entschuldigung für die Namen der Interfaces, mir fiel in diesem Moment nichts Cooleres ein: D.

Radu Murzea
quelle
3
Dies löst das Problem nicht wirklich. Sie benötigen noch die Besetzung in der for-Schleife. (Wenn Sie dies nicht tun, brauchen Sie auch die Besetzung in OPs Beispiel nicht).
Taemyr
2

Der Ansatz kann in Fällen sinnvoll sein, in denen fast jeder Typ innerhalb einer Familie entweder direkt als Implementierung einer Schnittstelle verwendet werden kann, die ein bestimmtes Kriterium erfüllt, oder zur Erstellung einer Implementierung dieser Schnittstelle verwendet werden kann. Die eingebauten Sammlungstypen hätten IMHO von diesem Muster profitiert, aber da sie nicht zu Beispielzwecken dienen, werde ich eine Sammlungsschnittstelle erfinden BunchOfThings<T>.

Einige Implementierungen von BunchOfThingssind veränderlich. manche nicht. In vielen Fällen möchte ein Objekt, das Fred verwenden kann, möglicherweise etwas enthalten, BunchOfThingsund er weiß, dass nichts anderes als Fred in der Lage ist, es zu ändern. Diese Anforderung kann auf zwei Arten erfüllt werden:

  1. Fred weiß, dass es die einzigen Verweise darauf enthält BunchOfThings, und dass BunchOfThingses nirgendwo im Universum externe Verweise darauf gibt. Wenn niemand einen Verweis auf das BunchOfThingsoder seine Interna hat, kann ihn niemand ändern, sodass die Einschränkung erfüllt wird.

  2. Weder die BunchOfThingsnoch irgendwelche ihrer Interna, auf die externe Verweise existieren, können auf irgendeine Weise modifiziert werden. Wenn absolut niemand a ändern kann BunchOfThings, ist die Bedingung erfüllt.

Eine Möglichkeit, die Einschränkung zu erfüllen, besteht darin, ein empfangenes Objekt bedingungslos zu kopieren (verschachtelte Komponenten rekursiv zu verarbeiten). Eine andere Möglichkeit wäre, zu testen, ob ein empfangenes Objekt Unveränderlichkeit verspricht, und, falls nicht, eine Kopie davon anzufertigen und dies auch mit verschachtelten Komponenten zu tun. Eine Alternative, die in der Regel sauberer als die zweite und schneller als die erste ist, besteht darin, eine AsImmutableMethode anzubieten , mit der ein Objekt aufgefordert wird, eine unveränderliche Kopie von sich selbst zu erstellen (unter Verwendung AsImmutablealler geschachtelten Komponenten, die dies unterstützen).

Verwandte Methoden können ebenfalls bereitgestellt werden asDetached(für die Verwendung, wenn Code ein Objekt empfängt und nicht weiß, ob es mutiert werden soll). In diesem Fall sollte ein mutierbares Objekt durch ein neues mutierbares Objekt ersetzt werden, aber ein unveränderliches Objekt kann beibehalten werden as-is), asMutable(in Fällen , in denen ein Objekt weiß, dass es ein zuvor zurückgegebenes Objekt enthält asDetached, dh entweder eine nicht gemeinsam genutzte Referenz auf ein veränderbares Objekt oder eine gemeinsam nutzbare Referenz auf ein veränderbares Objekt) und asNewMutable(in Fällen, in denen der Code ein Äußeres empfängt Referenz und weiß, dass eine Kopie der darin enthaltenen Daten mutiert werden soll. Wenn die eingehenden Daten mutierbar sind, gibt es keinen Grund, eine unveränderliche Kopie anzufertigen, die sofort zum Erstellen einer mutierbaren Kopie verwendet und dann verworfen wird.

Beachten Sie, dass die asXXMethoden zwar leicht unterschiedliche Typen zurückgeben können, ihre eigentliche Rolle jedoch darin besteht, sicherzustellen, dass die zurückgegebenen Objekte die Programmanforderungen erfüllen.

Superkatze
quelle
0

Wenn ich die Frage ignoriere, ob Sie ein gutes Design haben oder nicht, und annehme, dass es gut oder zumindest akzeptabel ist, möchte ich die Fähigkeiten der Unterklassen berücksichtigen, nicht den Typ.

Daher entweder:


Verschieben Sie einige Kenntnisse über die Existenz von Sandwiches und Aliens in die Basisklasse, obwohl Sie wissen, dass einige Instanzen der Basisklasse dies nicht können. Implementieren Sie es in der Basisklasse, um Ausnahmen auszulösen, und ändern Sie den Code in:

if (base.canMakeASandwich()) {
    base.makeASandwich(getRandomIngredients());
    // ... etc.
} else { // can't make sandwiches, must be able to contact aliens
    base.contactAliens(getFrequency());
    // ... etc.
}

Dann überschreibt eine oder beide Unterklassen canMakeASandwich()und nur eine implementiert jedes von makeASandwich()und contactAliens().


Verwenden Sie keine konkreten Unterklassen, sondern Schnittstellen, um festzustellen, über welche Funktionen ein Typ verfügt. Lassen Sie die Basisklasse in Ruhe und ändern Sie den Code in:

if (base instanceof SandwichMaker) {
    ((SandwichMaker)base).makeASandwich(getRandomIngredients());
    // ... etc.
} else { // can't make sandwiches, must be able to contact aliens
    ((AlienContacter)base).contactAliens(getFrequency());
    // ... etc.
}

oder möglicherweise (und zögern Sie nicht, diese Option zu ignorieren, wenn sie nicht zu Ihrem Stil oder zu einem für Sie vernünftigen Java-Stil passt):

try {
    ((SandwichMaker)base).makeASandwich(getRandomIngredients());
} catch (ClassCastException e) {
    ((AlienContacter)base).contactAliens(getFrequency());
}

Persönlich mag ich die letztere Art, eine halb erwartete Ausnahme zu erwischen, normalerweise nicht , weil die Gefahr besteht, dass ein ClassCastExceptionComing-out von getRandomIngredientsoder makeASandwichnicht richtig erkannt wird, aber YMMV.

Steve Jessop
quelle
2
ClassCastException abfangen ist nur ew. Das ist der gesamte Zweck von instanceof.
Radiodef
@Radiodef: Stimmt, aber ein Zweck von <ist, das Fangen zu vermeiden ArrayIndexOutOfBoundsException, und einige Leute entscheiden sich auch dafür. Keine Berücksichtigung des Geschmacks ;-) Grundsätzlich ist mein "Problem" hier, dass ich in Python arbeite, wobei der bevorzugte Stil darin besteht, dass das Abfangen einer Ausnahme dem Testen im Voraus vorzuziehen ist, ob die Ausnahme auftritt, egal wie einfach der Voraus-Test wäre . Vielleicht ist die Möglichkeit in Java einfach nicht erwähnenswert.
Steve Jessop
@SteveJessop »Es ist einfacher, um Vergebung zu bitten als um Erlaubnis« lautet der Slogan;)
Thomas Junk
0

Hier haben wir einen interessanten Fall einer Basisklasse, die sich in ihre eigenen abgeleiteten Klassen zurückwirft. Wir wissen genau, dass dies normalerweise schlecht ist, aber wenn wir sagen möchten, dass wir einen guten Grund gefunden haben, lassen Sie uns schauen und sehen, welche Einschränkungen dies haben könnte:

  1. Wir können alle Fälle in der Basisklasse abdecken.
  2. Keine ausländische abgeleitete Klasse müsste jemals einen neuen Fall hinzufügen.
  3. Wir können mit der Politik leben, für die die Basisklasse die Kontrolle übernehmen soll.
  4. Wir wissen jedoch aus unserer Wissenschaft, dass die Vorgehensweise in einer abgeleiteten Klasse für eine Methode, die sich nicht in der Basisklasse befindet, die der abgeleiteten Klasse und nicht der Basisklasse ist.

Wenn 4 dann haben wir: 5. Die Politik der abgeleiteten Klasse steht immer unter der gleichen politischen Kontrolle wie die Politik der Basisklasse.

2 und 5 implizieren beide direkt, dass wir alle abgeleiteten Klassen auflisten können, was bedeutet, dass es keine externen abgeleiteten Klassen geben soll.

Aber hier ist das Ding. Wenn sie ganz bei Ihnen sind, können Sie das if durch eine Abstraktion ersetzen, die ein virtueller Methodenaufruf ist (auch wenn es ein Unsinn ist), und das if und die Selbstdarstellung entfernen. Deshalb tu es nicht. Ein besseres Design ist verfügbar.

Joshua
quelle
-1

Fügen Sie eine abstrakte Methode in die Basisklasse ein, die eines in Sub1 und das andere in Sub2 ausführt, und rufen Sie diese abstrakte Methode in Ihren gemischten Schleifen auf.

class Sub1 : Base {
    @Override void doAThing() {
        this.makeASandwich(getRandomIngredients());
    }
}

class Sub2 : Base {
    @Override void doAThing() {
        this.contactAliens(getFrequency());
    }
}

for (Base base : bases) {
    base.doAThing();
}

Beachten Sie, dass dies abhängig von der Definition von getRandomIngredients und getFrequency andere Änderungen erfordern kann. Aber ehrlich gesagt, in der Klasse, in der Aliens kontaktiert werden, ist getFrequency wahrscheinlich sowieso besser zu definieren.

Übrigens sind Ihre asSub1- und asSub2-Methoden definitiv eine schlechte Übung. Wenn Sie dies tun, tun Sie es durch Casting. Diese Methoden bieten Ihnen nichts, was Sie beim Casting nicht kennen.

Random832
quelle
2
Ihre Antwort deutet darauf hin, dass Sie die Frage nicht vollständig durchgelesen haben: Polymorphismus kann hier einfach nicht helfen. Es gibt verschiedene Methoden für Sub1 und Sub2, dies sind nur Beispiele, und "doAThing" hat eigentlich keine Bedeutung. Es entspricht nichts, was ich tun würde. Und wie steht es um Ihren letzten Satz: Wie wäre es mit garantierter Typensicherheit?
Codebreaker
@codebreaker - Es entspricht genau dem, was Sie in der Schleife im Beispiel tun. Wenn das keine Bedeutung hat, schreibe die Schleife nicht. Wenn es als Methode keinen Sinn ergibt, ergibt es keinen Sinn als Schleifenkörper.
Random832
1
Und Ihre Methoden "garantieren" die Typensicherheit auf dieselbe Weise wie das Casting: indem Sie eine Ausnahme auslösen, wenn das Objekt vom falschen Typ ist.
Random832
Mir ist klar, dass ich ein schlechtes Beispiel gewählt habe. Das Problem ist, dass die meisten Methoden in den Unterklassen eher Gettern ähneln als mutativen Methoden. Diese Klassen erledigen ihre Aufgaben nicht selbst, sondern stellen meist nur Daten zur Verfügung. Polymorphismus hilft mir da nicht weiter. Ich habe es mit Produzenten zu tun, nicht mit Verbrauchern. Außerdem: Das Auslösen einer Ausnahme ist eine Laufzeitgarantie, die nicht so gut ist wie die Kompilierungszeit.
Codebreaker
1
Mein Punkt ist, dass Ihr Code nur eine Laufzeitgarantie bietet. Es gibt nichts, was jemanden davon abhält, isSub2 vor dem Aufruf von asSub2 nicht zu überprüfen.
Random832
-2

Sie können beide makeASandwichund contactAliensin die Basisklasse verschieben und sie dann mit Dummy-Implementierungen implementieren. Da die Rückgabetypen / Argumente nicht in eine gemeinsame Basisklasse aufgelöst werden können.

class Sub1 extends Base{
    Sandwich makeASandwich(Ingredients i){
        //Normal implementation
    }
    boolean contactAliens(Frequency onFrequency){
        return false;
    }
 }
class Sub2 extends Base{
    Sandwich makeASandwich(Ingredients i){
        return null;
    }
    boolean contactAliens(Frequency onFrequency){
       // normal implementation
    }
 }

Es gibt offensichtliche Nachteile dieser Art von Dingen, wie was dies für den Vertrag der Methode bedeutet und was Sie über den Kontext des Ergebnisses ableiten können und was nicht. Du kannst nicht denken, da ich es versäumt habe, ein Sandwich zu machen. Die Zutaten sind bei dem Versuch aufgebraucht.

Zeichen
quelle
1
Ich habe darüber nachgedacht, aber es verstößt gegen das Liskov-Substitutionsprinzip und es ist nicht so gut, damit zu arbeiten, zB wenn Sie "1" eingeben. und die Autovervollständigung kommt, sehen Sie makeASandwich aber nicht contactAliens.
Codebreaker
-5

Was Sie tun, ist absolut legitim. Achten Sie nicht auf die Neinsager, die nur das Dogma wiederholen, weil sie es in einigen Büchern lesen. Das Dogma hat keinen Platz in der Technik.

Ich habe den gleichen Mechanismus ein paarmal angewendet und kann mit Sicherheit sagen, dass die Java-Laufzeit an mindestens einer mir in den Sinn gekommenen Stelle das Gleiche hätte tun können, um so die Leistung, Benutzerfreundlichkeit und Lesbarkeit von Code zu verbessern benutzt es.

Nehmen wir zum Beispiel java.lang.reflect.Member, welches die Basis von java.lang.reflect.Fieldund ist java.lang.reflect.Method. (Die tatsächliche Hierarchie ist etwas komplizierter, aber das ist irrelevant.) Felder und Methoden sind sehr unterschiedliche Tiere: Eines hat einen Wert, den Sie erhalten oder festlegen können, während das andere keinen solchen Wert hat, der jedoch aufgerufen werden kann eine Reihe von Parametern und es kann einen Wert zurückgeben. Felder und Methoden sind also beide Mitglieder, aber die Dinge, die Sie mit ihnen tun können, unterscheiden sich ungefähr so ​​stark voneinander wie das Herstellen von Sandwiches oder das Kontaktieren von Aliens.

Wenn wir nun Code schreiben, der Reflektion verwendet, haben wir sehr oft einen Memberin unseren Händen, und wir wissen, dass es entweder ein Methododer ein Field(oder selten etwas anderes) ist, und dennoch müssen wir alles Mühsame tun instanceof, um genau herauszufinden was es ist und dann müssen wir es besetzen, um einen richtigen Verweis darauf zu bekommen. (Und das ist nicht nur mühsam, sondern auch nicht sehr leistungsfähig.) Die MethodKlasse hätte das von Ihnen beschriebene Muster sehr einfach implementieren können, wodurch das Leben Tausender Programmierer erleichtert wurde.

Natürlich ist diese Technik nur in kleinen, genau definierten Hierarchien eng gekoppelter Klassen praktikabel, über die Sie auf Quellenebene verfügen (und die Sie immer haben werden): Sie möchten so etwas nicht tun, wenn Ihre Klassenhierarchie dies ist von Personen, die nicht die Freiheit haben, die Basisklasse umzugestalten, verlängert werden können.

So unterscheidet sich das, was ich getan habe, von dem, was Sie getan haben:

  • Die Basisklasse stellt eine Standardimplementierung für die gesamte asDerivedClass()Methodenfamilie bereit , wobei jede einzelne davon zurückgegeben wird null.

  • Jede abgeleitete Klasse überschreibt nur eine der asDerivedClass()Methoden und gibt thisstattdessen zurück null. Es setzt nichts von alledem außer Kraft und möchte auch nichts über sie wissen. Also werden keine IllegalStateExceptions geworfen.

  • Die Basisklasse stellt auch finalImplementierungen für die gesamte isDerivedClass()Methodenfamilie bereit , die wie folgt codiert sind: Auf return asDerivedClass() != null; diese Weise wird die Anzahl der Methoden, die von abgeleiteten Klassen überschrieben werden müssen, minimiert.

  • Ich habe @Deprecateddiesen Mechanismus nicht benutzt , weil ich nicht daran gedacht habe. Nun, da Sie mir die Idee gegeben haben, werde ich sie nutzen, danke!

In C # ist ein ähnlicher Mechanismus über das asSchlüsselwort integriert. In C # können Sie sagen, dass DerivedClass derivedInstance = baseInstance as DerivedClassSie einen Verweis auf a erhalten, DerivedClassob Sie baseInstancezu dieser Klasse gehörten oder nullnicht. Dies führt (theoretisch) zu einer besseren Leistung als isdas von cast verfolgte ( isist das zugegebenermaßen besser benannte C # -Schlüsselwort fürinstanceof ), aber der benutzerdefinierte Mechanismus, den wir von Hand erstellt haben, funktioniert noch besser: das Paar instanceof-und-cast-Operationen von Java ebenfalls Da der asOperator von C # nicht so schnell ist wie der einzelne virtuelle Methodenaufruf unseres benutzerdefinierten Ansatzes.

Ich mache hiermit den Vorschlag, dass diese Technik als solche deklariert werden sollte als Muster und einen schönen Namen dafür zu finden.

Danke für die Ablehnung!

Eine Zusammenfassung der Kontroverse, um Ihnen das Lesen der Kommentare zu ersparen:

Der Einwand der Leute scheint zu sein, dass das ursprüngliche Design falsch war, was bedeutet, dass Sie niemals sehr unterschiedliche Klassen haben sollten, die von einer gemeinsamen Basisklasse abgeleitet sind, oder dass selbst wenn Sie dies tun, der Code, der eine solche Hierarchie verwendet, niemals in der Lage sein sollte, zu haben eine Basisreferenz und müssen die abgeleitete Klasse herausfinden. Sie sagen daher, dass der von dieser Frage und meiner Antwort vorgeschlagene Mechanismus des Selbstwerfens, der die Verwendung des ursprünglichen Entwurfs verbessert, niemals an erster Stelle hätte notwendig sein dürfen. (Sie sagen eigentlich nichts über den Mechanismus des Selbstwerfens selbst aus, sondern beschweren sich nur über die Art der Entwürfe, auf die der Mechanismus angewendet werden soll.)

Allerdings oben ich in dem Beispiel hat bereits gezeigt , dass die Schöpfer der Java - Laufzeit in der Tat wählte genau so einen Entwurf für die java.lang.reflect.Member, Field, MethodHierarchie, und in den Kommentaren unten ich auch zeigen , dass die Schöpfer der C # Laufzeit unabhängig in einer angekommen Äquivalent Design für die System.Reflection.MemberInfo, FieldInfo,MethodInfo Hierarchie. Das sind also zwei verschiedene reale Szenarien, die jedem unter die Nase fallen und die nachweislich praktikable Lösungen für genau solche Entwürfe bieten.

Darauf laufen alle folgenden Kommentare hinaus. Der Selbstgussmechanismus wird kaum erwähnt.

Mike Nakis
quelle
13
Es ist legitim, wenn Sie sich selbst in eine Box verwandelt haben. Das ist das Problem bei vielen dieser Arten von Fragen. Menschen treffen Entscheidungen in anderen Bereichen des Designs, die diese Art von Hackish-Lösungen erzwingen, wenn die eigentliche Lösung darin besteht, herauszufinden, warum Sie überhaupt in diese Situation geraten sind. Ein anständiges Design wird Sie nicht hierher bringen. Ich schaue gerade nach einer 200K SLOC-Anwendung und sehe nirgendwo, dass wir dies tun mussten. Es ist also nicht nur etwas, was die Leute in einem Buch lesen. Nicht in solche Situationen zu geraten, ist einfach das Endergebnis der Befolgung bewährter Praktiken.
Dunk
3
@Dunk Ich habe gerade gezeigt, dass die Notwendigkeit eines solchen Mechanismus beispielsweise in der java.lang.reflection.MemberHierarchie besteht. Die Entwickler der Java-Laufzeitumgebung waren in einer solchen Situation, ich nehme an, Sie würden nicht sagen, dass sie sich selbst in eine Box verwandelt haben, oder beschuldigen Sie sie, keine Best Practices befolgt zu haben? Der Punkt, den ich hier anspreche, ist, dass, sobald Sie diese Art von Situation haben, dieser Mechanismus eine gute Möglichkeit ist, Dinge zu verbessern.
Mike Nakis
6
@MikeNakis Die Entwickler von Java haben viele schlechte Entscheidungen getroffen, so dass allein nicht viel aussagt. In jedem Fall ist es besser, einen Besucher zu verwenden, als einen Ad-hoc-Test durchzuführen, der dann abgegossen wird.
Doval
3
Darüber hinaus ist das Beispiel fehlerhaft. Es gibt eine MemberSchnittstelle, die jedoch nur zur Überprüfung der Zugriffsberechtigungen dient. In der Regel erhalten Sie eine Liste von Methoden oder Eigenschaften, die Sie direkt verwenden (ohne sie zu mischen, daher wird "no" List<Member>angezeigt), sodass Sie keine Umwandlung vornehmen müssen. Beachten Sie, dass Classkeine getMembers()Methode bereitgestellt wird. Natürlich könnte ein ahnungsloser Programmierer das erstellen List<Member>, aber das würde genauso wenig Sinn machen, als es zu einem zu machen List<Object>und einiges hinzuzufügen Integeroder es Stringzu ergänzen.
SJuan76
5
@ MikeNakis "Viele Leute machen es" und "Berühmte Person macht es" sind keine wirklichen Argumente. Antimuster sind sowohl schlechte Ideen als auch per Definition weit verbreitet, aber diese Ausreden würden ihre Verwendung rechtfertigen. Nennen Sie echte Gründe für das eine oder andere Design. Mein Problem beim Ad-hoc-Casting ist, dass jeder Benutzer Ihrer API jedes Mal das richtige Casting durchführen muss. Ein Besucher muss nur einmal korrekt sein. Das Casting hinter einigen Methoden zu verbergen und nullanstelle einer Ausnahme zurückzukehren, macht es nicht viel besser. Wenn alles andere gleich ist, ist der Besucher bei der gleichen Arbeit sicherer.
Doval