2010-06-04 10 views
5

Estoy pensando en diseñar un método que devolvería un objeto que implementa una interfaz, pero cuyo tipo concreto no se conocerá hasta el tiempo de ejecución. Por ejemplo supongamos:C#: ¿Método para devolver el objeto cuyo tipo concreto se determina en tiempo de ejecución?

ICar 
Ford implements ICar 
Bmw implements ICar 
Toyota implements ICar 

public ICar GetCarByPerson(int personId) 

No sabemos qué coche nos pondremos en contacto hasta que el tiempo de ejecución.

a) Quiero saber qué tipo de coche tiene la persona.

b) dependiendo del tipo de coche concreto que recuperamos, llamaremos a diferentes métodos (porque algunos métodos solo tienen sentido en la clase). Entonces el código del cliente hará algo como.

ICar car = GetCarByPerson(personId); 

if (car is Bmw) 
{ 
    ((Bmw)car).BmwSpecificMethod(); 
} 
else if (car is Toyota) 
{ 
    ((Toyota)car).ToyotaSpecificMethod(); 
} 

¿Es este un buen diseño? ¿Hay un olor a código? ¿Hay una mejor manera de hacer esto?

Estoy bien con el método que devuelve la interfaz, y si el código del cliente llamaba a los métodos de interfaz, obviamente, esto estaría bien. Pero mi preocupación es si el envío de código de cliente a tipos concretos es un buen diseño.

+0

¿Cada ICar tiene este método ... hay alguna razón por la que no puede haber un DoSomething() que exista en la interfaz ICar? –

+2

Es un olor a código. Sin embargo, no eres el primero en utilizar este enfoque, ya que tiene sentido en algunos casos. Es posible que desee dar más detalles si desea obtener mejores respuestas ... muchas de estas decisiones implican concesiones que un simple ejemplo de coche ficticio no puede exponer. – Stephen

Respuesta

11

Usar la palabra clave is en C# (de la manera que ha demostrado anteriormente) es casi siempre un olor a código. Y apesta.

El problema es que ahora se requiere algo que se supone que solo conozca un ICar para realizar un seguimiento de varias clases diferentes que implementan ICar. Mientras esto funciona (ya que produce código que opera), es un diseño pobre. Vas a empezar con sólo un par de coches ...

class Driver 
{ 
    private ICar car = GetCarFromGarage(); 

    public void FloorIt() 
    { 
     if (this.car is Bmw) 
     { 
      ((Bmw)this.car).AccelerateReallyFast(); 
     } 
     else if (this.car is Toyota) 
     { 
      ((Toyota)this.car).StickAccelerator(); 
     } 
     else 
     { 
      this.car.Go(); 
     } 
    } 
} 

Y más tarde, otra coche va a hacer algo especial cuando FloorIt. Y agregará esa función al Driver, y pensará en los otros casos especiales que deben manejarse, y perderá veinte minutos rastreando cada lugar donde hay un if (car is Foo), ya que está disperso por todo el código. base de ahora - en el interior Driver, dentro Garage, dentro ParkingLot ... (estoy hablando de la experiencia en el trabajo en el código heredado aquí.)

Cuando usted se encuentra haciendo una declaración como if (instance is SomeObject), deténgase y pregúntese por qué este comportamiento especial necesita ser manejado aquí. La mayoría de las veces, puede ser un método nuevo en la clase de interfaz/resumen, y simplemente puede proporcionar una implementación predeterminada para las clases que no son "especiales".

Eso no quiere decir que nunca deba verificar los tipos con is; sin embargo, debes ser muy cuidadoso en esta práctica porque tiene una tendencia a salirse de control y ser abusado a menos que se mantenga bajo control.


Ahora, supongamos que ha determinado que usted debe escribir de manera concluyente a revisar su ICar. El problema con el uso is es que las herramientas de análisis de código estático le advierten sobre fundición en dos ocasiones, cuando lo hace

if (car is Bmw) 
{ 
    ((Bmw)car).ShiftLanesWithoutATurnSignal(); 
} 

