¿Cómo implementarías un patrón de diseño de “rasgo” en C #?

Sé que la característica no existe en C #, pero PHP recientemente agregó una característica llamada Traits que al principio me pareció un poco tonta hasta que empecé a pensar en ello.

Digamos que tengo una clase base llamada Client . Client tiene una sola propiedad llamada Name .

Ahora estoy desarrollando una aplicación reutilizable que será utilizada por muchos clientes diferentes. Todos los clientes aceptan que un cliente debe tener un nombre, por lo tanto, se encuentra en la clase base.

Ahora viene el Cliente A y dice que también necesita hacer un seguimiento del peso del cliente. El cliente B no necesita el peso, pero quiere seguir la altura. El cliente C quiere rastrear tanto el peso como la altura.

Con los rasgos, podríamos realizar los rasgos de las características Peso y Altura:

 class ClientA extends Client use TClientWeight class ClientB extends Client use TClientHeight class ClientC extends Client use TClientWeight, TClientHeight 

Ahora puedo satisfacer todas las necesidades de mis clientes sin agregar ninguna pelusa adicional a la clase. Si mi cliente vuelve más tarde y dice “Oh, realmente me gusta esa función, ¿puedo tenerla también?”, Simplemente actualizo la definición de la clase para incluir el rasgo adicional.

¿Cómo lograrías esto en C #?

Las interfaces no funcionan aquí porque quiero definiciones concretas para las propiedades y cualquier método asociado, y no quiero volver a implementarlas para cada versión de la clase.

