L'uso delle interfacce per i tipi di dati è un anti-pattern?


9

Supponiamo di avere varie entità nel mio modello (usando EF), ad esempio Utente, Prodotto, Fattura e Ordine.

Sto scrivendo un controllo utente in grado di stampare i riepiloghi degli oggetti entità nella mia applicazione in cui le entità appartengono a un set prestabilito, in questo caso dico che i riepiloghi dell'utente e del prodotto possono essere riassunti.

I riepiloghi avranno tutti solo un ID e una descrizione, quindi creo una semplice interfaccia per questo:

 public interface ISummarizableEntity {     
       public string ID { get; }    
       public string Description { get; } 
 }

Quindi, per le entità in questione, creo una classe parziale che implementa questa interfaccia:

public partial class User : ISummarizableEntity
{
    public string ID
    {
        get{ return UserID.ToString(); }
    }

    public string Description 
    {
        get{ return String.Format("{0} {1} is from {2} and is {3} years old", FirstName, LastName, Country, Age); }
    }
}

public partial class Product: ISummarizableEntity
{
    public string ID
    {
        get{ return ProductID.ToString(); }
    }

    public string Description 
    {
        get{ return String.Format("{0} weighs {1}{2} and belongs in the {3} department", ProductName, WeightValue, WeightUnit, Department); }
    }
}

In questo modo il mio controllo utente / vista parziale può semplicemente legarsi a qualsiasi raccolta di ISummarizableEntity e non deve assolutamente interessarsi alla fonte. Mi è stato detto che le interfacce non dovrebbero essere usate come tipi di dati, ma non ho ottenuto più informazioni di così. Per quanto posso vedere, anche se le interfacce normalmente descrivono il comportamento, il solo uso delle proprietà non è un anti-modello in sé, poiché le proprietà sono comunque solo zucchero sintattico per getter / setter.

Potrei creare un tipo di dati e una mappa concreti dalle entità a quello ma non ne vedo il vantaggio. Potrei fare in modo che gli oggetti entità ereditino da una classe astratta e quindi definire le proprietà, ma poi sto bloccando le entità in modo da non poterle più utilizzare poiché non possiamo avere l'ereditarietà multipla. Sono anche aperto ad avere qualsiasi oggetto ISummarizableEntity se volessi (ovviamente rinominerei l'interfaccia)

La soluzione che sto usando nella mia mente è mantenibile, estensibile, testabile e abbastanza robusta. Riesci a vedere l'anti-pattern qui?


C'è qualche motivo per cui preferisci questo rispetto ad avere qualcosa di simile EntitySummary, Usere Productognuno con un metodo simile public EntitySummary GetSummary()?
Ben Aaronson,

@Ben, suggerisci un'opzione valida. Ma richiederebbe comunque la definizione di un'interfaccia che consenta al chiamante di sapere che può aspettarsi che un oggetto abbia un metodo GetSummary (). È essenzialmente lo stesso design con un ulteriore livello di modularità nell'implementazione. Forse anche una buona idea se il sommario deve vivere da solo (anche se brevemente) separato dalla sua fonte.
Kent A.

@KentAnderson Sono d'accordo. Potrebbe essere o meno una buona idea, a seconda dell'interfaccia generale di queste classi e di come vengono utilizzati i riepiloghi.
Ben Aaronson,

Risposte:


17

Le interfacce non descrivono il comportamento. Al contrario, a volte.

Le interfacce descrivono i contratti, ad esempio "se devo offrire questo oggetto a qualsiasi metodo che accetta un ISummarizableEntity, questo oggetto deve essere un'entità in grado di riassumere se stesso" - nel tuo caso, definito come in grado di restituire un ID stringa e una descrizione stringa.

Questo è un uso perfetto delle interfacce. Nessun anti-pattern qui.


2
"Le interfacce non descrivono il comportamento." In che modo "riassumere se stesso" non è un comportamento?
Doval

2
@ThomasStringer, l'eredità, da un punto di vista purista OO, implica un antenato comune (ad esempio, un quadrato e un cerchio sono entrambe forme ). Nell'esempio di OP, un utente e un prodotto non condividono alcun lignaggio comune ragionevole. L'ereditarietà in questo caso sarebbe un chiaro modello anti-modello.
Kent A.

2
@Doval: immagino che il nome dell'interfaccia possa descrivere il comportamento previsto. Ma non è necessario; l'interfaccia potrebbe ugualmente essere denominata IHasIdAndDescription e la risposta sarebbe la stessa. L'interfaccia stessa non descrive il comportamento, descrive le aspettative.
pdr

2
@pdr Se si invia 20 V tramite un jack per cuffie, accadranno cose brutte. La forma non è abbastanza; c'è un'aspettativa molto reale e molto importante su quale tipo di segnale provenga da quella spina. Questo è esattamente il motivo per cui fingere che alle interfacce non siano associate specifiche di comportamento è sbagliato. Cosa puoi fare con un Listche non si comporta come un elenco?
Doval

3
Una presa elettrica con l'interfaccia appropriata può adattarsi alla presa, ma ciò non significa che condurrà l'elettricità (il comportamento desiderato).
JeffO

5

Hai scelto il percorso migliore per questo progetto perché stai definendo un tipo specifico di comportamento che sarà richiesto a più, diversi tipi di oggetti. L'ereditarietà in questo caso implicherebbe una relazione comune tra le classi che in realtà non esiste. In questo caso, la componibilità è preferita rispetto all'eredità.


3
Le interfacce non hanno nulla a che fare con l'eredità.
DougM,

1
@DougM, forse non l'ho detto bene, ma sono abbastanza sicuro che siamo d'accordo.
Kent A.

1

Le interfacce che portano solo proprietà dovrebbero essere evitate poiché:

  • offusca l'intento: hai solo bisogno di un contenitore di dati
  • incoraggia l'eredità: probabilità che qualcuno mescoli preoccupazioni in futuro
  • impedisce la serializzazione

Qui stai mescolando due preoccupazioni:

  • riepilogo come dati
  • riassunto come contratto

Un riepilogo è composto da due stringhe: un ID e una descrizione. Questi sono dati semplici:

public class Summary {
    private readonly string id;
    private readonly string description;
    public Summary(string id, string description) {
        this.id = id;
        this.description = description;
    }
    public string Id { get { return id; } }
    public string Description { get { return description; } }
}

Ora che hai definito quale riepilogo vuoi definire un contratto:

public interface ISummarizableEntity {
    public Summary GenerateSummary();
}

Si noti che l'uso dell'intelligenza nei getter è un anti-schema e dovrebbe essere evitato: dovrebbe invece trovarsi nelle funzioni. Ecco come appaiono le implementazioni:

public partial class User : ISummarizableEntity {
    public Summary GenerateSummary() {
        var id = UserID.ToString();
        var description = String.Format("{0} {1} is from {2} and is {3} years old", FirstName, LastName, Country, Age);
        return new Summary(id,description);
    }
}

public partial class Product : ISummarizableEntity {
    public Summary GenerateSummary() {
        var id = ProductID.ToString();
        var description = String.Format("{0} weighs {1}{2} and belongs in the {3} department", ProductName, WeightValue, WeightUnit, Department);
        return new Summary(id,description);
    }
}

"Le interfacce che portano solo proprietà dovrebbero essere evitate" Non sono d'accordo. Fornisci il motivo per cui lo pensi.
Euforico

Hai ragione, ho aggiunto alcuni dettagli
vanna,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.