2009-06-03 10 views
9

Teniendo en cuenta el siguiente código de ejemplo:Pros y contras de 'nuevas' propiedades en C#/.Net?

// delivery strategies 
public abstract class DeliveryStrategy { ... } 
public class ParcelDelivery : DeliveryStrategy { ... } 
public class ShippingContainer : DeliveryStrategy { ... } 

y la siguiente clase de muestra Orden:

// order (base) class 
public abstract class Order 
{ 
    private DeliveryStrategy delivery; 

    protected Order(DeliveryStrategy delivery) 
    { 
     this.delivery = delivery; 
    } 

    public DeliveryStrategy Delivery 
    { 
     get { return delivery; } 
     protected set { delivery = value; } 
    } 
} 

Cuando derivo un nuevo tipo de clase de orden, que heredarán la propiedad Entrega de tipo DeliveryStrategy.

Ahora, cuando se da que CustomerOrders deben ser entregados utilizando la estrategia ParcelDelivery, podríamos considerar 'nueva' ing la propiedad de entrega en la clase CustomerOrder:

public class CustomerOrder : Order 
{ 
    public CustomerOrder() 
     : base(new ParcelDelivery()) 
    { } 

    // 'new' Delivery property 
    public new ParcelDelivery Delivery 
    { 
     get { return base.Delivery as ParcelDelivery; } 
     set { base.Delivery = value; } 
    } 
} 

(El CustomerOrder necesita, obviamente, a asegúrese de que sea compatible (polimorfo) con el pedido)

Esto permite el uso directo de la estrategia de distribución de paquetes en CustomerOrder sin la necesidad de fundición.

¿Consideraría utilizar este patrón? ¿por qué por qué no?

Actualización: se me ocurrió este patrón, en lugar de usar genéricos, porque quiero usar esto para múltiples propiedades. No deseo utilizar argumentos de tipo genérico para todas estas propiedades

+2

Siempre que su método de sombreado no tenga condiciones previas más estrictas ni condiciones posteriores más débiles, creo que esta práctica es perfectamente aceptable. –

+0

En la primera instancia, ¿por qué necesitarías lanzar la estrategia de entrega de todos modos? El punto de utilizar el patrón de estrategia es que los comportamientos implementan una interfaz y, por lo tanto, tienen todos los mismos métodos, por lo que no es necesario transmitirlos. –

Respuesta

6

Creo que este es un buen patrón.Hace que sea más fácil utilizar explícitamente los tipos derivados al eliminar la necesidad de emitir el resultado, y no 'rompe' el comportamiento de la clase base. En realidad, un patrón similar se utiliza en algunas clases en el BCL, por ejemplo mirar la jerarquía de clases DbConnection:

  • DbConnection.CreateCommand() devuelve un DbCommand
  • SqlConnection.CreateCommand() oculta la implementación base usando 'nuevo' para devolver un SqlCommand.
  • (otras implementaciones DbConnection hacen lo mismo)

lo tanto, si se manipula el objeto de conexión a través de una variable DbConnection, CreateCommand devolverá un DbCommand; si lo manipula a través de una variable SqlConnection, CreateCommand devolverá un SqlCommand, evitando el reparto si lo está asignando a una variable SqlCommand.

+0

Aceptado como respuesta, para mostrar casos simulares del framework .Net (aunque no usan 'new' en SqlConnection!) –

+1

"aunque no usan 'nuevo' en SqlConnection": en realidad estoy casi seguro de que do: es la única forma de definir un método con el mismo nombre y parámetro que en la clase base, pero con un tipo de devolución diferente. Supongo que consultaron con Reflector, pero el código que muestra Reflector no es el código fuente real: solo puede mostrar lo que se generó realmente en IL, y aparentemente el modificador "nuevo" no emite ningún IL (o al menos Reflector no lo hace). no lo demuestro). –

+0

el uso de nuevo en el código fuente simplemente evita que el compilador le advierta que lo está haciendo por usted de todos modos (eso podría cambiar por supuesto) – ShuggyCoUk

5

Puede usar genéricos.

// order (base) class 
public abstract class Order<TDeliveryStrategy> where TDeliveryStrategy : DeliveryStrategy 
{ 
    private TDeliveryStrategy delivery; 

    protected Order(TDeliveryStrategy delivery) 
    { 
     this.delivery = delivery; 
    } 

    public TDeliveryStrategy Delivery 
    { 
     get { return delivery; } 
     protected set { delivery = value; } 
    } 
} 

public class CustomerOrder : Order<ParcelDelivery> 
{ 
    public CustomerOrder() 
     : base(new ParcelDelivery()) 
    { } 
} 
11

preferiría hacer el tipo genérico:

public abstract class Order<TDelivery> where TDelivery : Delivery 
{ 
    public TDelivery Delivery { ... } 
    ... 
} 

public class CustomerOrder : Order<ParcelDelivery> 
{ 
    ... 
} 

