Devo usare un'interfaccia quando i metodi sono solo simili?

1

Mi è stata posta l'idea di creare un oggetto che controlli se un punto entrerà in collisione con una linea:

public class PointAndLineSegmentCollisionDetector {
    public void Collides(Point p, LineSegment s) {
        // ...
    }
}

Questo mi ha fatto pensare che se avessi deciso di creare un oggetto Box , avrei bisogno di un PointAndBoxCollisionDetector e di un LineSegmentAndBoxCollisionDetector . Potrei persino rendermi conto che dovrei avere un BoxAndBoxCollisionDetector e un LineSegmentAndLineSegmentCollisionDetector . E, quando aggiungo nuovi oggetti che possono scontrarsi, avrei bisogno di aggiungerne altri ancora.

Tuttavia, tutti hanno un metodo Collides , quindi tutto quello che ho imparato sull'astrazione mi sta dicendo, "Crea un'interfaccia".

public interface CollisionDetector {
    public void Collides(Spatial s1, Spatial s2);
}

Ma ora ho una funzione che rileva solo una classe astratta o un'interfaccia che viene utilizzata da Punto , LineSegment , Box , ecc. Quindi, se l'ho fatto, allora ogni implementazione dovrebbe eseguire un controllo di tipo per assicurarsi che i tipi siano di tipo appropriato perché l'algoritmo di collisione è diverso per ogni tipo di corrispondenza.

Un'altra soluzione potrebbe essere questa:

public class CollisionDetector {
    public void Collides(Point p, LineSegment s) { ... }
    public void Collides(LineSegment s, Box b) { ... }
    public void Collides(Point p, Box b) { ... }
    // ...
}

Ma questo potrebbe finire per essere una classe enorme che sembra poco pratica, anche se sarebbe semplice in quanto è solo un insieme di metodi Collide .

Questo è simile alla classe Converti di C #. Che è bello perché è grande, ma è semplice capire come funziona. Questa sembra essere la soluzione migliore, ma ho pensato che avrei dovuto aprirlo per la discussione come wiki per ottenere altre opinioni.

    
posta Joshua Harris 11.11.2012 - 07:14
fonte

3 risposte

3

Un'interfaccia generica si adatterebbe bene qui

public interface ICollisionDetector<TFirst, TSecond>
{
    bool Collides(TFirst first, TSecond second);
}

public class PointAndBoxCollisionDetector : ICollisionDetector<Point, Box>
{
    public bool Collides(Point first, Box second)
    {
        // Collision logic
    }
}

Puoi usare un metodo FactoryMethod per generare il tuo rivelatore, in base ai tipi dei due oggetti che hai.

public class CollisionDetectorFactory
{
    public ICollisionDetector<TFirst, TSecond> Create<TFirst, TSecond>()
    {
         // Here you can have a giant switch/case, or 
         // you can map using reflection, or you can get
         // from an IoC Container/Service Locator.
    }
}
    
risposta data 11.11.2012 - 08:31
fonte
3

Teoria

Si dovrebbe implementare un'interfaccia comune quando:

  • Gli oggetti sono simili e hanno una logica simile, e:

  • La logica condivisa viene utilizzata in altri luoghi in cui la distinzione tra i tipi non è richiesta.

Ad esempio, non si dovrebbe implementare un'interfaccia comune per Dog: Animal e RSS , solo perché entrambi hanno un metodo comune Feed . Non solo la logica dietro questi due metodi è diversa ( new Dog(...).Feed() lancerebbe un robot che darà del cibo a un cane, mentre new RSS(...).Feed() semplicemente scaricherà l'RSS più recente dal server), ma non riesci a immaginare un comune Feeder che sarebbe responsabile del controllo del cibo per cani e durante il download degli ultimi feed RSS.

D'altro canto, sarebbe conveniente avere un'interfaccia comune sia per new Dog(...).Feed() che new Goldfish(...).Feed() , perché mentre l'implementazione sarebbe diversa, dal momento che il robot non dovrebbe mettere una ciotola di carne in un acquario , l'interfaccia comune consente comunque ai chiamanti di non fare alcuna differenza tra gli animali:

var myPets = new [] { (IAnimal)new Dog(...), new Goldfish(...) };
...
new FeedingEngine().FeedPets(myPets);

