2010-03-13 10 views
7

Sto usando .NET 3.5. Abbiamo alcune classi di terze parti complesse che vengono generate automaticamente e fuori dal mio controllo, ma con le quali dobbiamo lavorare a scopo di test. Vedo che il mio team fa un sacco di proprietà/impostazione delle proprietà profondamente nidificate nel nostro codice di test, e sta diventando piuttosto ingombrante.Impostazione fluida delle proprietà C# e metodi di concatenamento

Per risolvere il problema, vorrei creare un'interfaccia fluente per l'impostazione delle proprietà sui vari oggetti nell'albero gerarchico. Ci sono un gran numero di proprietà e classi in questa libreria di terze parti, e sarebbe troppo noioso mappare tutto manualmente.

Il mio primo pensiero era di usare solo gli inizializzatori dell'oggetto. Red, Blue e Green sono proprietà e Mix() è un metodo che imposta una quarta proprietà Color al colore RGB-safe più vicino con quel colore misto. Le vernici devono essere omogeneizzate con Stir() prima di poter essere utilizzate.

Bucket b = new Bucket() { 
    Paint = new Paint() { 
    Red = 0.4; 
    Blue = 0.2; 
    Green = 0.1; 
    } 
}; 

che lavora per inizializzare il Paint, ma ho bisogno di catena Mix() e altri metodi ad esso. Prossimo tentativo:

Create<Bucket>(Create<Paint>() 
    .SetRed(0.4) 
    .SetBlue(0.2) 
    .SetGreen(0.1) 
    .Mix().Stir() 
) 

Ma questo non scala bene, perché avrei dovuto definire un metodo per ogni proprietà voglio impostare, e ci sono centinaia di diverse proprietà in tutte le classi. Inoltre, C# non ha un modo per definire dinamicamente i metodi prima di C# 4, quindi non penso di poter collegare le cose per farlo automaticamente in qualche modo.

terzo tentativo:

Create<Bucket>(Create<Paint>().Set(p => { 
    p.Red = 0.4; 
    p.Blue = 0.2; 
    p.Green = 0.1; 
    }).Mix().Stir() 
) 

Ciò non sembra troppo male, e sembra che sarebbe fattibile. È un approccio consigliabile? È possibile scrivere un metodo Set che funzioni in questo modo? O dovrei perseguire una strategia alternativa?

+0

In realtà ho un approccio simile in uno dei miei progetti. –

risposta

9

Funziona?

Bucket b = new Bucket() { 
    Paint = new Paint() { 
    Red = 0.4; 
    Blue = 0.2; 
    Green = 0.1; 
    }.Mix().Stir() 
}; 

Supponendo Mix() e Stir() sono definiti per restituire un oggetto Paint.

Per chiamare i metodi che restituiscono void, è possibile utilizzare un metodo di estensione che vi permetterà di eseguire l'inizializzazione aggiuntive sull'oggetto si passa:

public static T Init<T>(this T @this, Action<T> initAction) { 
    if (initAction != null) 
     initAction(@this); 
    return @this; 
} 

che potrebbero essere usati simile a set(), come descritto :

Bucket b = new Bucket() { 
    Paint = new Paint() { 
    Red = 0.4; 
    Blue = 0.2; 
    Green = 0.1; 
    }.Init(p => { 
    p.Mix().Stir(); 
    }) 
}; 
+0

In retrospettiva, questo sembra un approccio ovvio. Devo mancare alla foresta perché gli alberi non lo guardino troppo a lungo! –

+0

FYI per quelli confusi da '@ questo': il' @ 'significa" considera il seguente token come un identificatore letterale "(non come parola chiave). L'effetto è di nominare un parametro chiamato 'this'. –

+1

Non ne sono perplesso, ma non vedo alcun motivo per combattere il compilatore qui quando puoi usare un nome equivalente come 'self' o' source'. Altrimenti, buona risposta e +1. – Aaronaught

4

vorrei pensare in questo modo:

si vuole in sostanza, il tuo ultimo metodo della catena di restituire un secchio. Nel tuo caso, credo che si desidera che il metodo sia Mix(), come si può Mescolare() il secchio dopo

public class BucketBuilder 
{ 
    private int _red = 0; 
    private int _green = 0; 
    private int _blue = 0; 

