Das böse "interfaceof"

Ich spiel zurzeit ein bisschen mit einer PhysicsEngine rum (LiquidFun von Google um genau zu sein (Box2D Branch)) und es macht richtig Spaß, insbesondere wenn man es mal hingekriegt hat das sich alles so verhält wie es soll.
Da dachte ich mir ich füge noch ein bisschen “Spiel” zu dem kleinen Android Programm und dabei bin ich auf ein fundamentales Problem gestoßen…

LiquidFun verwaltet und berechnet alle Objekte die ich ihm gebe, wenn zwei Objekte zusammenstoßen bekomm ich auf meinem Listener die Methode “beginContact()” aufgerufen und die zwei Physikobjekte übergeben.
Soweit so gut, aber wie füge ich dem ganzen jetzt die Logik hinzu?

Weder ich kenne die Beiden Objekte die dort zusammengekracht sind, noch die Objekte kennen sich gegenseitig nebst der Info ihrer Masse, Position und Geschwindigkeit und was noch das hilft mir aber nicht weiter.
Die Frage ist doch: Ist das eine Kugel? Eine Einheit? Ein aufsammelbares Objekt? Welchem Spieler gehört dieses Objekt?

Panzer:

public void contactWith( Object object ) {
     if( object interfaceof Unit ) {
          Unit unit = (Unit) object;
          if( unit.getUser() != this.getUser() )
               this.attack( unit );
     
     } else if( object interfaceof Collectable ) {
          this.collect( (Collectable) object );

     } else if( object interfaceof Bullet ) {
          Bullet bullet = (Bullet) object;
          if( bullet.getUser() = this.getUser() )
               this.damage( bullet.getDamageOnHit() );

     etc....
}

Tja, das “böse” “interfaceof” wird hier verwendet, was in Java doch immer (meiner Erfahrung nach) als böses übel der Java Sprache genau wie static behandelt wird. “Wenn man interfaceof verwendet, dann hat man etwas grundlegend falsch gemacht”.
Wie würdet ihr das hier lösen? LiquidFun ist OpenSource, also ich kanns umschreiben und aus “Object” eine speziellere Klasse machen oder Generics verwenden, wobei die Generics Variante in diesem Fall ungeeignet ist weil fast jede Klasse betroffen wäre und selbst Klassen die das verwendete Generic garnicht kennen können müssten Klassen initialisieren die den Generictyp benötigen.
Das Generalproblem bleibt bestehen: Schlussendlich sollen Klassen über eine allgemeine Schnittstelle miteinander agieren, die alles sein könnten.

Ist das die Ausnahme?

Ich würde ein allgeneines Interface mit allen möglichen Interaktionen definieren, und die einzelnen Klassen implementieren davon die, die sinnvoll sind mit entsprechendem Code, alle anderen leer:```interface Collider{
/* overloaded mehods */
void doDamage(Avatar avatar);
void doDamage(Vehicle vehicle);
void doDamage(Building building);
void doDamage(Collider anythingThatDoesNotGetDamage);

boolen isCollectable();

boolean isSame(User user);

}
public void contactWith( Object object ) {
Collidable other = (Collidable)object;
if(!other.isSame(getUser()))
/* static binded overloaded method */
other.doDamage(this);
if(other.isCollectable())
inventory.add(other);

 etc....

}


Ist natürlich ungetestet...

bye
TT

Hmja, das gute alte instanceof. Es gibt Fälle, wo es angebracht ist. Aber die Fälle wo es NICHT angebracht ist, kann man wohl klassifizieren: Wenn der instanceof-Test verwendet wird, um das Verhalten des Objektes zu verändern, dann ist es NICHT angebracht, bzw. dann kann man meistens irgendwas Polymorphes tricksen. Wie Timothy_Truckle es schon angedeutet hat, bedeutet das meistens, dass man einen Umweg über das Objekt selbst geht, und dort das macht, was vom Typ abhängt (oder auch um von dort aus wieder zurückzummen, bereichtert um das Wissen, was für ein Objekt es denn ist…)

Tatsächlich habe ich darüber auch schonmal im theoretisch-abstrakten Sinne nachgedacht: „Kann man ALLES programmieren/modellieren, ohne irgendwelche Typabfragen zu machen?“. Aber das tatsächlich auszudifferenzieren ist schwierig. Ketzerisch könnte man sagen: Mit Polymophie überläßt man die „Typabfrage“ einfach der umgebenden Infrastruktur… :rolleyes: Etwas platt formuliert: Wenn man versucht, das „instanceof“ zu vermeiden, und deswegen dann eine enum Type { UNIT, BULLET, COLLECTABLE; } einführt, bringt das nix :wink: Aber die Grenzen sind teilweise nicht so klar. Ein Grenzfall ist das von TT angedeutete isCollectable(). Wie wäre es noch mit isUnit() und isBullet()? Dann wären alle Probleme gelöst :smiley:

Intressanterweise wird in Wikipedia: Double dispatch genau das Beispiel mit den Kollisionen verwendet.

Leider habe ich weder zu LiquidFun noch zu Box2D auf die schnelle eine Online-JavaDoc gefunden - nur eine, die mal jemand erstellt hat, und von der nicht klar ist, wie akutell sie ist. Wenn ich das richtig sehe, bekommt man ja ein Contact und da sind die beiden Objekte drin (jeweils als „Fixture“?!). Also, selbst wenn die eigenen Spielobjekte jeweils ein passendes Interface implementieren, wird man zumindest um einen Test nicht drumrumkommen, ob die Kollidierenden Objekte von dieser (eigenen) Basisklasse erben:

Fixture fA = contact.getFixtureA();
Fixture fB = contact.getFixtureB();
if (fA instanceof MyGameObject && fB instanceof MyGameObject)
{
    MyGameObject mA = (MyGameObject)fA;
    MyGameObject mB = (MyGameObject)fB;
    handle(mA, mB);
}

In der handle-Methode kann dann aber die weitere Unterscheidung so gehandelt werden, wie TT es angedeutet hat.

package bytewelt.collide;

public class CollideExample
{
    public static void main(String[] args)
    {
        MyGameObject m[] =
        { 
            new Unit(), 
            new Bullet(), 
            new Collectable()
        };

        for (int i = 0; i < m.length; i++)
        {
            for (int j = i; j < m.length; j++)
            {
                handle(m**, m[j]);
            }
        }
    }

    static void handle(MyGameObject a, MyGameObject b)
    {
        a.collideWith(b);
    }

}

abstract class MyGameObject
{
    abstract void collideWith(MyGameObject other);

    abstract void collideWith(Unit other);

    abstract void collideWith(Bullet other);

    abstract void collideWith(Collectable other);
}

class Unit extends MyGameObject
{
    @Override
    void collideWith(MyGameObject other)
    {
        other.collideWith(this);
    }

    @Override
    void collideWith(Unit other)
    {
        System.out.println("Unit and Unit");
    }

    @Override
    void collideWith(Bullet other)
    {
        System.out.println("Unit and Bullet");
    }

    @Override
    void collideWith(Collectable other)
    {
        System.out.println("Unit and Collectable");
    }
}

class Bullet extends MyGameObject
{
    @Override
    void collideWith(MyGameObject other)
    {
        other.collideWith(this);
    }

    @Override
    void collideWith(Unit other)
    {
        System.out.println("Bullet and Unit");
    }

    @Override
    void collideWith(Bullet other)
    {
        System.out.println("Bullet and Bullet");
    }

    @Override
    void collideWith(Collectable other)
    {
        System.out.println("Bullet and Collectable");
    }
}

class Collectable extends MyGameObject
{
    @Override
    void collideWith(MyGameObject other)
    {
        other.collideWith(this);
    }

    @Override
    void collideWith(Unit other)
    {
        System.out.println("Collectable and Unit");
    }

    @Override
    void collideWith(Bullet other)
    {
        System.out.println("Collectable and Bullet");
    }

    @Override
    void collideWith(Collectable other)
    {
        System.out.println("Collectable and Collectable");
    }
}


Unit and Unit
Bullet and Unit
Collectable and Unit
Bullet and Bullet
Collectable and Bullet
Collectable and Collectable

Natürlich wirft das Fragen bezüglich der Erweiterbarkeit auf. Wenn ein neuer Typ dazukommt, wird das aufwändig…

[quote=Marco13]@Override
void collideWith(MyGameObject other)
{
other.collideWith(this);
}[/quote]hier muss man aufpassen, dass kann schnell zur Endlosschleife werden…

[quote=Marco13;126648]Natürlich wirft das Fragen bezüglich der Erweiterbarkeit auf. Wenn ein neuer Typ dazukommt, wird das aufwändig…[/quote]Einen neuen Typ einführen ist immer aufwändig.
Und hier ist es “nur” tipp-Aufwand (bei dem also tatsächlich neuer Code entsteht), den man sogar bequem von der IDE erledigen lassen könnte.
Der Vorteil hier ist, dass man über die neue abstrakte Methode vom Compiler (oder, wenn man die IDE hat arbeiten lassen vom SCM) gesagt bekommt, wo noch was zu ändern ist.
Bei einem “herkömmlichen” Ansatz muss man das mit richtig viel unproduktivem Aufwand austesten…

bye
TT

@Marco13
Finde deinen Vorschlag gut :slight_smile:

Events/Listener waeren ein moeglicher Ausweg aus einer statischen Vererbungshierarchie.

Man kann vermutlich einige Fälle auf den gleichen zurückführen. Also, vermutlich ist oft egal, in welcher Reihenfolge die Objekte stehen. Und Falls bei der Kollision etwas “kompliziertes” gemacht werden muss, kann man das ja recht leicht auslagern

class Unit {
    void collideWith(Collectable other) {
       Utilities.handleUnitCollectable(this, other);
    }
}
class Collectable {
    void collideWith(Unit other) {
       Utilities.handleUnitCollectable(other, this);
    }
}

Aber gerade, wenn es um “viele” Objektarten geht, könnte man sich irgendwelche Verallgemeinerungen vorstellen. Man wird nicht drumrumkommen, die ganze Matrix abzudecken, aber … wenn es jetzt wirklich 10 Klassen gibt, und jede davon 10 Methoden bräuchte, und morgen vielleicht noch 5 dazukommen, die aber nur bei der Kollision mit 2 der anderen relevant ist, könnte man auch grob sowas machen wie das hier: Auf Basis der Kombination der Klassen der beiden Objekte einen speziellen “Handler” raussuchen

package bytewelt.collide.generic;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;


public class CollideExampleGeneric
{
    public static void main(String[] args)
    {
        
        CollisionHandlers ch = new CollisionHandlers();
        ch.register(Unit.class, Bullet.class, new CollisionHandlerUnitBullet());
        ch.register(Unit.class, Unit.class, new CollisionHandlerUnitUnit());
        
        MyGameObject a = new Unit();
        MyGameObject b = new Bullet();
        
        ch.handle(a, a);
        ch.handle(a, b);
        ch.handle(b, a);
        ch.handle(b, b);
    }
}

class CollisionHandlers
{
    private final Map<Tuple2<?,?>, CollisionHandler<?,?>> map = 
        new LinkedHashMap<>();
    
    <A, B> void register(Class<A> ca, Class<B> cb, CollisionHandler<A, B> ch)
    {
        map.put(Tuple2.of(ca, cb), ch);
    }
    
    
    void handle(Object a, Object b)
    {
        Tuple2<?, ?> key0 = Tuple2.of(a.getClass(), b.getClass());
        Tuple2<?, ?> key1 = Tuple2.of(b.getClass(), a.getClass());
        @SuppressWarnings("unchecked")
        CollisionHandler<Object, Object> ch0 = 
            (CollisionHandler<Object, Object>) map.get(key0);
        @SuppressWarnings("unchecked")
        CollisionHandler<Object, Object> ch1 = 
            (CollisionHandler<Object, Object>) map.get(key1);
        if (ch0 != null)
        {
            ch0.handle(a, b);
        }
        else if (ch1 != null)
        {
            ch1.handle(b, a);
        }
        else
        {
            System.out.println("No handler for "+key0);
        }
    }
    
}


interface CollisionHandler<A, B>
{
    void handle(A a, B b);
}

class CollisionHandlerUnitUnit implements CollisionHandler<Unit, Unit>
{
    @Override
    public void handle(Unit a, Unit b)
    {
        System.out.println("Unit and Unit");
    }
}
class CollisionHandlerUnitBullet implements CollisionHandler<Unit, Bullet>
{
    @Override
    public void handle(Unit a, Bullet b)
    {
        System.out.println("Unit and Bullet");
    }
}

abstract class MyGameObject
{
}

class Unit extends MyGameObject
{
}

class Bullet extends MyGameObject
{
}

class Collectable extends MyGameObject
{
}


final class Tuple2<S, T>
{
    /**
     * Creates a new 2-tuple consisting of the given elements
     * 
     * @param <S> The type of the first element
     * @param <SS> A subtype of the first element type
     * @param <T> The type of the second element
     * @param <TT> A subtype of the second element type
     * @param s The first element
     * @param t The second element
     * @return The new tuple
     */
    public static <S, SS extends S, T, TT extends T> Tuple2<S, T> of(SS s, TT t)
    {
        return new Tuple2<S, T>(s, t);
    }
    
    /**
     * The first element
     */
    private final S first;
    
    /**
     * The second element
     */
    private final T second;
    
    /**
     * Creates a new tuple consisting of the given element
     * 
     * @param first The first element
     * @param second The second element
     */
    private Tuple2(S first, T second)
    {
        this.first = first;
        this.second = second;
    }
    
    /**
     * Returns the first element of this tuple
     * 
     * @return The first element of this tuple
     */
    public S getFirst()
    {
        return first;
    }
    
    /**
     * Returns the second element of this tuple
     * 
     * @return The second element of this tuple
     */
    public T getSecond()
    {
        return second;
    }
    
    @Override
    public String toString()
    {
        return "("+first+","+second+")";
    }
    
    @Override
    public int hashCode()
    {
        final int prime = 31;
        int result = 1;
        result = prime * result + Objects.hashCode(first);
        result = prime * result + Objects.hashCode(second);
        return result;
    }

    @Override
    public boolean equals(Object object)
    {
        if (this == object)
        {
            return true;
        }
        if (object == null)
        {
            return false;
        }
        if (getClass() != object.getClass())
        {
            return false;
        }
        Tuple2<?,?> other = (Tuple2<?,?>) object;
        if (!Objects.equals(first, other.first))
        {
            return false;
        }
        if (!Objects.equals(second, other.second))
        {
            return false;
        }
        return true;
    }
    
}

Unit and Unit
Unit and Bullet
Unit and Bullet
No handler for (class bytewelt.collide.generic.Bullet,class bytewelt.collide.generic.Bullet)

(Wobei man sich da aussuchen kann, ob die Reihenfolge eine Rolle spielen soll, oder nicht). Die Vor- und Nachteile muss man abwägen. Interessant daran könnte sein, dass man zur Laufzeit das Verhalten ändern könnte, und das es leichter erweiterbar ist. Aber das ist nur spontan hingeschrieben.

Okay das ist kurz gesagt genial!
@Marco13
Ich will ganz ehrlich sein, dass ich deine weitergehenden Überlegungen (letzter Beitrag) nicht ganz verstehe was du dort genau machst. Generics sind jetzt auch nicht viel anderst wie wenn man manuell interfaceof und casts verwendet.
Aber das Problem bleibt bestehen, ich habs schon mehrmals versucht Generics in die Bibliothek zu kriegen. Das ist ein C++ Projekt nach Java portiert aber entsprechend ist die Architektur „pseudo-objektorientiert“ aufgebaut worden und… chaos.

Wie bereits von dir selber angedeutet fängt man jetzt an in das allgemeine Interface, für jedes Objekt, Methoden reinzuschaufeln und schlussendlich wird es Objekte geben die diese Methoden garnicht verwenden, soll heißen man hat vielleicht 19 leere Implementierungen und die 20. Methode wird verwendet und das für jedes einzelne Objekt - sammelt sich ganz schön viel Müll an.
Besser wäre es natürlich wenn jedes Objekt für sich selbst soviele Methoden überladen könnte wie gebraucht werden, immerhin soll es so erweiterbar und abstrakt wie möglich gehalten werden um ganz leicht (ohne etwas am Kern zu ändern) möglichst unendlich viele Möglichkeiten einbauen zu können. Die Physikengine jedesmal umzuschreiben… jooooa :smiley:

Aber allgemein finde ich das eine klasse Idee! Bin begeistert von den Antworten.

Jetzt ganz isoliert und ohne jede persönliche Implikationen betrachtet: Das ist der dümmste Satz, den ich seit geraumer Zeit gehört habe.

Generics sind Bestandteile des Typsystems, sie dienen erst einmal vordringlich der besseren **Beschreibung **deiner Datenstrukturen. instanceof und insbesondere Casts dienen der **Aushebelung **des Typsystems, also ziemlich genau dem Gegenteil. Wenn der Compiler Generics sieht und für OK befindet, kannst du dir bis auf ganz seltene, exotische Ausnahmefälle sicher sein, dass das zumindest typmäßig in Ordnung geht, was du da vorhast. Mit Casts sagst du dagegen, dass du es besser weißt als der Compiler, und ihn (besser gesagt seine Typprüfung) bewußt umgehen willst. Statt Compilierfehler zu verursachen explodiert dann dein Code zur Laufzeit mit einer ClassCastException.

Natürlich gibt es Fälle, in denen ein Cast unumgänglich ist. Aber das sollte der letzte Notnagel sein, wenn alle anderen Möglichkeiten (wie etwa Generics) nicht greifen, oder wirklich nicht praktikabel sind.

Die Diskussion erinnert mich an die Dispatch-Methoden aus Xtend. Wobei das auch nur syntaktischer Zucker um instanceOf ist. Aber vielleicht ist die Erklärung wann man das braucht hier ganz nützlich.

Dem übrigen stimme ich zwar zu, aber… ein paar Punkte zu obigem:

  • Den konzeptuellen Unterschied zwischen einem „sichtbaren“ instanceof und dem „unsichtbaren“, das die runtime für einen erledigt, kann man in mancher Hinsicht nur schwer erklären
  • Gerade du solltest wissen, dass das Java Typsystem auch seine Grenzen hat. Manchmal kann man mit den beschränkten Mitteln von Interfaces und Generics bestimmte Konstrukte einfach nicht abbilden
  • Ganz so dumm, aus akademischer Sicht, ist das gar nicht: The Black Magic of (Java) Method Dispatch (zumindest ganz interessant)

@TMII Die Formulierung „ich habs schon mehrmals versucht Generics in die Bibliothek zu kriegen“ kann man mit viel gutem Willen lesen, aber sie klingt erstmal komisch. Wenn du Generics willst, schreib’ einfach <FOO, BAR, SMURF> vor eine Methode, dann hast du sie :wink: Also, die sollten ja kein Selbstzweck sein. Gerade ein Problem, wie das, um das es ursprünglich ging, ist mit Generics nicht in aller Tiefe zu lösen. Und ich war auch schon gelegentlich an dem Punkt, wo ich irgendwelche Node<? extends Node<? super B, ? extends Node<?>, ? super C>-Konstrukte hatte, und mir dann dachte: Sche!ß drauf, ein Node und der passende unchecked cast an der richtigen Stelle tut’s auch. Das ganze soll ja auch benutzbar bleiben…

Ich weiß (zumindest in groben Zügen), wie Generics in Java **implementiert **sind - Brückenmethoden, Casts u.s.w.

Aber das sind eben Implementationsdetails, die für die Verständnis, was Generics eigentlich sind, warum man sie braucht und wie man sie verwendet, nicht zwingend notwendig sind. Wichtig ist, dass der Compiler die Konstrukte prüft, bevor er sie irgendwie zu Bytecode zermanscht, und das ist der entscheidende Unterschied zu einem „manuellen“ Cast.

Gerade du solltest wissen, dass das Java Typsystem auch seine Grenzen hat.

Auch das schrub ich: „Natürlich gibt es Fälle, in denen ein Cast unumgänglich ist.“

Über die Grenzen von Javas Typsystem habe ich oft genug gejammert, ich kenne sie ziemlich gut. Aber das ist kein Grund, wie der TO gleich zum Brecheisen zu greifen, nur weil einem gerade der Schraubenzieher zu kompliziert vorkommt.

Um noch etwas Konstruktives beizutragen, hier ein Java-Beispiel für Double Dispatch: Visitor Design Pattern in Java: Double dispatch (within a single hierarchy)