Utilizzo di struct per forzare la convalida del tipo built-in

9

Gli oggetti di dominio comune hanno proprietà che possono essere rappresentate da un tipo built-in ma i cui valori validi sono un sottoinsieme dei valori che possono essere rappresentati da quel tipo.

In questi casi, il valore può essere memorizzato utilizzando il tipo built-in, ma è necessario garantire che i valori siano sempre convalidati nel punto di immissione, altrimenti potremmo finire con un valore non valido.

Un modo per risolvere questo è memorizzare il valore come struct personalizzato che ha un singolo campo di supporto private readonly del tipo built-in e il cui costruttore convalida il valore fornito. Possiamo quindi essere sempre sicuri di utilizzare solo valori convalidati utilizzando questo tipo di struct .

Possiamo anche fornire operatori di cast da e verso il tipo built-in sottostante in modo che i valori possano entrare e uscire senza interruzioni dal tipo sottostante.

Prendiamo ad esempio una situazione in cui dobbiamo rappresentare il nome di un oggetto dominio, e i valori validi sono qualsiasi stringa che sia compresa tra 1 e 255 caratteri di lunghezza inclusi. Possiamo rappresentarlo usando la seguente struttura:

public struct ValidatedName : IEquatable<ValidatedName>
{
    private readonly string _value;

    private ValidatedName(string name)
    {
        _value = name;
    }

    public static bool IsValid(string name)
    {
        return !String.IsNullOrEmpty(name) && name.Length <= 255;
    }

    public bool Equals(ValidatedName other)
    {
        return _value == other._value;
    }

    public override bool Equals(object obj)
    {
        if (obj is ValidatedName)
        {
            return Equals((ValidatedName)obj);
        }
        return false;
    }

    public static implicit operator string(ValidatedName x)
    {
        return x.ToString();
    }

    public static explicit operator ValidatedName(string x)
    {
        if (IsValid(x))
        {
            return new ValidatedName(x);
        }
        throw new InvalidCastException();
    }

    public static bool operator ==(ValidatedName x, ValidatedName y)
    {
        return x.Equals(y);
    }

    public static bool operator !=(ValidatedName x, ValidatedName y)
    {
        return !x.Equals(y);
    }

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

    public override string ToString()
    {
        return _value;
    }
}

L'esempio mostra il to- string cast come implicit in quanto questo non può mai fallire ma il cast from- string come explicit come questo genererà valori non validi, ma ovviamente questi potrebbero essere entrambi implicit o explicit .

Nota anche che si può inizializzare questa struttura solo con un cast da string , ma si può verificare se un cast di questo tipo fallirà in anticipo usando il metodo IsValid static .

Questo sembra essere un buon modello per applicare la convalida dei valori di dominio che possono essere rappresentati da tipi semplici, ma non li vedo spesso usati o suggeriti e sono interessato al perché.

Quindi la mia domanda è: quali sono i vantaggi e gli svantaggi dell'uso di questo modello e perché?

Se ritieni che questo sia un modello errato, mi piacerebbe capire perché e anche ciò che ritieni sia l'alternativa migliore.

NB ho originariamente chiesto questa domanda su Stack Overflow ma è stata messa in attesa principalmente come basata sull'opinione pubblica (di per sé soggettiva), speriamo che possa avere più successo qui.

Qui sopra c'è il testo originale, sotto un paio di altri pensieri, in parte in risposta alle risposte ricevute prima che andasse in attesa:

  • Uno dei punti principali delle risposte era la quantità di codice della piastra della caldaia necessaria per il modello sopra, specialmente quando sono richiesti molti di questi tipi. Tuttavia, in difesa del pattern, questo potrebbe essere ampiamente automatizzato usando i template e in realtà per me non sembra poi così male, ma questa è solo la mia opinione.
  • Da un punto di vista concettuale, non sembra strano quando si lavora con un linguaggio strongmente tipizzato come C # per applicare solo il principio strongmente tipizzato ai valori compositi, piuttosto che estenderlo a valori che possono essere rappresentati da un istanza di un tipo predefinito?
posta gmoody1979 06.01.2015 - 15:56
fonte

4 risposte

4

Questo è abbastanza comune nei linguaggi in stile ML come Standard ML / OCaml / F # / Haskell dove è molto più facile creare i tipi di wrapper. Offre due vantaggi:

  • Consente a un pezzo di codice di far rispettare una stringa che ha subito la convalida, senza doversi occupare della convalida stessa.
  • Ti consente di localizzare il codice di convalida in un'unica posizione. Se un ValidatedName contiene mai un valore non valido, l'errore si trova nel metodo IsValid .

Se ottieni correttamente il metodo IsValid , hai la certezza che qualsiasi funzione che riceve ValidatedName sta effettivamente ricevendo un nome convalidato.

Se devi fare manipolazioni di stringhe puoi aggiungere un metodo pubblico che accetta una funzione che accetta una stringa (il valore di ValidatedName ) e restituisce una stringa (il nuovo valore) e convalida il risultato dell'applicazione della funzione . Ciò elimina lo standard di riferimento per ottenere il valore String sottostante e il re-wrapping.