    public Bucket Mix() 
    { 
     Bucket bucket = new Bucket(_paint); 
     bucket.Mix(); 
     return bucket; 
    } 
} 

quindi è necessario impostare almeno un colore prima di chiamare Mix(). Forza questo con alcune interfacce di sintassi.

public interface IStillNeedsMixing : ICanAddColours 
{ 
    Bucket Mix(); 
} 

public interface ICanAddColours 
{ 
    IStillNeedsMixing Red(int red); 
    IStillNeedsMixing Green(int green); 
    IStillNeedsMixing Blue(int blue); 
} 

E cerchiamo di applicarle alla BucketBuilder

public class BucketBuilder : IStillNeedsMixing, ICanAddColours 
{ 
    private int _red = 0; 
    private int _green = 0; 
    private int _blue = 0; 

    public IStillNeedsMixing Red(int red) 
    { 
     _red += red; 
     return this; 
    } 

    public IStillNeedsMixing Green(int green) 
    { 
     _green += green; 
     return this; 
    } 

    public IStillNeedsMixing Blue(int blue) 
    { 
     _blue += blue; 
     return this; 
    } 

    public Bucket Mix() 
    { 
     Bucket bucket = new Bucket(new Paint(_red, _green, _blue)); 
     bucket.Mix(); 
     return bucket; 
    } 
} 

Ora è necessario una proprietà statica iniziale per dare il via alla catena di

public static class CreateBucket 
{ 
    public static ICanAddColours UsingPaint 
    { 
     return new BucketBuilder(); 
    } 
} 

E questo è più o meno di esso, ora avete un interfaccia fluente con parametri RGB opzionali (purché ne entri almeno uno) come bonus.

CreateBucket.UsingPaint.Red(0.4).Green(0.2).Mix().Stir(); 

La cosa con Fluent interfacce è che non sono così facili da mettere insieme, ma sono facili per lo sviluppatore di codice contro e sono molto estensibile. Se vuoi aggiungere un flag Matt/Gloss a questo senza cambiare tutto il tuo codice chiamante, è facile da fare.

Inoltre, se il provider della tua API cambia tutto sotto di te, devi solo riscrivere questo pezzo di codice; tutto il codice di chiamata può rimanere lo stesso.

0

Vorrei utilizzare il metodo di estensione Init perché U può sempre giocare con il delegato. inferno Si può sempre dichiarare metodi di estensione che occupano le espressioni e anche giocare con i expresions (conservarli per dopo, modificare, a prescindere) questo modo si può facilmente grups negozio di default come:

Create<Paint>(() => new Paint{p.Red = 0.3, p.Blue = 0.2, p.Green = 0.1}). 
Init(p => p.Mix().Stir()) 

In questo modo si puoi usare tutte le azioni (o funzioni) e memorizzare gli inizializzatori standard come catene di espressione per dopo?

0

Se si desidera essere in grado di concatenare le impostazioni delle proprietà senza dover scrivere una tonnellata di codice, un modo per farlo sarebbe utilizzare la generazione del codice (CodeDom). Puoi usare Reflection per ottenere un elenco delle proprietà mutabili, generare una classe di build fluente con un metodo finale Build() che restituisce la classe che stai effettivamente cercando di creare.

Ho intenzione di saltare tutte le informazioni sullo schema di registrazione su come registrare lo strumento personalizzato - è abbastanza facile trovare la documentazione ma ancora prolisso e non penso che aggiungerei molto includendolo . Ti mostrerò comunque cosa sto pensando per il codegen.

public static class PropertyBuilderGenerator 
{ 
    public static CodeTypeDeclaration GenerateBuilder(Type destType) 
    { 
     if (destType == null) 
      throw new ArgumentNullException("destType"); 
     CodeTypeDeclaration builderType = new 
      CodeTypeDeclaration(destType.Name + "Builder"); 
     builderType.TypeAttributes = TypeAttributes.Public; 
     CodeTypeReference destTypeRef = new CodeTypeReference(destType); 
     CodeExpression resultExpr = AddResultField(builderType, destTypeRef); 
     PropertyInfo[] builderProps = destType.GetProperties(
      BindingFlags.Instance | BindingFlags.Public); 
     foreach (PropertyInfo prop in builderProps) 
     { 
      AddPropertyBuilder(builderType, resultExpr, prop); 
     } 
     AddBuildMethod(builderType, resultExpr, destTypeRef); 
     return builderType; 
    } 