este modo se garantiza la seguridad de tipos en tiempo de compilación, en lugar de dejarlo hasta el tiempo de ejecución. También previene la situación de:

CustomerOrder customerOrder = new CustomerOrder(); 
Order order = customerOrder; 
order.Delivery = new NonParcelDelivery(); // Succeeds! 

ParcelDelivery delivery = customerOrder.Delivery; // Returns null 

Ouch.

Considero que new suele ser el último recurso. Introduce una complejidad añadida tanto en términos de implementación como de uso.

Si no quiere seguir la ruta genérica, introduciría una propiedad genuinamente nueva (con un nombre diferente).

+0

No puedo esperar a C# 4 con contravarianza en los tipos de devolución. – erikkallen

+0

Tipos de retorno covariantes, tipos de parámetros contravariantes :) Sin embargo, eso es solo para interfaces y solo en ciertos escenarios, hasta donde yo sé ... –

+0

@Jon: hice el conjunto de propiedades protegido, para este caso exacto. La clase base y la clase derivada deben trabajar juntas para lograr esto. –

1

¿Hay alguna razón por la cual deba cambiar el tipo de devolución? Si no hay entonces sugeriría simplemente haciendo que la propiedad de entrega virtual, de modo que tiene que ser definido por las clases heredadas en su lugar:

public abstract class Order 
{ 
    protected Order(DeliveryStrategy delivery) 
    { 
     Delivery = delivery; 
    } 

    public virtual DeliveryStrategy Delivery { get; protected set; } 
} 

public class CustomerOrder : Order 
{ 
    public CustomerOrder() 
     : base(new ParcelDelivery()) 
    { } 

    public DeliveryStrategy Delivery { get; protected set; } 
} 

Si requiere el cambio en el tipo de retorno, entonces se preguntaría por qué lo haría necesita ese cambio drástico de comportamiento en el tipo de devolución. De todos modos, si eso es así, entonces esto no funcionará para ti.

Para responder directamente a su pregunta, solo usaría el patrón que ha descrito si es necesario que el tipo de devolución sea diferente de la clase base, y con moderación (analizaría mi modelo de objetos para ver si hay otra cosa que podría hacer primero). Si ese no es el caso, entonces usaría el patrón que he descrito arriba.

3

Usar la palabra clave 'nueva' para ocultar las propiedades de escritura de la clase base es una mala idea en mi opinión. La palabra clave nueva le permite ocultar un miembro de una clase base en una clase derivada, en lugar de anularla. Esto significa que las llamadas a dichos miembros que utilizan una referencia de clase base todavía acceden al código de clase base, no al código de clase derivado. C# tiene la palabra clave 'virtual', que permite que las clases derivadas realicen la implementación, en lugar de simplemente ocultarla. Hay un artículo razonablemente bueno, here, que habla sobre las diferencias.

En su caso, parece que está intentando utilizar el método hide para introducir property covariance en C#. Sin embargo, hay problemas con este enfoque.

A menudo, el valor de tener una clase base es permitir que los usuarios de su código puedan tratar tipos polimórficamente. ¿Qué sucede con su implementación si alguien establece la propiedad Entrega usando una referencia a la clase base? ¿Se romperá la clase derivada si la propiedad de entrega NO es una instancia de ParcelDelivery? Estos son los tipos de preguntas que debe hacerse sobre esta elección de implementación.

Ahora, si la propiedad Entrega en la clase base no proporcionó un colocador, entonces tiene una situación ligeramente diferente. Los usuarios de la clase base solo pueden recuperar la propiedad y no establecerla. Dado que enrutas el acceso de tu propiedad a la clase base, el acceso a través de la clase base continúa funcionando. Sin embargo, si su clase derivada no está sellada, las clases heredadas de ella podrían presentar los mismos tipos de problemas al ocultar la propiedad Entrega con una versión propia.

Como algunas de las otras publicaciones ya han mencionado, puede usar genéricos como forma de lograr diferentes tipos de propiedad de entrega. El ejemplo de Jon es bastante bueno para demostrar esto. Hay un problema con el enfoque de los genéricos, si necesita derivar de CustomerOrder y cambiar la propiedad de entrega a un nuevo tipo.

Existe una alternativa a los genéricos. Debe considerar si realmente desea una propiedad configurable en su caso. Si se deshace del colocador en la propiedad Entrega, los problemas introducidos al usar la clase de Orden desaparecen directamente. Dado que configura la propiedad de entrega utilizando parámetros de constructor, puede garantizar que todas las órdenes tengan el tipo de estrategia adecuado.

1

considerar este enfoque:

public interface IOrder 
{ 
    public DeliveryStrategy Delivery 
    { 
     get; 
    } 
} 

// order (base) class 
public abstract class Order : IOrder 
{ 
    protected readonly DeliveryStrategy delivery; 

    protected Order(DeliveryStrategy delivery) 
    { 
     this.delivery = delivery; 
    } 

    public DeliveryStrategy Delivery 
    { 
     get { return delivery; } 
    } 
} 

