Architettura ingannevole in C # che implica un'operazione simmetrica e una classe di visualizzazione

1

Sto cercando di costruire un'architettura in C # e non riesco a trovare un modo per eseguire l'operazione. Fondamentalmente, c'è un insieme di classi A , B , C e D che ereditano da I . Ognuna di queste classi ha un'operazione Foo che opera su una delle altre classi e restituisce un oggetto di tipo R :

class A : I
{
    public R Foo(A a);
    public R Foo(B b);
    public R Foo(C c);
    public R Foo(D d);
}
class B : I
{
    public R Foo(A a);
    public R Foo(B b);
    public R Foo(C c);
    public R Foo(D d);
}
class C : I
{
    public R Foo(A a);
    public R Foo(B b);
    public R Foo(C c);
    public R Foo(D d);
}
class D : I
{
    public R Foo(A a);
    public R Foo(B b);
    public R Foo(C c);
    public R Foo(D d);
}

Questa operazione Foo è simmetrica (cioè a.Foo(b) == b.Foo(a) , d.Foo(a) == a.Foo(d) , ecc.). (Ovviamente, questo significa che dobbiamo solo definire Foo(A a) in A , Foo(A a) e Foo(B b) in B , e così via, ma tu hai l'idea).

Se introduciamo una nuova classe E , deve implementare Foo per tipi A , B , C , D e E .

class E : I
{
    public R Foo(A a);
    public R Foo(B b);
    public R Foo(C c);
    public R Foo(D d);
    public R Foo(E e);
}

Ovviamente non possiamo aggiungere esternamente un'implementazione per public R Foo(E e) in ciascuna delle classi A , B , C e D (beh, non senza metodi di estensione), ma poiché Foo è simmetrico, a.Foo(e) == e.Foo(a) , quindi abbiamo solo bisogno di implementare Foo(A a) in E .

Ora, la parte che rende questo per me veramente difficile, è che c'è anche un oggetto "viewer" V che contiene un elenco di I . V deve essere in grado di eseguire l'operazione Foo tra due I 's senza conoscendo i loro tipi effettivi. V sa solo che l'operazione Foo esiste tra due I 's. In teoria, questo è come apparirebbe:

class V {
    public R Foo(I i1, I i2)
    {
        // Check if i1 has an implementation for Foo taking
        // the type of i2.
        if (!i1.HasFoo(i2.GetType()))
        {
            // Do the same for i2 on i1.
            if (!i2.HasFoo(i1.GetType()))
                return null;
            return i2.Foo(i1);
        }
        return i1.Foo(i2);
    }
}

Ovviamente questo non funzionerebbe mai, per gli stessi motivi questo non funziona. Ci ho pensato per un po 'di tempo e non riesco a pensare a un'architettura che soddisfi queste esigenze in modo pulito. Ho ideato una serie di metodi contorti che abusano di tipi e schemi strategici e modelli di visitatori e dell'intero sh'bang, ma nulla che non sia soggetto a bug o eccessivamente complicato.

Come dovrei implementarlo, se è anche possibile?

Questa è l'applicazione reale, non astratta. È un motore di rilevamento delle collisioni, in cui le classi A , B , ecc. Sono diverse geometrie con proprietà fondamentalmente diverse (cerchio contro linea infinita vs poligono, ecc.). I diversi Foo s sono i diversi algoritmi che verificano se due forme geometriche si scontrano. V è un contenitore algoritmico per un gran numero di questi oggetti geometrici (un quadruplo o qualcosa del genere).

    
posta user3002473 28.06.2015 - 19:35
fonte

5 risposte

1

Sembra che il tuo metodo Foo non debba esistere nelle sottoclassi I .

Come hai detto, con il tuo approccio suggerito l'aggiunta di una nuova sottoclasse I viola il principio Open / Closed.

Invece, prova a spostare Foo in una nuova classe responsabile dell'operazione simmetrica

abstract class SymmetricalFoo<T,U>
    where T : I 
    where U : I
{
    public R Foo(T t, u u)
    {
        this.Bar(t, u);
    }
    public R Foo(U u, T u)
    {
        this.Bar(t, u);
    }
    protected abstract R Bar(T t, U u);
}

Implementa un% co_de concreto per ogni% di sottoclasse di SymmetricalFoo .

In questo modo quando aggiungi una nuova sottoclasse I non avrai bisogno di modificare le classi esistenti. Devi solo creare nuovi I s.

Come per la tua classe SymmetricalFoo . Questo va contro il mio odio generale per il modello ... ma penso che tu abbia bisogno di una Fabbrica astratta .

Passa due istanze di V sottoclassi alla fabbrica a ottieni il I appropriato come risultato.

Lascerò l'implementazione di quella factory (perché non mi piace scriverli!), ma probabilmente coinvolgerà la classe generica SymmetricalFoo a implementare un'interfaccia non generica SymmetricalFoo<T,U> .

    
risposta data 28.06.2015 - 20:28
fonte
4

Ciò che hai incontrato si chiama doppio invio e ciò che hai implementato è una forma di Pattern visore . Il problema che hai riscontrato è una delle limitazioni del pattern Visitor. Di solito devi scegliere tra ereditarietà e visitatore. Quando ne hai uno, rende l'altro più difficile. Il sistema di invio di tipo e metodo di C # non è abbastanza potente da supportare entrambi allo stesso tempo.

L'unica soluzione possibile è di rinunciare al controllo del compilatore se ogni coppia di possibilità è implementata e basta mantenere il dizionario dove i due tipi sono fondamentali e l'operazione è il valore. (è lo stesso di come lo state facendo nella vostra risposta, ma la vostra soluzione "nasconde" questo fatto). La fiducia, che tutte le permutazioni sono implementate, può essere riguadagnata se si scrive un test automatico, che verifica se tutte le permutazioni hanno un calcolo valido.

E direi che non è necessario unire i generici. Dovresti comunque convertire entrambi i valori in object tipo prima di passarli nella routine di doppia spedizione.

    
risposta data 29.06.2015 - 08:32
fonte
2

In realtà questo può essere risolto in modo relativamente semplice con il trucco di C # dell'utilizzo di dynamic per eseguire il doppio invio . Hai semplicemente bisogno di un metodo sulla classe base I come:

public R Foo(I i)
{
    return (i as dynamic).Foo(this as dynamic);
}

Il visualizzatore può chiamare questo, perché sa che ha un'istanza di I . Il trucco qui è che quando lanci in modalità dinamica, scegli sempre il sovraccarico appropriato per il tipo runtime dell'oggetto (vedi il link sopra se non è chiaro cosa significa).

Nota che questo risolve anche il tuo problema nell'aggiungere nuove classi. Supponiamo che tu abbia A , B , C , D e tu aggiungi E . E ha un override Foo esplicito per tutti i precedenti, ma nessuno di questi ha un override esplicito per E .

Ora fai:

I e == new E();
I a = new A();
a.Foo(e);

Questo chiamerà E.Foo(A a) . In alternativa, se lo fai:

e.Foo(a);

Questo chiamerà prima A.Foo(I i) (poiché non c'è A.Foo(E e) ). Quindi chiamerà E.Foo(A a) , che è lo stesso di sopra.

Come al solito con dynamic , i lati negativi sono:

  • Perdita di sicurezza del tipo
  • Prestazioni

Dato che lo stai utilizzando in tale un ambito specifico, direi che il primo punto è praticamente irrilevante qui. Non ti obbliga a usare dynamic da nessun'altra parte.

Quanto è importante la performance è da te trovare per il tuo codice specifico. Se questi metodi non vengono chiamati in un ciclo chiuso, è improbabile che importi, e se non sei sicuro, dovresti profilare.

Nota anche che se chiami questo per due classi in cui nessuno dei due può convertirsi all'altro, dovrai recitare all'infinito e traboccare lo stack!

    
risposta data 28.06.2015 - 22:15
fonte
0

Come dovresti implementarlo?

Dovresti adottare alcune convenzioni, come la classe fatta per ultime, ottenere l'implementazione del comportamento e l'altra semplicemente inoltra la chiamata. Idealmente, puoi implementarli come metodi statici in modo che un metodo e il suo mirror si trovino uno accanto all'altro nel codice.

Sì, questo significa che devi modificare il set quando sono espansi. Fastidioso, ma semplice da fare e improbabile che accada molto.

C # non può davvero forzare la simmetria in questo modo facilmente. La cosa migliore è semplicemente fare il codice e poi punire i programmatori che infrangono le regole.

    
risposta data 28.06.2015 - 20:36
fonte
-1

Mi sono appena reso conto che C # ha% tipi di% co_de!

interface I
{
    int Precedence { get; }
}

class A : I
{
    public int Precedence { get { return 0; } }
    public R Foo(A a) { return ...; }
}

class B : I
{
    public int Precedence { get { return 1; } }
    public R Foo(A a) { return ...; }
    public R Foo(B b) { return ...; }
}

class C : I
{
    public int Precedence { get { return 2; } }
    public R Foo(A a) { return ...; }
    public R Foo(B b) { return ...; }
    public R Foo(C c) { return ...; }
}

class D : I
{
    public int Precedence { get { return 3; } }
    public R Foo(A a) { return ...; }
    public R Foo(B b) { return ...; }
    public R Foo(C c) { return ...; }
    public R Foo(D d) { return ...; }
}

using Microsoft.CSharp.RuntimeBinder;
class V
{
    public R Foo(I i1, I i2)
    {
        try 
        {
            if (i1.Precedence >= i2.Precedence)
                return DynamicFoo(i1, i2);
            return DynamicFoo(i2, i1);
        }
        catch (RuntimeBinderException)
        {
            return null;
        }
    }

    private R DynamicFoo(dynamic a, dynamic b)
    {
        return a.Foo(b);
    }
}

Qui usiamo la convenzione per cui qualsiasi classe con una precedenza di dynamic deve implementare n per tutte le classi con precedenza minore o uguale a Foo .

I problemi con questa soluzione sono che se vuoi implementare una nuova classe n devi sapere qual è il precedente valore di precedenza più alto (in questo caso 3), e l'utente deve capire la convenzione di precedenza nel primo posto. Potresti anche sostenere che il rilevamento di un'eccezione è un metodo inefficiente di rilevamento quando non esiste E , ma sembra un'ottimizzazione prematura.

Un esempio di implementazione di Foo :

class E : I
{
    public int Precedence { get { return 4; } }
    public R Foo(A a) { return ...; }
    public R Foo(B b) { return ...; }
    public R Foo(C c) { return ...; }
    public R Foo(D d) { return ...; }
    public R Foo(E e) { return ...; }
}
    
risposta data 29.06.2015 - 05:08
fonte

Leggi altre domande sui tag