C # Design, molte proprietà, costruttore complesso ed uguaglianza

4

Sono ancora un novizio relativo con OOP, quindi sto ancora cercando di capire alcune delle migliori pratiche su come progettare oggetti "buoni". So che questa domanda è probabilmente piuttosto soggettiva, motivo per cui sono qui piuttosto che su StackOverflow.

Sto progettando un oggetto che avrà 20 proprietà di stringa separate, nessuna delle quali è richiesta. Questi vincoli sono fissi e non possono essere modificati in quanto è un'implementazione di un'origine dati esterna di cui non ho alcun controllo.

Il problema di progettazione in cui mi imbatto è come costruire in modo efficace un'istanza dell'oggetto e essere in grado di confrontare l'uguaglianza confrontando ciascun campo senza violare le best practice normali.

  • Un costruttore di 20 argomenti sembra molto sbagliato, soprattutto perché ogni campo è una stringa, e ci sono casi in cui molti di essi saranno nulli. Potrei usare gli argomenti opzionali in C # 4 per ridurre il numero di argomenti che sono effettivamente chiamati, ma dato che tutto è una stringa, dovrei usare parametri nominati quando chiamo il costruttore, e sembra che sarebbe ancora più brutto.

  • Ho pensato di provare a raggruppare le proprietà in classi più piccole, ma il raggruppamento più logico ha ancora un oggetto con 10 proprietà e almeno la metà delle proprietà rimanenti non ha raggruppamenti logici.

  • Potrei assegnare a ciascuna proprietà un setter pubblico, ma ciò renderebbe l'oggetto mutabile e quindi complicherebbe gli argomenti per l'override di GetHashCode() . E davvero non ho bisogno che l'oggetto sia mutabile. Una volta che è stato creato, non dovrebbe mai esserci un motivo per cambiarlo, ma non voglio fare affidamento su questo perché chissà che cosa cercheranno di fare con qualcuno.

  • L'implementazione di IEquatable<T> è una possibilità per il test di uguaglianza, ma la mia comprensione è che si raccomanda di sovrascrivere Equals(object obj) e GetHashCode() quando si implementa IEquatable così tutti i metodi Equals hanno lo stesso comportamento .

Si tratta di un caso limite in cui è necessario eseguire l'override di Equals senza sovrascrivere GetHashCode ? Non intendo utilizzare questo oggetto come chiave in un dizionario o come membro di HashSet , ma non posso prevedere ciò che qualcun altro tenterebbe di fare in futuro.

O dovrei abbandonare l'idea di fare un confronto di uguaglianza su tutte le proprietà con Equals e implementare il mio metodo Compare() ?

O esiste un metodo migliore per costruire l'oggetto che gli consentirebbe di essere immutabile senza dover passare tutti gli argomenti tramite un costruttore o un metodo?

EDIT: In risposta al commento di Sign, lo scopo dell'oggetto è in ultima analisi essere in grado di confrontare i dati dall'origine dati esterna con i nostri dati interni per garantire che sia accurato. Entrambi i set di dati contengono informazioni sull'indirizzo, ma sono suddivisi in modo diverso.

I dati interni, sono generalmente memorizzati come indirizzo tipico (Via, Città, Stato, Codice postale, Contea, Paese), ma ci sono variazioni quando si dispone di una suite / appartamento # o di un piano in un edificio.

I dati esterni vengono ulteriormente suddivisi in pezzi più piccoli.

Quindi questo oggetto dovrebbe essere un collegamento comune tra le 2 origini dati e consentire all'utente di confrontare campi uguali per l'uguaglianza.

EDIT # 2: Alla fine, ho scelto la risposta di Doc Brown perché una versione modificata della sua idea del dizionario (con Enums come Keys invece di stringhe) era il modo più semplice per andare senza avere un costruttore disordinato e comunque fornire l'immutabilità che speravo di avere. A lungo termine, ho intenzione di sperimentare alcuni dei suggerimenti di Oliver in quanto alcune delle sue modifiche hanno avuto molto da offrire.

    
posta psubsee2003 23.12.2011 - 15:02
fonte

6 risposte

6