el impacto en el rendimiento es probablemente insignificante a menos que sea en un bucle interno, pero la mejor forma de escribir esto es

var bmw = car as Bmw; 
if (bmw != null) // careful about overloaded == here 
{ 
    bmw.ParkInThreeSpotsAtOnce(); 
} 

Esto requiere solo un molde (internamente) en lugar de dos.

Si no desea ir por ese camino, otro enfoque limpio es simplemente usar una enumeración:

enum CarType 
{ 
    Bmw, 
    Toyota, 
    Kia 
} 

interface ICar 
{ 
    void Go(); 

    CarType Make 
    { 
     get; 
    } 
} 

seguido por

if (car.Make == CarType.Kia) 
{ 
    ((Kia)car).TalkOnCellPhoneAndGoFifteenUnderSpeedLimit(); 
} 

Usted puede rápidamente switch en una enumeración, y te permite saber (hasta cierto punto) el límite concreto de lo que se pueden usar los automóviles.

Una desventaja de usar una enumeración es que CarType está grabado en piedra; si otro ensamblado (externo) depende de ICar y agregaron el nuevo auto Tesla, no podrán agregar un tipo Tesla a CarType. Los enumeradores tampoco se prestan bien a las jerarquías de clases: si desea Chevy para ser CarType.Chevyy a CarType.GM, o bien tiene que usar la enumeración como indicadores (feo en este caso) o asegúrese de marcar Chevy antes GM, o tiene muchos || s en sus cheques contra las enumeraciones.

+2

+1 para 'Toyota.StickAccelerator()' –

0

Usted querría simplemente un método virtual, SpecificationMethod, que se implementa en cada clase. Recomiendo leer el contenido de FAQ Lite en inheritence. El método de diseño que menciona también se puede aplicar a .Net.

+1

No creo que estés equivocado, pero creo que es un poco presuntuoso. Claramente, el OP tiene un problema donde "algunos métodos solo tienen sentido en [ciertas clases]". – Stephen

0

Una mejor solución hubiera hecho que ICar declarara un GenericCarMethod() y tuviera Bmw y Toyota que lo anularan. En general, no es una buena práctica de diseño confiar en downcasting si puedes evitarlo.

8

Este es un clásico problema de envío doble y tiene un patrón aceptable para resolverlo (Patrón de visitante).

//This is the car operations interface. It knows about all the different kinds of cars it supports 
//and is statically typed to accept only certain ICar subclasses as parameters 
public interface ICarVisitor { 
    void StickAccelerator(Toyota car); //credit Mark Rushakoff 
    void ChargeCreditCardEveryTimeCigaretteLighterIsUsed(Bmw car); 
} 

//Car interface, a car specific operation is invoked by calling PerformOperation 
public interface ICar { 
    public string Make {get;set;} 
    public void PerformOperation(ICarVisitor visitor); 
} 

public class Toyota : ICar { 
    public string Make {get;set;} 
    public void PerformOperation(ICarVisitor visitor) { 
    visitor.StickAccelerator(this); 
    } 
} 

public class Bmw : ICar{ 
    public string Make {get;set;} 
    public void PerformOperation(ICarVisitor visitor) { 
    visitor.ChargeCreditCardEveryTimeCigaretteLighterIsUsed(this); 
    } 
} 

public static class Program { 
    public static void Main() { 
    ICar car = carDealer.GetCarByPlateNumber("4SHIZL"); 
    ICarVisitor visitor = new CarVisitor(); 
    car.PerformOperation(visitor); 
    } 
} 
+0

+1: Buena demostración limpia del patrón Visitor. Deseo * que hayan usado esto en el código que tengo que mantener. –

+2

Ja, me gustaría utilizar este tipo de código que ** I ** escribió que tengo que mantener: D –

+0

¡Gran ejemplo! Sería bueno adjuntar un ejemplo para la instanciación del ICarVisitor correcto. – bloparod

Cuestiones relacionadas