Utilizzo di struct per imporre la convalida del tipo incorporato


9

Comunemente gli oggetti dominio hanno proprietà che possono essere rappresentate da un tipo incorporato 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 incorporato ma è necessario assicurarsi che i valori siano sempre convalidati nel punto di entrata, altrimenti potremmo finire per lavorare con un valore non valido.

Un modo per risolverlo è archiviare il valore come personalizzato structche ha un singolo private readonlycampo di supporto del tipo incorporato e il cui costruttore convalida il valore fornito. Possiamo quindi essere sempre sicuri di usare solo valori validati usando questo structtipo.

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

Prendiamo ad esempio una situazione in cui dobbiamo rappresentare il nome di un oggetto dominio e i valori validi sono qualsiasi stringa compresa tra 1 e 255 caratteri inclusi. Potremmo 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 stringcast in implicitquanto ciò non può mai fallire, ma il stringcast in explicitquanto ciò genererà valori non validi, ma ovviamente questi potrebbero essere entrambi implicito explicit.

Si noti inoltre che si può inizializzare questa struttura solo tramite un cast string, ma si può verificare se tale cast fallirà in anticipo usando il IsValid staticmetodo.

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

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

Se ritieni che questo sia un cattivo modello, vorrei capire perché e anche quello che ritieni sia la migliore alternativa.

NB Inizialmente avevo posto questa domanda su Stack Overflow ma era stata messa in attesa principalmente come basata sull'opinione (ironicamente soggettiva in sé) - speriamo che qui possa avere più successo.

Sopra è il testo originale, sotto un altro paio di pensieri, in parte in risposta alle risposte ricevute prima che andasse in attesa:

  • Uno dei principali punti sollevati dalle risposte riguardava la quantità di codice della piastra della caldaia necessario per lo schema sopra, specialmente quando sono richiesti molti di questi tipi. Tuttavia, a difesa del modello, questo potrebbe essere in gran parte automatizzato usando i modelli 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 fortemente tipizzato come C # applicare il principio fortemente tipizzato ai valori compositi, piuttosto che estenderlo a valori che possono essere rappresentati da un'istanza di un tipo incorporato?

potresti creare una versione ispirata a un bool (T) lambda
maniaco del cricchetto

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. Ti offre due vantaggi:

  • Consente a un pezzo di codice di imporre che una stringa sia stata sottoposta a convalida, senza doversi occupare di essa stessa della convalida.
  • Ti consente di localizzare il codice di validazione in un unico posto. Se ValidatedNamemai contiene un valore non valido, sai che l'errore è nel IsValidmetodo.

Se si ottiene il IsValidmetodo corretto, si ha la garanzia che qualsiasi funzione che riceve un ValidatedNamesta effettivamente ricevendo un nome convalidato.

Se è necessario eseguire manipolazioni di stringhe, è possibile 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 la piastra di caldaia per ottenere il valore String sottostante e reincartarlo.

Un uso correlato per il wrapping dei valori è il monitoraggio della loro provenienza. Ad esempio, le API del sistema operativo basate su C a volte forniscono handle per le risorse come numeri interi. Puoi racchiudere le API del sistema operativo in modo da utilizzare invece una Handlestruttura e fornire l'accesso al costruttore solo a quella parte del codice. Se il codice che produce la Handles è corretto, verranno mai utilizzati solo handle validi.


1

quali sono i vantaggi e gli svantaggi dell'utilizzo di questo modello e perché?

Buono :

  • È autonomo. Troppi bit di convalida hanno viticci che raggiungono luoghi diversi.
  • Aiuta l'auto-documentazione. Vedere un metodo prendere un ValidatedStringrende molto più chiaro sulla semantica della chiamata.
  • Aiuta a limitare la convalida in un punto piuttosto che dover essere duplicata su metodi pubblici.

Cattivo :

  • L'inganno del casting è nascosto. Non è C # idiomatico, quindi può creare confusione durante la lettura del codice.
  • Si getta. Avere stringhe che non soddisfano la convalida non è uno scenario eccezionale. Fare IsValidprima del cast è un po 'imbarazzante.
  • Non può dirti perché qualcosa non è valido.
  • L'impostazione predefinita ValidatedStringnon è valida / convalidata.

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


1
Grazie, penso che il tuo quarto "con" sia ancora l'argomento più convincente contro di esso - l'uso di default o un array del tipo potrebbe darti valori non validi (a seconda che ovviamente la stringa zero / null sia un valore valido). Questi sono (penso) gli unici due modi per finire con un valore non valido. Ma poi, se NON stessimo usando questo modello, queste due cose ci darebbero comunque valori non validi, ma suppongo che almeno sapremmo che avevano bisogno di essere validati. Quindi questo potrebbe potenzialmente invalidare l'approccio in cui il valore predefinito del tipo sottostante non è valido per il nostro tipo.
gmoody1979,