Un uso correlato per i valori di avvolgimento è tracciare la loro provenienza. Per esempio. Le API del sistema operativo basate su C a volte forniscono handle per le risorse come numeri interi. È possibile racchiudere le API del sistema operativo per utilizzare invece una struttura Handle e fornire solo l'accesso al costruttore a quella parte del codice. Se il codice che produce Handle s è corretto, verranno utilizzati solo gli handle validi.

    
risposta data 06.01.2015 - 16:23
fonte
1

what do you see as being the advantages and disadvantages of using this pattern, and why?

Buono :

  • È autonomo. Troppi bit di validazione hanno viticci che arrivano in luoghi diversi.
  • Aiuta l'auto-documentazione. Vedere un metodo prendere un ValidatedString rende molto più chiaro la semantica della chiamata.
  • Aiuta a limitare la convalida a un punto anziché a dover essere duplicato con metodi pubblici.

Bad :

  • Il trucco del cast è nascosto. Non è un C # idiomatico, quindi può causare confusione durante la lettura del codice.
  • Getta. Avere stringhe che non soddisfano la convalida non è uno scenario eccezionale. Fare IsValid prima che il cast sia un po 'spiacevole.
  • Non può dirti perché qualcosa non è valido.
  • Il valore predefinito ValidatedString non è valido / convalidato.

Ho visto questo genere di cose più spesso con User e AuthenticatedUser tipi di cose, in cui l'oggetto cambia effettivamente. Può essere un buon approccio, anche se sembra fuori posto in C #.

    
risposta data 06.01.2015 - 16:12
fonte
0

La tua strada è piuttosto pesante e intensa. In genere definisco entità di dominio come:

public class Institution
{
    private Institution() { }

    public Institution(int organizationId, string name)
    {
        OrganizationId = organizationId;            
        Name = name;
        ReplicationKey = Guid.NewGuid();

        new InstitutionValidator().ValidateAndThrow(this);
    }

    public int Id { get; private set; }
    public string Name { get; private set; }        
    public virtual ICollection<Department> Departments { get; private set; }

    ... other properties    

    public Department AddDepartment(string name)
    {
        var department = new Department(Id, name);
        if (Departments == null) Departments = new List<Department>();
        Departments.Add(department);            
        return department;
    }

    ... other domain operations
}

Nel costruttore dell'entità, la convalida viene attivata utilizzando FluentValidation.NET, per assicurarsi che non sia possibile creare un'entità con stato non valido. Tieni presente che le proprietà sono tutte in sola lettura, puoi solo impostarle tramite il costruttore o le operazioni di dominio dedicato.

La convalida di questa entità è una classe separata:

public class InstitutionValidator : AbstractValidator<Institution>
{
    public InstitutionValidator()
    {
        RuleFor(institution => institution.Name).NotNull().Length(1, 100).WithLocalizedName(() =>   Prim.Mgp.Infrastructure.Resources.GlobalResources.InstitutionName);       
        RuleFor(institution => institution.OrganizationId).GreaterThan(0);
        RuleFor(institution => institution.ReplicationKey).NotNull().NotEqual(Guid.Empty);
    }  
}

Questi validatori possono anche essere facilmente riutilizzati, e scrivi meno codice standard. E un altro vantaggio è che è leggibile.

    
risposta data 07.01.2015 - 08:25
fonte
0

Mi piace questo approccio ai tipi di valore. Il concetto è ottimo, ma ho alcuni suggerimenti / lamentele sull'implementazione.

Casting : in questo caso non mi piace l'utilizzo del casting. Il cast esplicito da stringa non è un problema, ma non c'è molta differenza tra (ValidatedName)nameValue e new ValidatedName(nameValue) . Quindi sembra un po 'inutile. Il cast implicito nella stringa è il problema peggiore. Penso che ottenere il valore effettivo della stringa dovrebbe essere più esplicito, perché potrebbe essere accidentalmente assegnato alla stringa e il compilatore non ti avviserà della possibile "perdita di precisione". Questo tipo di perdita di precisione dovrebbe essere esplicito.

ToString : preferisco usare ToString overload solo per scopi di debug. E non penso che restituire il valore grezzo sia una buona idea. Questo è lo stesso problema con la conversione implicita alla stringa. Ottenere il valore interno dovrebbe essere un'operazione esplicita. Credo che tu stia cercando di far sì che la struttura si comporti come una normale stringa per il codice esterno, ma penso che, così facendo, stai perdendo parte del valore che ottieni dall'implementazione di questo tipo di tipo.

Uguali e GetHashCode : le strutture utilizzano l'uguaglianza strutturale per impostazione predefinita. Quindi il tuo Equals e GetHashCode stanno duplicando questo comportamento predefinito. Puoi rimuoverli e sarà praticamente la stessa cosa.

    
risposta data 07.01.2015 - 09:33
fonte

Leggi altre domande sui tag