patterns pattern dofactory book c# design-patterns code-reuse

dofactory - facade design pattern c#



¿Cómo implementaría un patrón de diseño de "rasgo" en C#? (7)

Sé que la característica no existe en C #, pero PHP recientemente agregó una característica llamada Traits que pensé que era un poco tonto al principio hasta que comencé a pensar en ello.

Supongamos 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, está en la clase base.

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

Con los rasgos, podemos hacer que los rasgos de las características de peso y altura sean los siguientes:

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 pelusa extra a la clase. Si mi cliente vuelve más tarde y dice "Ah, realmente me gusta esa característica, ¿puedo tenerla también?", Simplemente actualizo la definición de 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 los métodos asociados, y no quiero volver a implementarlos 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 estoy refiriendo a una clase de programación, cada uno de mis clientes tiene clientes sobre los que desea registrar información)


Basándome en 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 agradable como me gustaría, pero tampoco está mal.


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

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

Considere que los Rasgos, al menos en el sentido de Scala, pueden ser considerados como "bastante mágicos para compilar 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 usar interfaces y "muchos métodos manuales de proxy" (por ejemplo, composición).

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

Feliz codificación.


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 propiedades de dependencia para esto?

Esto tendría el efecto de hacer que las clases del cliente sean livianas en el tiempo de ejecución cuando tiene muchas propiedades que no siempre configuran todos los descendientes. 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; } }


Esto suena como la versión de PHP de la Programación Orientada a Aspectos. En algunos casos, existen herramientas para ayudar como PostSharp o MS Unity. Si desea rodar su propio código, la inyección de código usando C # Attributes es un enfoque, o como métodos de extensión sugeridos para casos limitados.

Realmente depende de lo complicado que desea obtener. Si está tratando de construir algo complejo, estaría buscando algunas de estas herramientas para ayudar.


Existe 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 los rasgos del lenguaje C #.

Eche un vistazo al documento (PDF) en CSharpT para obtener una descripción completa de lo que ha hecho, basado en el compilador 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 utiliza un compilador posterior para reescribir el IL e inyectar los métodos en una clase. Esto le permite escribir un código como ese:

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<RSwitchable>, Does<RTunable> { }

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

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

Para utilizar la radio directamente antes de volver a grabar (es decir, en el mismo ensamblaje donde se declara el tipo de Radio ), debe recurrir a los métodos de extensiones As<R> ():

radio.As<RSwitchable>().TurnOn(); radio.As<RTunable>().Seek(42);

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


Puede obtener la sintaxis mediante el uso de interfaces de marcadores y métodos de extensión.

Requisito previo: las interfaces necesitan definir el contrato que luego será utilizado por 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 esto:

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

Editar: surgió la pregunta de cómo se pueden almacenar datos adicionales. Esto también se puede abordar haciendo algo de codificación adicional:

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<string, object> data = new Dictionary<string, object>(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 rasgo pueden agregar y recuperar datos si la "interfaz de rasgo" 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 dinámicos a través del DLR, haciendo que el acceso a las propiedades adicionales sea transparente cuando se usa con la palabra clave dynamic .