2010-03-13 9 views
7

Estoy usando .NET 3.5. Tenemos algunas clases complejas de terceros que se generan automáticamente y que están fuera de mi control, pero que debemos trabajar con fines de prueba. Veo a mi equipo haciendo muchas cosas anidadas profundamente que obtienen/configuran en nuestro código de prueba, y se está volviendo bastante engorroso.Configurando con fluidez las propiedades de C# y los métodos de encadenamiento

Para solucionar el problema, me gustaría hacer una interfaz fluida para establecer propiedades en los diversos objetos en el árbol jerárquico. Hay una gran cantidad de propiedades y clases en esta biblioteca de terceros, y sería demasiado tedioso mapear todo manualmente.

Mi idea inicial era solo utilizar los inicializadores de objetos. Red, Blue y Green son propiedades, y Mix() es un método que establece una cuarta propiedad Color con el color RGB más seguro con ese color mixto. Las pinturas deben homogeneizarse con Stir() antes de poder usarse.

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

que funciona para inicializar el Paint, pero necesito cadena Mix() y otros métodos para ello. Siguiente intento:

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

Pero eso no escala bien, porque tendría que definir un método para cada propiedad Quiero configurar, y hay cientos de diferentes propiedades en todas las clases. Además, C# no tiene una forma de definir dinámicamente los métodos anteriores a C# 4, por lo que no creo que pueda engancharme en cosas para hacer esto automáticamente de alguna manera.

Tercer intento:

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

que no se ve tan mal, y parece que sería factible. ¿Es este un enfoque aconsejable? ¿Es posible escribir un método Set que funcione de esta manera? ¿O debería seguir una estrategia alternativa?

+0

Actualmente tengo un enfoque similar en uno de mis proyectos. –

Respuesta

9

¿Funciona?

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

Suponiendo Mix() y Stir() se definen para devolver un objeto Paint.

Para llamar a métodos que devuelven void, puede utilizar un método de extensión que le permitirá llevar a cabo la inicialización adicional sobre el objeto que se pasa en:

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

que podría ser utilizado similar a set() como se describe :

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

En retrospectiva, esto parece ser un enfoque obvio. ¡Debo extrañar el bosque para que los árboles lo miren durante demasiado tiempo! –

+0

FYI para aquellos desconcertados por '@ esto': el' @ 'significa" tratar el siguiente token como un identificador literal "(no como una palabra clave). El efecto es nombrar un parámetro llamado 'this'. –

+1

No me deja perplejo, pero no veo ninguna razón para luchar contra el compilador aquí cuando puedes usar un nombre equivalente como 'self' o' source'. De lo contrario, buena respuesta y +1. – Aaronaught

4

yo creo que de esta manera:

¿quieres esencialmente el último método de la cadena para devolver un cubo. En su caso, pienso que usted quiere que este método es Mix(), como se puede Revolver() el cubo después

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; 
    } 
} 

por lo que necesita para establecer al menos un color antes de llamar Mix(). Vamos a forzar eso con algunas interfaces de sintaxis.

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

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

Y vamos a aplicarlos a la 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; 
    } 
} 

Ahora tiene una propiedad estática inicial para dar comienzo a la cadena

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

Y eso es prácticamente todo, ahora tiene una una interfaz fluida con parámetros RGB opcionales (siempre que ingrese al menos uno) como bonificación.

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

La cosa con Fluido Interfaces es que no son tan fáciles de armar, pero son fáciles para el desarrollador de código en contra y que son muy extensible. Si desea agregar una bandera de Matt/Gloss a esto sin cambiar todo su código de llamada, es fácil de hacer.

Además, si el proveedor de su API cambia todo lo que hay debajo de usted, solo tiene que volver a escribir este código; todo el código callin puede permanecer igual.

0

Usaría el método de extensión Init porque U siempre puede jugar con el delegado. Infierno Siempre se puede declarar métodos de extensión que ocupan las expresiones e incluso jugar con las expresiones (guardarlos para más adelante, modificar, lo que sea) De esta manera usted puede fácilmente grups de almacenamiento por defecto como:

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

De esta manera usted puede usar todas las acciones (o funcs) y almacenar en caché los inicializadores estándar como cadenas de expresión para más adelante?

0

Si realmente desea poder encadenar las configuraciones de propiedad sin tener que escribir una tonelada de código, una forma de hacerlo sería usar la generación de código (CodeDom). Puede usar Reflection para obtener una lista de las propiedades mutables, generar una clase de generador con fl uido con un método final Build() que devuelve la clase que realmente está tratando de crear.

Voy a omitir todas las cosas repetitivas acerca de cómo registrar la herramienta personalizada; es bastante fácil encontrar documentación en ella, pero aún así es larga y no creo que agregue mucho al incluirla . Sin embargo, te mostraré en lo que estoy pensando para el 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); 
    } 
} 

creo que esto debería sólo de hacerlo - es, obviamente, no probado, pero ¿dónde ir desde aquí es que se crea un codegen (heredando de BaseCodeGeneratorWithSite) que compila una CodeCompileUnit poblada con una lista de tipos. Esa lista proviene del tipo de archivo que registra con la herramienta; en este caso, probablemente solo lo convertiría en un archivo de texto con una lista de tipos delimitada por líneas para la que desea generar código de generador. Haga que la herramienta escanee esto, cargue los tipos (podría tener que cargar los ensamblados primero) y genere bytecode.

Es difícil, pero no es tan difícil como parece, y cuando haya terminado usted será capaz de escribir código como este:

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

que creo que es casi exactamente lo que quiere.Todo lo que tiene que hacer para invocar la generación de código es registrar la herramienta con una extensión personalizada (digamos .buildertypes), poner un archivo con esa extensión en su proyecto, y poner una lista de tipos en ella:

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

Y así. Cuando guarde, generará automáticamente el archivo de código que necesita que admita declaraciones de escritura como la anterior.

He utilizado este enfoque antes para un sistema de mensajes altamente intrincado con cientos de tipos de mensajes diferentes. Me tomaba demasiado tiempo construir siempre el mensaje, establecer un conjunto de propiedades, enviarlo a través del canal, recibir del canal, serializar la respuesta, etc. Usar un codegen simplificó enormemente el trabajo ya que me permitió generar un clase de mensajería única que tomó todas las propiedades individuales como argumentos y escupió una respuesta del tipo correcto. No es algo que recomendaría a todos, pero cuando se trata de proyectos muy grandes, a veces es necesario que comiences a inventar tu propia sintaxis.

Cuestiones relacionadas