2015-11-29 12 views
5

Spesso lavoro con classi che rappresentano entità prodotte da una fabbrica. Per abilitare facilmente il test delle mie fabbriche, di solito implemento lo IEquatable<T>, pur sovrascrivendo GetHashCode e Equals (come suggerito dallo MSDN).Dovrei usare IEquatable per facilitare il test delle fabbriche?

Ad esempio; prendere la seguente classe di entità che è semplificata, ad esempio. In genere le mie classi hanno molte più proprietà. Occasionalmente c'è anche una collezione, che nel metodo Equals controllo usando SequenceEqual.

public class Product : IEquatable<Product> 
{ 
    public string Name 
    { 
     get; 
     private set; 
    } 

    public Product(string name) 
    { 
     Name = name; 
    } 

    public override bool Equals(object obj) 
    { 
     if (obj == null) 
     { 
      return false; 
     } 

     Product product = obj as Product; 

     if (product == null) 
     { 
      return false; 
     } 
     else 
     { 
      return Equals(product); 
     } 
    } 

    public bool Equals(Product other) 
    { 
     return Name == other.Name; 
    } 

    public override int GetHashCode() 
    { 
     return Name.GetHashCode(); 
    } 
} 

Questo significa che può quindi eseguire unit test semplici in questo modo (supponendo che il costruttore viene testato altrove).

[TestMethod] 
public void TestFactory() 
{ 
    Product expected = new Product("James"); 

    Product actual = ProductFactory.BuildJames(); 

    Assert.AreEqual(expected, actual); 
} 

Tuttavia questo solleva una serie di domande.

  1. GetHashCode non viene effettivamente utilizzato, ma ho impiegato del tempo per implementarlo.
  2. In realtà raramente desidero utilizzare Equals nella mia applicazione effettiva oltre il test.
  3. Trascorro più tempo a scrivere altri test per garantire che lo Equals funzioni correttamente.
  4. Ora ho altri tre metodi da mantenere, ad es. aggiungi una proprietà alla classe, aggiorna i metodi.

Ma questo mi dà un ottimo TestMethod.

Si tratta di un uso appropriato di IEquatable o devo fare un altro approccio?

+0

L'utilizzo di IEquatable è certamente inappropriato perché non esiste una sola nozione di uguaglianza per un prodotto. Ma potresti usare un "IEqualityComparer" che Resharper può anche generare per te. Ed è una scelta se testare un simile comparatore generato. – usr

+1

GetHashCode basato sul campo mutabile è una cattiva idea http://blogs.msdn.com/b/ericlippert/archive/2011/02/28/guidelines-and-rules-for-gethashcode.aspx –

risposta

4

Se questa è una buona idea o non dipende realmente dal tipo di tipo che la vostra fabbrica crea. Ci sono due generi di tipi: (. Tipi di riferimento in breve)

  • Tipi con semantica di valore (i tipi di valore in breve) e

  • tipi con la semantica di riferimento

In C# è comune utilizzare struct per i tipi di valore e class per i tipi di riferimento, ma non è necessario, è possibile utilizzare class per entrambi. Il punto è che:

  • tipi di valore sono destinate ad essere piccole, di solito immutabile, oggetti self-contained cui scopo principale è quello di contenere un certo valore, mentre

  • tipi di riferimento sono oggetti che hanno complesso stato mutevole, possibilmente riferimenti ad altri oggetti, e le funzionalità non banale, vale a dire gli algoritmi, la logica di business, ecc

Se la fabbrica è la creazione di un tipo di valore, poi certo, andare avanti e rendere IEquatable e l'uso questo ne al trucco.Ma nella maggior parte dei casi, non usiamo le fabbriche per i tipi di valore, che tendono ad essere piuttosto banali, usiamo le fabbriche per i tipi di riferimento, che tendono ad essere piuttosto complessi, quindi se la vostra fabbrica sta creando un tipo di riferimento, allora davvero, questi i tipi di oggetti sono solo da confrontare per riferimento, quindi aggiungere i metodi Equals() e GetHashCode() è ovunque da fuorviante a sbagliato.

Dai un suggerimento a quello che succede con le mappe hash: la presenza di Equals() e GetHashCode() in un tipo generalmente indica che è possibile utilizzare un'istanza di questo tipo come chiave in una mappa hash; ma se l'oggetto non è un tipo di valore immutabile, il suo stato può cambiare dopo che è stato inserito nella mappa, nel qual caso il metodo inizierà a valutare qualcos'altro, ma la mappa di hash non si preoccuperà mai di richiamare GetHashCode() in ordine di riposizionare l'oggetto nella mappa. Il risultato in questi casi tende a essere il caos.

Quindi, la linea di fondo è che se la vostra fabbrica crea oggetti complessi, allora dovreste forse adottare un approccio diverso. La soluzione ovvia è richiamare la factory e quindi controllare ciascuna proprietà dell'oggetto restituito per assicurarsi che siano tutte come previsto.

Potrei forse proporre un miglioramento a questo, anche se attenzione che ci ho pensato, non l'ho mai provato, quindi potrebbe non essere una buona idea in pratica. Eccolo:

La tua fabbrica presumibilmente crea oggetti che implementano una particolare interfaccia. (Altrimenti, qual è il punto di avere una fabbrica, giusto?) Quindi, in teoria si potrebbe stabilire che le istanze di oggetti create di recente che implementano questa interfaccia dovrebbero avere alcune proprietà inizializzate su un particolare insieme di valori. Questa sarebbe una regola imposta dall'interfaccia, quindi potresti avere qualche funzione legata all'interfaccia che controlla se questo è vero, e questa funzione potrebbe anche essere parametrizzata con qualche suggerimento da aspettarsi valori iniziali diversi in circostanze diverse.

(ultima volta che ho controllato, in C# un metodo legato a un'interfaccia di solito era un estensione metodo; non ricordo fuori dalla parte superiore della mia testa se C# permette metodi statici di essere parte di un'interfaccia, o se . i progettisti di C# hanno ancora aggiunto al linguaggio qualcosa di pulito ed elegante come i metodi di interfaccia di default di Java)

Così, con un metodo di estensione, si potrebbe forse apparire come segue:

public boolean IsProperlyInitializedInstance(this IProduct self, String hint) 
{ 
    if(self.Name != hint) 
     return false; 
    //more checks here 
    return true; 
} 

IProduct product = productFactory.BuildJames(); 
Assert.IsTrue(product.IsProperlyInitializedInstance(hint:"James")); 
1

Per il codice di prova è possibile utilizzare una riflessione uguaglianza basata, qualcosa come: Comparing object properties in c#

Molte librerie di test forniscono tale utilità, in questo modo non è necessario modificare il progetto del codice di produzione per soddisfare i test.