(Por “cliente”, me refiero a una persona literal que me ha contratado como desarrollador, mientras que por “cliente” me refiero a una clase de progtwigción; cada uno de mis clientes tiene clientes sobre los que desean registrar información)

    Puede obtener la syntax utilizando interfaces de marcador y métodos de extensión.

    Requisito previo: las interfaces deben definir el contrato que luego se usa con el método de extensión. Básicamente, la interfaz define el contrato para poder “implementar” un rasgo; idealmente, la clase donde se agrega la interfaz ya debería tener todos los miembros de la interfaz presentes para que no se requiera una implementación adicional.

     public class Client { public double Weight { get; } public double Height { get; } } public interface TClientWeight { double Weight { get; } } public interface TClientHeight { double Height { get; } } public class ClientA: Client, TClientWeight { } public class ClientB: Client, TClientHeight { } public class ClientC: Client, TClientWeight, TClientHeight { } public static class TClientWeightMethods { public static bool IsHeavierThan(this TClientWeight client, double weight) { return client.Weight > weight; } // add more methods as you see fit } public static class TClientHeightMethods { public static bool IsTallerThan(this TClientHeight client, double height) { return client.Height > height; } // add more methods as you see fit } 

    Use así:

     var ca = new ClientA(); ca.IsHeavierThan(10); // OK ca.IsTallerThan(10); // compiler error 

    Edición: Se planteó la pregunta de cómo podrían almacenarse datos adicionales. Esto también puede abordarse haciendo algunos códigos adicionales:

     public interface IDynamicObject { bool TryGetAttribute(string key, out object value); void SetAttribute(string key, object value); // void RemoveAttribute(string key) } public class DynamicObject: IDynamicObject { private readonly Dictionary data = new Dictionary(StringComparer.Ordinal); bool IDynamicObject.TryGetAttribute(string key, out object value) { return data.TryGet(key, out value); } void IDynamicObject.SetAttribute(string key, object value) { data[key] = value; } } 

    Y luego, los métodos de rasgos pueden agregar y recuperar datos si la “interfaz de rasgos” se hereda de IDynamicObject :

     public class Client: DynamicObject { /* implementation see above */ } public interface TClientWeight, IDynamicObject { double Weight { get; } } public class ClientA: Client, TClientWeight { } public static class TClientWeightMethods { public static bool HasWeightChanged(this TClientWeight client) { object oldWeight; bool result = client.TryGetAttribute("oldWeight", out oldWeight) && client.Weight.Equals(oldWeight); client.SetAttribute("oldWeight", client.Weight); return result; } // add more methods as you see fit } 

    Nota: al implementar IDynamicMetaObjectProvider , el objeto incluso permitiría exponer los datos dynamics a través del DLR, haciendo transparente el acceso a las propiedades adicionales cuando se utiliza con la palabra clave dynamic .

    El lenguaje C # (al menos a la versión 5) no tiene soporte para Rasgos.

    Sin embargo, Scala tiene rasgos y Scala se ejecuta en la JVM (y CLR). Por lo tanto, no es una cuestión de tiempo de ejecución, sino simplemente de la lengua.

    Tenga en cuenta que los rasgos, al menos en el sentido de Scala, pueden considerarse como “bastante magia para comstackr en métodos proxy” ( no afectan al MRO, que es diferente de Mixins en Ruby). En C #, la forma de obtener este comportamiento sería utilizar interfaces y “muchos métodos de proxy manual” (por ejemplo, composición).

    Este proceso tedioso se podría realizar con un procesador hipotético (¿quizás generación automática de código para una clase parcial a través de plantillas?), Pero eso no es C #.

    Feliz codificacion

    Hay un proyecto académico, desarrollado por Stefan Reichart, del Software Composition Group de la Universidad de Berna (Suiza), que proporciona una verdadera implementación de rasgos en el lenguaje C #.

    Eche un vistazo al documento (PDF) en CSharpT para obtener una descripción completa de lo que ha hecho, basado en el comstackdor mono.

    Aquí hay una muestra de lo que se puede escribir:

     trait TCircle { public int Radius { get; set; } public int Surface { get { ... } } } trait TColor { ... } class MyCircle { uses { TCircle; TColor } } 

    Me gustaría señalar NRoles , un experimento con roles en C #, donde los roles son similares a los rasgos .

    NRoles usa un post-comstackdor para reescribir el IL e inyectar los métodos en una clase. Esto te permite escribir código así:

     public class RSwitchable : Role { private bool on = false; public void TurnOn() { on = true; } public void TurnOff() { on = false; } public bool IsOn { get { return on; } } public bool IsOff { get { return !on; } } } public class RTunable : Role { public int Channel { get; private set; } public void Seek(int step) { Channel += step; } } public class Radio : Does, Does { } 

    donde clase Radio implementa RSwitchable y RTunable . Detrás de escena, Does es una interfaz sin miembros, así que básicamente Radio comstack a una clase vacía. La reescritura de IL posterior a la comstackción inyecta los métodos de RSwitchable y RTunable en Radio , que luego se pueden usar como si realmente derivaran de los dos roles (de otro conjunto):

     var radio = new Radio(); radio.TurnOn(); radio.Seek(42); 

    Para usar la radio directamente antes de que ocurra la reescritura (es decir, en el mismo ensamblaje donde se declara el tipo de Radio ), debe recurrir a los métodos de extensiones As ():

     radio.As().TurnOn(); radio.As().Seek(42); 

    ya que el comstackdor no permitiría llamar a TurnOn o Seek directamente en la clase de Radio .

    Esta es realmente una extensión sugerida a la respuesta de Lucero donde todo el almacenamiento estaba en la clase base.

    ¿Qué hay de usar las propiedades de dependencia para esto?

    Esto tendría el efecto de hacer que las clases de clientes sean livianas en el tiempo de ejecución cuando tiene muchas propiedades que no siempre son establecidas por cada descendiente. Esto se debe a que los valores se almacenan en un miembro estático.

     using System.Windows; public class Client : DependencyObject { public string Name { get; set; } public Client(string name) { Name = name; } //add to descendant to use //public double Weight //{ // get { return (double)GetValue(WeightProperty); } // set { SetValue(WeightProperty, value); } //} public static readonly DependencyProperty WeightProperty = DependencyProperty.Register("Weight", typeof(double), typeof(Client), new PropertyMetadata()); //add to descendant to use //public double Height //{ // get { return (double)GetValue(HeightProperty); } // set { SetValue(HeightProperty, value); } //} public static readonly DependencyProperty HeightProperty = DependencyProperty.Register("Height", typeof(double), typeof(Client), new PropertyMetadata()); } public interface IWeight { double Weight { get; set; } } public interface IHeight { double Height { get; set; } } public class ClientA : Client, IWeight { public double Weight { get { return (double)GetValue(WeightProperty); } set { SetValue(WeightProperty, value); } } public ClientA(string name, double weight) : base(name) { Weight = weight; } } public class ClientB : Client, IHeight { public double Height { get { return (double)GetValue(HeightProperty); } set { SetValue(HeightProperty, value); } } public ClientB(string name, double height) : base(name) { Height = height; } } public class ClientC : Client, IHeight, IWeight { public double Height { get { return (double)GetValue(HeightProperty); } set { SetValue(HeightProperty, value); } } public double Weight { get { return (double)GetValue(WeightProperty); } set { SetValue(WeightProperty, value); } } public ClientC(string name, double weight, double height) : base(name) { Weight = weight; Height = height; } } public static class ClientExt { public static double HeightInches(this IHeight client) { return client.Height * 39.3700787; } public static double WeightPounds(this IWeight client) { return client.Weight * 2.20462262; } } 

    Sobre la base de lo que Lucero sugirió , se me ocurrió esto:

     internal class Program { private static void Main(string[] args) { var a = new ClientA("Adam", 68); var b = new ClientB("Bob", 1.75); var c = new ClientC("Cheryl", 54.4, 1.65); Console.WriteLine("{0} is {1:0.0} lbs.", a.Name, a.WeightPounds()); Console.WriteLine("{0} is {1:0.0} inches tall.", b.Name, b.HeightInches()); Console.WriteLine("{0} is {1:0.0} lbs and {2:0.0} inches.", c.Name, c.WeightPounds(), c.HeightInches()); Console.ReadLine(); } } public class Client { public string Name { get; set; } public Client(string name) { Name = name; } } public interface IWeight { double Weight { get; set; } } public interface IHeight { double Height { get; set; } } public class ClientA : Client, IWeight { public double Weight { get; set; } public ClientA(string name, double weight) : base(name) { Weight = weight; } } public class ClientB : Client, IHeight { public double Height { get; set; } public ClientB(string name, double height) : base(name) { Height = height; } } public class ClientC : Client, IWeight, IHeight { public double Weight { get; set; } public double Height { get; set; } public ClientC(string name, double weight, double height) : base(name) { Weight = weight; Height = height; } } public static class ClientExt { public static double HeightInches(this IHeight client) { return client.Height * 39.3700787; } public static double WeightPounds(this IWeight client) { return client.Weight * 2.20462262; } } 

    Salida:

     Adam is 149.9 lbs. Bob is 68.9 inches tall. Cheryl is 119.9 lbs and 65.0 inches. 

    No es tan bonito como me gustaría, pero tampoco es tan malo.

    Esto suena como la versión de PHP de Aspect Oriented Programming. Hay herramientas para ayudar como PostSharp o MS Unity en algunos casos. Si desea reinvertir su propio código, la inyección con Atributos de C # es un enfoque, o como métodos de extensión sugeridos para casos limitados.

    Realmente depende de lo complicado que quieras conseguir. Si estás tratando de construir algo complejo, estaría buscando algunas de estas herramientas para ayudarte.