a continuación, utilizar

public class CustomerOrder : Order 
{ 
    public CustomerOrder() 
     : base(new ParcelDelivery()) 
    { } 

    public ParcelDelivery Delivery 
    { 
     get { return (ParcelDelivery)base.Delivery; } 
    } 


    DeliveryStrategy IOrder.Delivery 
    { 
     get { return base.Delivery} 
    } 
} 

Esto todavía está lejos de ser perfecto (el ejemplo no muestra qué clase de la base tiene que saber acerca de la estrategia de entrega en absoluto y tendría mucho más sentido ser genérico con una restricción, pero esto al menos le permite usar el mismo nombre para la propiedad y obtener seguridad tipo.

El como en su ejemplo no tenía sentido, si algo es alguna vez no el tipo correcto no debe enmascararlo con nulo, debe lanzar ya que su invariante ha sido violado.

siempre son preferibles los campos de solo lectura siempre que sea posible. Ellos hacen que la inmutabilidad sea clara.

+0

Me gusta la alternativa que ofrecen las interfaces, pero creo que este patrón hace que el modelo sea más complejo (tiene dos propiedades de entrega en la misma clase). –

+0

En ausencia de varianza co/contra, debe hacerlo usted mismo, ya sea en el extremo consumidor (diferentes nombres/moldes) o en el proveedor (dos propiedades mutuas como la mía o sombreada) En todos los escenarios de proveedores habrá dos propiedades (simplemente no lo ve con 'nuevo') – ShuggyCoUk

1

Su solución no hace lo que usted piensa que hace. Parece que funciona pero no está llamando a su "nuevo" método. Tenga en cuenta los siguientes cambios en el código para añadir un poco de salida para ver qué método se llama:

// order (base) class 
public abstract class Order 
{ 
    private DeliveryStrategy delivery; 

    protected Order(DeliveryStrategy delivery) 
    { 
     this.delivery = delivery; 
    } 

    public DeliveryStrategy Delivery 
    { 
     get 
     { 
      Console.WriteLine("Order"); 
      return delivery; 
     } 
     protected set { delivery = value; } 
    } 
} 

public class CustomerOrder : Order 
{ 
    public CustomerOrder() 
     : base(new ParcelDelivery()) 
    { } 

    // 'new' Delivery property 
    public new ParcelDelivery Delivery 
    { 
     get 
     { 
      Console.WriteLine("CustomOrder"); 
      return base.Delivery as ParcelDelivery; 
     } 
     set { base.Delivery = value; } 
    } 
} 

A continuación, el siguiente fragmento de código a utilizar realmente la clase CustomOrder:

Order o = new CustomerOrder(); 
var d = o.Delivery; 

salida sería la "Orden ". El nuevo método rompe específicamente el polimorfismo. Crea una nueva propiedad de Entrega en CustomOrder que no es parte de la clase base de la Orden. Por lo tanto, cuando utiliza su CustomOrder como si fuera un pedido, no llama a la nueva propiedad de entrega porque solo existe en CustomOrder y no forma parte de la clase Order.

Lo que intenta hacer es anular un método que no se puede descartar. Si se refiere a la propiedad de ser reemplazable, lo convierten en abstracto:

// order (base) class 
public abstract class Order 
{ 
    private DeliveryStrategy delivery; 

    protected Order(DeliveryStrategy delivery) 
    { 
     this.delivery = delivery; 
    } 

    public abstract DeliveryStrategy Delivery 
    { 
     get { return delivery; } 
     protected set { delivery = value; } 
    } 
} 

public class CustomerOrder : Order 
{ 
    public CustomerOrder() 
     : base(new ParcelDelivery()) 
    { } 

    public override ParcelDelivery Delivery 
    { 
     get { return base.Delivery as ParcelDelivery; } 
     set { base.Delivery = value; } 
    } 
} 
+0

Hace wat espero que lo haga. Cambie Console.WriteLine ("Order") a Console.WriteLine (delivery.GetType(). GetName()) (o algo así) y verá ;-) –

+0

delivery.GetType() le dará ParcelDelivery porque usted se lo pasó al constructor de Order, no porque está llamando a la nueva propiedad Delivery de la subclase. Si desea usar el reflejo para ver dónde se encuentra, qué método está llamando en realidad, cambie el writeline a: Console.WriteLine (System.Reflection.MethodBase.GetCurrentMethod(). DeclaringType.FullName); – ZombieDev

0

Usando new a la sombra de los miembros virtuales de una clase base es una mala idea, porque los tipos de sub-derivada no serán capaces de reemplazar de manera adecuada. Si existen miembros de la clase que se desea sombrear en las clases derivadas, los miembros de la clase base no se deben declarar como abstract o virtual, sino que simplemente deben llamar a un miembro protected abstract o protected virtual. Un tipo derivado puede sombrear el método de clase base con uno que llame al miembro protected apropiado y arroje el resultado de manera apropiada.