public class FeedingEngine
{
    public void FeedPets(IEnumerable<IAnimal> pets)
    {
        foreach (var pet in pets)
        {
            if (pet.Status == AnimalStatus.Alive && pet.IsPotentiallyHungry)
            {
                pet.Feed();
            }
        }
    }
}

Il tuo caso

Entrambe le tue proposte sono corrette. Dal punto di vista del consumatore di ICollisionDetector , tutto quello che devo sapere è che posso passare qualsiasi coppia di oggetti spaziali e ottenere il risultato della collisione, non importa come.

C'è una leggera differenza tra questi approcci: è il fatto che la prima proposta richiede di ricontrollare se tutti gli oggetti che ereditano ISpatial sono gestiti correttamente dal rilevatore di collisione. Può essere soggetto a errore in seguito: se, lavorando sul tuo codice in un secondo momento, un altro sviluppatore implementa un altro oggetto territoriale, diciamo Sphere: ISpatial , ma dimentica di implementare la logica di collisione, Collides(sphere, box) fallirà e sarebbe difficile capire perché sta fallendo, specialmente se il rilevatore di collisione è usato indirettamente, come attraverso il motore di gioco.

Il secondo approccio è un opt-in. Se il rilevatore di collisione non ha un Collides(Sphere s, Box b) , nessuno potrebbe chiamare tale conversione. Detto questo, questo stesso crea un altro problema: non sarai in grado di usare generici o astrazioni di alto livello. Ad esempio:

var box = new Box(...);
var torus = new Torus(...);

var engine = new GameEngine();
engine.Show(new [] { box, torus });
engine.StartColliding(); // Moves objects to the center of the map and stops on collision.

è facile da implementare con la tua prima proposta se sia Box che Torus implementano ISpatial , ma sarebbe estremamente difficile da fare con la tua seconda proposta.

Per poter utilizzare la tua prima proposta riducendo il problema che ho notato sopra, ecco una terza proposta:

public interface ISpatial
{
    bool IsColliding(ISpatial obj);
}

public class CollisionDetector
{
    public bool IsColliding(ISpatial s1, ISpatial s2)
    {
        return s1.IsColliding(s2);
    }
}

public class Torus : ISpatial
{
    public bool IsColliding(ISpatial obj)
    {
        // Check that obj is not null.

        // Do the collision logic for known types.
        var box = obj as Box;
        if (box != null)
        {
            // Do the collision logic.
            return result;
        }

        return obj.IsColliding(this);
    }
}

public class Cube : ISpatial
{
    public bool IsColliding(ISpatial obj)
    {
        // Check that obj is not null.

        // Do the collision logic for known types.

        return obj.IsColliding(this);
    }
}

Poiché la classe cubo viene creata per prima, non è a conoscenza del toro; invece di lanciare un NotImplementedException , semplicemente delega la logica di rilevamento collisione al toro, che è ben consapevole dell'oggetto cubo, poiché la classe torus era la seconda da creare.

Nota: ovviamente, devi ancora gestire le situazioni in cui né il primo né il secondo oggetto sono a conoscenza l'uno dell'altro (ad esempio se due oggetti sono stati creati da due sviluppatori diversi nello stesso momento). Il codice sopra riportato sarà semplicemente overflow e crash, ma puoi facilmente modificarlo per generare NotImplementedException .

    
risposta data 11.11.2012 - 08:10
fonte
1

Parlando in modo programmatico , la soluzione di pdr offre il miglior design, ma avrei suggerito prima di usare un contenitore IoC (configurato per restituire istanze singole), quindi usando la riflessione se non è possibile e infine utilizzando un interruttore / caso.

Ora matematicamente parlando , penso che ci siano forse soluzioni più ottimali per questo problema. Ad esempio, se il tuo obiettivo è in realtà solo rilevare la collisione tra diversi oggetti in movimento e essere in grado di risolvere queste collisioni, ad esempio in un gioco come "uccelli arrabbiati", ci sono molti algoritmi ottimali.

Suggerirei che potresti esporre il problema che stai cercando di risolvere sul link Poiché per me è più un problema di programmazione matematica / fisica che un problema di programmazione "puro".

Sarebbe un peccato progettare una soluzione complessa orientata agli oggetti mentre esiste forse una soluzione matematica più semplice (e con prestazioni migliori).

    
risposta data 11.11.2012 - 15:39
fonte

Leggi altre domande sui tag