Tutti i contro sono problemi di implementazione piuttosto che problemi con il concetto. Inoltre trovo che le "eccezioni dovrebbero essere eccezionali" un concetto sfocato e mal definito. L'approccio più pragmatico è quello di fornire un metodo basato su eccezioni e non basato su eccezioni e lasciare che il chiamante scelga.
Doval,

@Doval Accetto salvo quanto indicato nell'altro mio commento. L'intero punto del pattern è sapere con certezza che se abbiamo un ValidatedName, deve essere valido. Ciò si interrompe se il valore predefinito del tipo sottostante non è anche un valore valido del tipo di dominio. Questo ovviamente dipende dal dominio, ma è più probabile che sia il caso (avrei pensato) per i tipi basati su stringhe rispetto ai tipi numerici. Il modello funziona meglio quando il valore predefinito del tipo sottostante è adatto anche come valore predefinito del tipo di dominio.
gmoody1979,

@Doval - Sono generalmente d'accordo. Il concetto in sé va bene, ma sta effettivamente cercando di affinare i tipi di perfezionamento in un linguaggio che non li supporta. Ci saranno sempre problemi di implementazione.
Telastyn,

Detto questo, suppongo che potresti verificare il valore predefinito sul cast "in uscita" e in qualsiasi altro posto necessario all'interno dei metodi della struttura e lanciarlo se non inizializzato, ma questo inizia a diventare confuso.
gmoody1979,

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. Si noti che le proprietà sono tutte di sola lettura: è possibile impostarle solo tramite il costruttore o le operazioni di dominio dedicate.

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 si scrive meno codice del boilerplate. E un altro vantaggio è che è leggibile.


Al downvoter interessa spiegare perché la mia risposta è stata sottoposta a downgrade?
L-Four,

La domanda riguardava una struttura per vincolare i tipi di valore e si è passati a una classe senza spiegare PERCHÉ. (Non un downvoter, sto solo dando un suggerimento.)
DougM,

Ho spiegato perché trovo che questa sia un'alternativa migliore, e questa era una delle sue domande. Grazie per la risposta.
L-Four,

0

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

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

ToString : preferisco usare gli ToStringoverload solo per scopi di debug. E non credo che restituire il valore grezzo sia una buona idea. Questo è lo stesso problema con la conversione implicita in 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 del codice esterno, ma penso che, facendo ciò, stai perdendo parte del valore che ottieni dall'implementazione di questo tipo di tipo.

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


Casting: Semanticamente questo mi sembra più una trasformazione di una stringa in un ValidatedName piuttosto che la creazione di un nuovo ValidatedName: stiamo identificando una stringa esistente come ValidatedName. Pertanto per me il cast sembra più corretto semanticamente. D'accordo, c'è poca differenza nella digitazione (delle dita sulla varietà della tastiera). Non sono d'accordo sul cast della stringa: ValidatedName è un sottoinsieme di stringhe, quindi non può mai esserci una perdita di precisione ...
gmoody1979

ToString: Non sono d'accordo. Per me ToString è un metodo perfettamente valido da utilizzare al di fuori degli scenari di debug, supponendo che soddisfi i requisiti. Anche in questa situazione in cui un tipo è un sottoinsieme di un altro tipo, penso che abbia senso trasformare l'abilità dal sottoinsieme al superinsieme nel modo più semplice possibile, in modo che se l'utente lo desidera, può quasi trattarlo come del tipo super set, ovvero stringa ...
gmoody1979,

Equals e GetHashCode: Sì, le strutture utilizzano l'uguaglianza strutturale, ma in questo caso viene confrontato il riferimento della stringa, non il valore della stringa. Quindi dobbiamo scavalcare Equals. Sono d'accordo che ciò non sarebbe necessario se il tipo sottostante fosse un tipo di valore. Dalla mia comprensione dell'implementazione predefinita di GetHashCode per i tipi di valore (che è abbastanza limitato), questo darà lo stesso valore ma sarà più performante. Dovrei davvero verificare se è così, ma è un po 'un problema secondario al punto principale della domanda. Grazie per la tua risposta a proposito :-).
gmoody1979,

@ gmoody1979 Le strutture vengono confrontate usando Equals su ogni campo per impostazione predefinita. Non dovrebbe essere un problema con le stringhe. Lo stesso con GetHashCode. Per quanto riguarda la struttura essendo sottoinsieme di stringhe. Mi piace pensare al tipo come rete di sicurezza. Non voglio lavorare con ValidatedName e poi scivolare accidentalmente per usare la stringa. Preferirei se il compilatore mi avesse fatto specificare esplicitamente che ora desidero lavorare con dati non controllati.
Euforico

Scusa sì, buon punto su uguali. Sebbene l'override dovrebbe funzionare meglio dato che il comportamento predefinito deve usare la riflessione per fare il confronto. Casting: sì, forse un buon argomento per renderlo un cast esplicito.
gmoody1979,
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.