2016-01-15 18 views
10

Sto costruendo una semplice API Guard per proteggere da parametri illegali passati a funzioni e così via.Come creare un'API Fluent Nested Guard

Ho il seguente codice:

public static class Guard 
{ 
    public static GuardArgument<T> Ensure<T>(T value, string argumentName) 
    { 
     return new GuardArgument<T>(value, argumentName); 
    } 
} 

public class GuardArgument<T> 
{ 
    public GuardArgument(T value, string argumentName) 
    { 
     Value = value; 
     Name = Name; 
    } 

    public T Value { get; private set; } 
    public string Name { get; private set; } 
} 

// Example extension for validity checks 
public static GuardArgument<T> IsNotNull<T>(this GuardArgument<T> guardArgument, string errorMessage) 
{ 
    if (guardArgument.Value == null) 
    { 
     throw new ArgumentNullException(guardArgument.Name, errorMessage); 
    } 

    return guardArgument; 
}  

Al momento il codice può essere utilizzato in modo simile a (notare che questo è solo un esempio muto):

void DummyMethod(int? someObject) { 

    Guard.Ensure(someObject, "someObject") 
     .IsNotNull() 
     .IsGreaterThan(0) 
     .IsLessThan(10); 
} 

Questa tutte le opere bene. Quello che voglio essere in grado di fare ora è estendere l'API per includere proprietà secondarie nei controlli nel modo seguente:

Guard.Ensure(someObject, "someObject") 
    .IsNotNull() 
    .Property(
     (x => x.ChildProp1, "childProp1") 
      .IsNotNull() 
      .IsGreaterThan(10) 
    ) 
    .Property(
     (x => x.ChildProp2, "childProp2") 
      .IsNotNull() 
      .IsLessThan(10) 
    ); 

Ovviamente il nuovo metodo .Property deve restituire il genitore GuardArgument al fine di catena. Inoltre, la proprietà del bambino deve essere in grado di utilizzare i metodi di controllo esistenti (IsNotNull() ecc.) Per evitare la duplicazione del codice.

Non riesco a capire come costruire i parametri della funzione lambda/Property o dove deve essere posizionato il metodo .Property - cioè dovrebbe essere una proprietà su GuardArgument o da qualche altra parte, o anche se c'è una struttura migliore per l'API .

+1

potrebbe essere un errore di battitura, ma il costruttore GuardArgument imposta 'Name = Name' –

+1

Tu dici: "Ovviamente il nuovo metodo '.Property' deve restituire il genitore' GuardArgument' al fine per catena. " - No. Non è corretto. Se stai provando a fare un'API fluente, non dovresti passare il genitore - dovresti costruire un nuovo 'GuardArgument' che nidifica il genitore altrimenti puoi creare bug terribili conservando i riferimenti lungo la catena e poi provando a costruire nuove catene da loro. – Enigmativity

risposta

7

La seguente funzione consente una sintassi simile a ciò che si desidera.

public static GuardArgument<T> Property<T, TProp>(this GuardArgument<T> guardArgument, Func<T, TProp> getProperty, string propertyName, Action<GuardArgument<TProp>> validate) 
{ 
    GuardArgument<TProp> propertyGuardArgument = new GuardArgument<TProp>(getProperty(guardArgument.Value), propertyName); 

    validate(propertyGuardArgument); 

    return guardArgument; 
} 

La funzione crea un nuovo GuardArgument per la proprietà selezionata e quindi passa questo nel parametro Action per consentire di convalidare come si desidera.

Ciò consente anche il concatenamento infinito di proprietà, anche se non sono sicuro che sarebbe particolarmente leggibile.

Usage:

Guard.Ensure(someObject, "someObject") 
    .IsNotNull() 
    .Property(x => x.ChildProp1, "childProp1", childProp1 => 
     childProp1.IsNotNull() 
        .IsLessThan(10) 
        .Property(y => y.InnerChildProperty, "innerChildProperty", innerChildProperty => 
         innerChildProperty.IsNotNull() 
        ) 
    ) 
    .Property(x => x.ChildProp2, "childProp2", childProp2 => 
     childProp2.IsNotNull() 
        .IsGreaterThan(10) 
    ); 
+0

Sembra JavaScript "piramide di sventura";) – jlvaquero

+0

In ogni caso, ricordati che puoi appiattirlo dichiarando e creando Func e Azione prima fuori dalla fluente e poi passali a Property(). – jlvaquero

0

Penso che non si abbia alcun vantaggio dall'inserimento dei controlli di proprietà nella catena dei controlli dell'oggetto principale. Quindi consiglierei di creare una catena per l'oggetto genitore e per ogni proprietà un'altra catena. Questo è molto più leggibile:

Guard.Ensure(a, "a") 
    .IsNotNull("a is null"); 
    Guard.Ensure(a.p0, "a.p0") 
    .IsGreaterThan(10); 
    Guard.Ensure(a.p1, "a.p1") 
    .IsGreaterThan(5); 
0

Penso che stai reinventando una ruota qui. Installa questa estensione - Code Contracts e qui è docs come usarlo.

Oltre al codice basato afferma simile alla tua, vale a dire:

public int[] Bar(){ 
    Contract.Ensures(Contract.ForAll(0, Contract.Result<int[]>().Length, index => Contract.Result<int[]>()[index] > 0)); 

.... 
} 

o

Contract.Requires<ArgumentNullException>(x.Value.NestedObject != null, ”x.Value.NestedObject”); 

ma ha anche attributi e set completo di funzioni per le interfacce di controllo, bello pre- e post- condizioni ecc. verificarlo!

+2

Ho usato i Contratti di codice su un progetto e li ho trovati un problema da usare. Riducono la velocità di costruzione a una scansione. Dopo un paio di mesi li tirammo fuori e andammo invece per qualcosa come la domanda. – Sean

+2

Grazie @Sean, di recente abbiamo iniziato a usarli e non ne abbiamo molti, quindi non ho notato che i tempi di costruzione sono ancora peggiori. – vittore

+0

Ho già esaminato i Contratti di codice e, come Sean, la velocità di costruzione era lenta e veniva utilizzata solo per proteggere l'input del metodo. Da qui la necessità di qualcosa di molto più leggero in peso .. – Graham