    private static void AddBuildMethod(CodeTypeDeclaration builderType, 
     CodeExpression resultExpr, CodeTypeReference destTypeRef) 
    { 
     CodeMemberMethod method = new CodeMemberMethod(); 
     method.Attributes = MemberAttributes.Public | MemberAttributes.Final; 
     method.Name = "Build"; 
     method.ReturnType = destTypeRef; 
     method.Statements.Add(new MethodReturnStatement(resultExpr)); 
     builderType.Members.Add(method); 
    } 

    private static void AddPropertyBuilder(CodeTypeDeclaration builderType, 
     CodeExpression resultExpr, PropertyInfo prop) 
    { 
     CodeMemberMethod method = new CodeMemberMethod(); 
     method.Attributes = MemberAttributes.Public | MemberAttributes.Final; 
     method.Name = prop.Name; 
     method.ReturnType = new CodeTypeReference(builderType.Name); 
     method.Parameters.Add(new CodeParameterDeclarationExpression(prop.Type, 
      "value")); 
     method.Statements.Add(new CodeAssignStatement(
      new CodePropertyReferenceExpression(resultExpr, prop.Name), 
      new CodeArgumentReferenceExpression("value"))); 
     method.Statements.Add(new MethodReturnStatement(
      new CodeThisExpression())); 
     builderType.Members.Add(method); 
    } 

    private static CodeFieldReferenceExpression AddResultField(
     CodeTypeDeclaration builderType, CodeTypeReference destTypeRef) 
    { 
     const string fieldName = "_result"; 
     CodeMemberField resultField = new CodeMemberField(destTypeRef, fieldName); 
     resultField.Attributes = MemberAttributes.Private; 
     builderType.Members.Add(resultField); 
     return new CodeFieldReferenceExpression(
      new CodeThisReferenceExpression(), fieldName); 
    } 
} 

credo che questo dovrebbe solo di farlo - è, ovviamente, non testato, ma dove si va da qui è che si crea un codegen (che eredita da BaseCodeGeneratorWithSite) che compila un CodeCompileUnit popolato con un elenco dei tipi. Tale elenco deriva dal tipo di file che si registra con lo strumento: in questo caso probabilmente lo trasformo in un file di testo con un elenco delimitato da linee di tipi per il quale si desidera generare il codice builder. Chiedi allo strumento di eseguire la scansione di questo, caricare i tipi (potrebbe essere necessario caricare prima gli assembly) e generare bytecode.

E 'dura, ma non così difficile come sembra, e quando hai finito sarete in grado di scrivere codice come questo:

Paint p = new PaintBuilder().Red(0.4).Blue(0.2).Green(0.1).Build().Mix.Stir(); 

che credo sia quasi esattamente quello che vuoi.Tutto quello che dovete fare per richiamare la generazione del codice è registrare lo strumento con un'estensione personalizzata (diciamo .buildertypes), ha messo un file con tale estensione nel progetto, e mettere un elenco dei tipi in esso:

MyCompany.MyProject.Paint 
MyCompany.MyProject.Foo 
MyCompany.MyLibrary.Bar 

E così via. Quando salvi, genererà automaticamente il file di codice che ti serve che supporta istruzioni di scrittura come quella sopra.

Ho usato questo approccio in precedenza per un sistema di messaggistica molto contorto con diverse centinaia di tipi di messaggi diversi. Ci è voluto troppo tempo per costruire sempre il messaggio, impostare un gruppo di proprietà, inviarlo attraverso il canale, ricevere dal canale, serializzare la risposta, ecc ... utilizzando un codegen ha semplificato notevolmente il lavoro in quanto mi ha permesso di generare un classe di messaggistica singola che ha preso tutte le proprietà individuali come argomenti e restituisce una risposta del tipo corretto. Non è qualcosa che consiglierei a tutti, ma quando hai a che fare con progetti molto grandi, a volte devi iniziare a inventare la tua sintassi!

Problemi correlati