Se insisti di avere il tuo oggetto immutabile, non c'è ovviamente altro modo che fornire le 20 stringhe attraverso il costruttore. E se vuoi essere in grado di lasciare alcune stringhe, devi dire quali argomenti stai fornendo e quali no, il che ti porta ad una qualche forma di parametri nominati. Ovviamente, oltre alle possibilità che hai menzionato da solo, puoi anche

  • fornisce i 20 argomenti da un elenco di stringhe
  • fornisci loro un dizionario (chiave = > valore, dove "chiave" è il nome dell'attributo)
  • fornisci loro un oggetto di una classe helper, che ha le stesse 20 proprietà con getter e setter, quindi questa non sarà immutabile

Potrebbe anche essere una buona idea implementare Equals e GetHashCode utilizzando il reflection, eseguendo il loop su tutte le proprietà di stringhe pubbliche della tua classe.

    
risposta data 23.12.2011 - 15:39
fonte
5

Penso che un costruttore potrebbe essere utile qui. Ha il vantaggio di mantenere immutabile la classe di valore, ma la creazione non è più un gran casino dato che devi solo specificare ciò che desideri e sono liberi di specificare i valori predefiniti per gli altri.

Naturalmente questo può essere esteso per includere guardie e altre cose, ma questo dovrebbe dare un'idea generale:

public class Big
{
    public readonly string A;
    public readonly string B;
    public readonly string C;
    public readonly string D;
    public readonly string E;

    public Big(string a, string b, string c, string d, string e)
    {
        A = a;
        B = b;
        C = c;
        D = d;
        E = e;
    }
}

public class BigBuilder
{
    private string _a = string.Empty;
    private string _b = string.Empty;
    private string _c = string.Empty;
    private string _d = string.Empty;
    private string _e = string.Empty;

    public BigBuilder WithA(string a)
    {
        _a = a;
        return this;
    }

    public BigBuilder WithB(string b)
    {
        _b = b;
        return this;
    }

    public BigBuilder WithC(string c)
    {
        _c = c;
        return this;
    }

    public BigBuilder WithD(string d)
    {
        _d = d;
        return this;
    }

    public BigBuilder WithE(string e)
    {
        _e = e;
        return this;
    }

    public Big Build()
    {
        return new Big(_a, _b, _c, _d, _e);
    }
}

public class Test
{
    public static void Run()
    {
        new BigBuilder()
            .WithC("6")
            .WithD("hello")
            .Build();
    }
}
    
risposta data 08.06.2016 - 13:54
fonte
1

Non c'è alcun problema con l'implementazione di GetHashCode () se il tuo oggetto è immutabile o no. Se è immutabile, l'hashcode può essere precalcolato; se è mutabile, l'hashcode deve essere ricalcolato ogni volta che viene richiamato il metodo GetHashCode (). E dal momento che il tuo oggetto consiste di nient'altro che 20 stringhe, che sono immutabili, e quindi i loro hashcode sono già calcolati, il calcolo del tuo hashcode è quasi banale. Basta moltiplicare i codici hash dei membri della stringa non nulli insieme o cercare lo stackoverflow per un algoritmo di hash leggermente migliore.

Or is there a better method for building the object that would allow it to still be immutable without having to pass all of the arguments through a constructor or a method?

Sono un sostenitore fanatico di oggetti immutabili, quindi ti consiglierei di rendere il tuo immutabile. E non passerei un dizionario o una lista al costruttore; Passerei 20 argomenti separati, non c'è nulla di sbagliato in questo, e non c'è nulla guadagnato in altri modi. Qualsiasi altro modo è obbligato a spostare alcuni dei controlli che il compilatore farà per te in fase di compilazione, a verificare che dovrai eseguire da solo il runtime: sempre una cattiva idea.

Is this an edge case where overriding Equals without overriding GetHashCode should be done? I don't intend on using this object as a key in a dictionary or as a member of a HashSet, but I can't predict what someone else would try to do in the future.

Per IEquatable<T> è vero che non devi, in senso stretto, implementare GetHashCode() , tranne per evitare l'avviso che riceverai se non lo fai, ma è buona norma implementare sempre GetHashCode() , proprio perché qualcuno potrebbe provare a usarlo in futuro.

Nota: poiché renderai il tuo oggetto immutabile, (non è vero?) puoi precomputare il tuo codice hash e memorizzarlo con l'oggetto, in modo che il metodo Equals() possa prima confrontare i codici hash e restituire immediatamente se essi non combaciano, piuttosto che fare sempre un confronto completo. In pratica non hai bisogno di fare trucchi del genere a meno che tu non abbia già stabilito di avere un problema di prestazioni, ma è un trucco che vale la pena tenere a mente.

Or should I just abandon the idea of doing an equality comparison on all of the properties with Equals and implement my own Compare() method?

Non so cosa intendi con questa domanda. Implementerai il tuo metodo Equals() e sarà una serie di if( !Object.Equals( MyField1, other.MyField1 ) ) return false;

    
risposta data 23.12.2011 - 16:30
fonte
1

Apparentemente, non sei interessato alle singole proprietà; vuoi solo confrontare un mucchio di campi. Pertanto, un dizionario o un elenco di coppie nome-campo / valore mi sembrano molto appropriati. Ti permette di fare i confronti in un ciclo invece di dover trattare ogni campo individualmente. È anche un approccio universale in quanto è possibile applicarlo a diversi set di proprietà. Inoltre, è più flessibile in quanto non dovrai modificare gli algoritmi se aggiungi nuovi campi.

Non aderirei all'approccio immutabile. Invece, dai alla tua classe un metodo Add e dimentica il costruttore con la lista infinita dei parametri.

public class FieldComparer : IEquatable<FieldComparer>
{
    private Dictionary<string,string> _fieldDict = new Dictionary<string,string>();

    public void Add(string fieldName, string value) {
        fieldDict.Add(fieldName, value);
    }

    // The magic comes here
}

EDIT: Come risposta alla tua preoccupazione, ho due suggerimenti per approcci diversi:

1

Nella classe sopra descritta, aggiungi le singole proprietà per i campi che vuoi gestire individualmente:

public string LastName {
    get {
        string last_Name;
        _fieldDict.TryGet("LastName", out lastName);
        return lastName;
    }
    set {
        _fieldDict["LastName"] = value;
    }
}

2

Utilizza le proprietà in primo luogo e rendile disponibili tramite un indicizzatore per poterle scorrere ciclicamente:

public string FistName { get; set; }
public string LastName { get; set; }
public string City { get; set; }

public const int PropertyCount = 3;

public string this[int i]
{
    get
    {
        switch (i) {
            case 0: 
                return FirstName;
            case 1: 
                return LastName;
            case 2: 
                return City;
            default:
                throw new ArgumentNullException("Index out of bounds.");
        }
    }
    set
    {
        switch (i) {
            case 0: 
                FirstName = value;
                break;
            case 1: 
                LastName = value;
                break;
            case 2: 
                City = value;
                break;
            default:
                throw new ArgumentNullException("Index out of bounds.");
        }
    }
}

(Il setter dell'indicizzatore potrebbe non essere necessario per il tuo scopo.) È quindi possibile accedere alle proprietà in questo modo:

var comp = new FieldComparer();
comp.FistName = "John";
Console.WriteLine(comp[0]); // -> John

EDIT: La soluzione # 2 ha lo svantaggio, che perdiamo l'universalità dell'approccio. Potremmo ripristinarlo, se abbiamo implementato l'indicizzatore e PropertyCount da un'interfaccia. (Dovremmo anche convertire PropertyCount in una proprietà di sola lettura). Il confronto avverrà quindi in una classe separata.

public interface IPropertyIndexer
{
    string this[int i] { get; }
    int PropertyCount { get; }
}

Invece, mostrerò una soluzione completa usando un enumeratore invece di un indicizzatore. Entrambi gli approcci sono comparabili. La versione dell'enumeratore che utilizza yield return è un po 'più corta della versione dell'indicizzatore che utilizza un'istruzione switch .

3

public interface IPropertyEnumerator
{
    IEnumerator<string> AllValues { get; }
}

public class Address : IPropertyEnumerator
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string City { get; set; }

    public IEnumerator<string> AllValues
    {
        get
        {
            yield return FirstName;
            yield return LastName;
            yield return City;
        }
    }
}

public class Customer : IPropertyEnumerator
{
    public string Name { get; set; }
    public string CustNo { get; set; }

    public IEnumerator<string> AllValues
    {
        get
        {
            yield return Name;
            yield return CustNo;
        }
    }
}

public static class PropertyComparer
{
    public static bool Equals<T>(T x, T y)
        where T : IPropertyEnumerator
    {
        var enum1 = x.AllValues;
        var enum2 = y.AllValues;
        while (enum1.MoveNext()) {
            if (!enum2.MoveNext() || enum1.Current != enum2.Current) {
                return false;
            }
        }
        return !enum2.MoveNext(); // true if number of properties equal
    }
}

public class Example
{
    public void Compare()
    {
        var a1 = new Address { FirstName = "John", LastName = "Doe", City = "New York" };
        var a2 = new Address { FirstName = "John", LastName = "Doe", City = "New York" };
        Console.WriteLine(PropertyComparer.Equals(a1, a2));

        var c1 = new Customer { CustNo = "A123", Name = "Miller" };
        var c2 = new Customer { CustNo = "B777", Name = "Stanley" };
        Console.WriteLine(PropertyComparer.Equals(c1, c2));
    }
}
    
risposta data 24.12.2011 - 01:35
fonte
0

Sembra che tu voglia due costruttori uno per ogni tipo di dati di input. Se questi dati sono già contenuti in un oggetto, puoi farlo. Il costruttore del dizionario non sembra una buona idea dato che un insieme arbitrario di dati non sarà sufficiente per fare qualsiasi cosa con (un oggetto con solo un paese e un nome di strada non andrebbe bene).

    
risposta data 23.12.2011 - 16:22
fonte
0

Dipende ovviamente dai requisiti ... Per efficienza si intende la velocità di esecuzione o la velocità / chiarezza degli sviluppatori?

Ecco un'altra possibilità:

Chiedi al tuo costruttore di prendere un oggetto di classe in quanto è solo un parametro. Certamente questo "può" essere più facile da mantenere e tutto il lavoro disordinato per farlo può essere fatto altrove. Questo è sicuramente consigliabile laddove la chiarezza è più importante in modo che ogni campo abbia un nome logico e non sia inserito in un dizionario che potrebbe non essere compilato correttamente. Chissà su chi lavorerà sul tuo codice successivo;) O forse ci saranno tra 2 anni e potrai risparmiare tempo ed errori? Ma è più importante della velocità di esecuzione? Per me la chiarezza supera le differenze di velocità insignificanti, fino a un punto in cui l'accumulo non è insignificante, naturalmente;)

    
risposta data 23.12.2011 - 17:53
fonte

Leggi altre domande sui tag