Metodo di estensione dell'interfaccia che restituisce il tipo di interfaccia in C #

6

La programmazione contro le interfacce è una buona pratica spesso ascoltata nello sviluppo del software. Insieme ai metodi di estensione, offre una grande funzionalità. Tuttavia, in C #, ci sono dei limiti. Dichiariamo un'interfaccia semplice per un punto 3D:

public interface IPoint3D
{
  double X { get; }
  double Y { get; }
  double Z { get; }
}

Posso aggiungere metodi di estensione per questo punto in una classe statica, ad esempio:

public static class Point3DExtensions
{
  public static double SumOfCoordinates(this IPoint3D point)
  {
    return point.X + point.Y + point.Z;
  }
}

Tuttavia, ciò che non posso codificare è la funzionalità che restituisce un'istanza dell'interfaccia stessa, come ad esempio:

public static class Point3DExtensions
{
  public static IPoint3D Add(this IPoint3D point1, IPoint3D point2)
  {
    return new IPoint3D(point1.X + point2.X, point1.Y + point2.Y, point1.Z + point2.Z);
  }
}

Il problema qui è la parte new IPoint3D , perché non riesco a creare un'istanza di un'interfaccia (è solo un contratto, non un'implementazione).

Come soluzione, vedrei due possibilità:

Nello stesso assembly dell'interfaccia, crea una classe interna che implementa l'interfaccia, ad esempio:

internal class SimplePoint : IPoint3D
{
  public double X { get; }
  public double Y { get; }
  public double Z { get; }

  public SimplePoint(double x, double y, double z)
  {
    this.X = x;
    this.Y = y;
    this.Z = z;
  }
}

Questa classe non verrà vista dall'assembly, ma ora il mio metodo di estensione Add può ancora avere IPoint3D come tipo restituito, ma in realtà restituisce un'istanza di SimplePoint . Nessuno dall'esterno dell'assembly sarà in grado di eseguire il cast dell'istanza restituita a SimplePoint e non è necessario.

La seconda opzione sarebbe quella di aggiungere qualcosa come un Create -Method all'interfaccia:

IPoint3D Create(double x, double y, double z);

Tuttavia, questo metodo non è statico, quindi dovrei invocarlo da qualsiasi istanza di IPoint3D , che è un po 'strano, e non ho sempre un'istanza del genere. Il problema qui è che i metodi statici non sono consentiti per le interfacce in C #. Se lo fossero, si potrebbe semplicemente aggiungere static IPoint3D Create(double x, double y, double z); o anche qualcosa come new(double x, double y, double z); (per un costruttore) all'interfaccia e usarlo per la creazione dell'oggetto.

Ora, le domande attuali:

  • Sto tentando di utilizzare in modo improprio le interfacce qui?
  • Mi sono perso ogni possibilità di ottenere il comportamento desiderato?
  • Quali lingue offrirebbero questa funzionalità desiderata?

Nota a margine: cosa vorrei avere ad es. un metodo di estensione per le classi che implementano un'interfaccia ICircle3D , che può calcolare un numero di punti IPoint3D su quelle cerchie.

    
posta Timitry 09.12.2016 - 13:38
fonte

2 risposte

11

Dubito che un'interfaccia come IPoint3D ti porti più vantaggi che problemi, quindi ecco il mio suggerimento: creare una classe concrete , immutable (quindi anche sealed) invecePoint3D. Quando si usano le interfacce, le operazioni geometriche punto / vettore possono essere implementate solo in un modo in cui le informazioni del tipo sottostante vengono perse, ma questo non è un problema del linguaggio di programmazione, ma un problema del dominio.

La ragione di ciò diventa più chiara quando provi a realizzare due diverse implementazioni di quell'interfaccia, diciamo un AutocadPoint3D (che rappresenta un punto in un modello CAD di autocad, con molti dati aggiuntivi come tag, colore, livello ecc. ) e un GoogleEarthPoint3D (che rappresenta una coordinata in un modello KML). Ora prova ad aggiungere punti di queste due classi con un metodo come

   IPoint3D Add(IPoint3D point1, IPoint3D point2)

Quindi quale dovrebbe essere il tipo sottostante del risultato? Un nuovo AutocadPoint3D , un GoogleEarthPoint3D o qualcosa di diverso (come SimplePoint )? L'unica cosa che probabilmente ha senso probabilmente sarebbe il SimplePoint , perché è qualcosa come il "più piccolo comune denominatore" di tutti i punti 3D. Inoltre, IPoint3D non può essere utilizzato per forzare le implementazioni sigillate, quindi il codice che si basa su IPoint3D non può essere sicuro che il tipo sottostante sia sempre immutabile. Ciò potrebbe risentire di effetti collaterali imprevisti quando l'implementatore dell'interfaccia non è molto attento.

Pensa al motivo per quale scopo di solito usi le interfacce. Lo scopo più frequente è probabilmente l'iniezione di dipendenza, specialmente per i test unitari. Ma raramente ha senso deridere un oggetto valore come Point3D . Immagina di dover testare una classe che ha bisogno di un IPoint3D , probabilmente inserirai un oggetto SimplePoint con alcuni valori predefiniti come dati di test - ma potresti anche usare un SimplePoint direttamente e salvare l'interfaccia. Quindi l'interfaccia aggiunge solo "rumore" al tuo codice, senza alcun beneficio reale.

    
risposta data 09.12.2016 - 14:37
fonte
3

Per prima cosa, ti preghiamo di non lasciare che questa risposta ti distolga dal consiglio di Doc Brown. Una classe come Point3D ha una caratteristica che lo rende leggermente diverso dagli altri oggetti di trasferimento dati: le applicazioni che utilizzano Point3D devono usarlo in genere in molte operazioni matematiche. Non è raro vedere che un'applicazione contiene migliaia di righe di codice che manipolano direttamente Point3D . Essendo un oggetto piccolo, la copia (e la conversione) è favorita dall'astrazione (di un'interfaccia).

Detto questo, il dilemma è una limitazione nota della versione corrente di C #, come è stato riconosciuto da Jon Skeet in questo risposta :

I've previously suggested that "static interfaces" could express this reasonably neatly. They'd only be useful for generic type constraints (I suspect, anyway) but then you could express:

  • Constructors with arbitrary parameters
  • Static methods and properties
  • Operators

Dato che lo standard C # sta iterando molto rapidamente negli ultimi anni, non sarei sorpreso se questo venisse in qualche modo aggiunto presto alla lingua.

(Edited)

Detto questo, i progettisti di C # dovevano usare un po 'di cautela, perché tradizionalmente C # è progettato per essere estremamente orientato alla riflessione, e c'è un megaton di codice legacy che dipende dalla riflessione che si interromperebbe se nuove funzionalità fossero introdotte senza attenzione considerazione.

Un tipico workaround è simile a questo. Nota che:

  • In genere, la funzione Create è separata dal contratto dati, perché il contratto dati non deve necessariamente essere generico (dipende ricorsivamente dal tipo concreto), ma la funzione Create deve utilizzare tale trucco.
  • Puoi vedere lo stesso trucco utilizzato in IComparable<T> ,
    come in
    public class Concrete : IComparable<Concrete> {...}
  • La creazione di un'istanza vuota inutile di SimplePoint prima di chiamare il suo metodo Create , poiché in return new T().Create(...) è uno sfortunato sovraccarico di questa soluzione alternativa.

Ancora una volta, ti preghiamo di non lasciare che questo esempio di codice ti distolga dal consiglio di Doc Brown. La soluzione alternativa è inutilmente complicata e non utile per Point3D .

public interface IPoint3DFactory<Impl>
{
    Impl Create(double x, double y, double z);
}

public static class IPoint3DExtensionMethods
{
    public static T Add<T>(this T left, T right)
        where T : IPoint3D, IPoint3DFactory<T>, new()
    {
        return new T().Create(left.X + right.X, left.Y + right.Y, left.Z + right.Z);
    }
}

public class SimplePoint : IPoint3D, IPoint3DFactory<SimplePoint>
{
    public double X { get; }
    public double Y { get; }
    public double Z { get; }

    public SimplePoint()
    {
    }

    public SimplePoint(double x, double y, double z)
    {
            X = x;
            Y = y;
            Z = z;
    }

    public SimplePoint Create(double x, double y, double z)
    {
        return new SimplePoint(x, y, z);
    }
}
    
risposta data 09.12.2016 - 15:52
fonte

Leggi altre domande sui tag