unit-testing - patron - inyección de dependencia en c#
¿Cómo usar la Inyección de Dependencia sin romper la encapsulación? (9)
Aquí es donde creo que debe usar contenedores de inyección de dependencia que le permiten encapsular la creación de su automóvil, sin permitir que los llamadores de su cliente necesiten saber cómo crearlo en absoluto. Así es como Symfony resolvió este problema (aunque no es el mismo idioma, los principios siguen siendo los mismos):
http://components.symfony-project.org/dependency-injection/documentation
hay una sección sobre contenedores de inyección de dependencia.
Para hacerlo breve y resumir todo citado directamente de la página de documentación:
Cuando usamos el contenedor, solo solicitamos un objeto de correo [Este sería su automóvil en su ejemplo], y ya no necesitamos saber cómo crearlo; todo el conocimiento sobre cómo crear una instancia del correo [car] ahora está integrado en el contenedor.
Es la esperanza de que te ayude
¿Cómo puedo realizar la inyección de dependencia sin romper la encapsulación?
Usando un ejemplo de Inyección de Dependencia de Wikipedia :
public Car {
public float getSpeed();
}
Nota: Otros métodos y propiedades (por ejemplo, PushBrake (), PushGas (), SetWheelPosition ()) se omiten para mayor claridad
Esto funciona bien; no sabes cómo mi objeto implementa getSpeed
: está " encapsulado ".
En realidad, mi objeto implementa getSpeed
como:
public Car {
private m_speed;
public float getSpeed( return m_speed; );
}
Y todo está bien. Alguien construye mi objeto de Car
, afina los pedales, el claxon, el volante y el auto responde.
Ahora digamos que cambio un detalle de implementación interna de mi auto:
public Car {
private Engine m_engine;
private float m_currentGearRatio;
public float getSpeed( return m_engine.getRpm*m_currentGearRatio; );
}
Todo está bien. El Car
está siguiendo los principios OO correctos, ocultando detalles de cómo se hace algo. Esto libera a la persona que llama para resolver sus problemas, en lugar de tratar de entender cómo funciona un automóvil. También me da la libertad de cambiar mi implementación como mejor me parezca.
Pero la inyección de dependencia me obligaría a exponer mi clase a un objeto Engine
que no creé ni inicié. Peor aún es que ahora he expuesto que mi Car
incluso tiene un motor:
public Car {
public constructor(Engine engine);
public float getSpeed();
}
Y ahora la palabra externa es consciente de que uso un Engine
. No siempre uso un motor, es posible que no quiera usar un Engine
en el futuro, pero ya no puedo cambiar mi implementación interna:
public Car {
private Gps m_gps;
public float getSpeed( return m_gps.CurrentVelocity.Speed; )
}
sin romper la llamada:
public Car {
public constructor(Gps gps);
public float getSpeed();
}
Pero la inyección de dependencia abre toda una lata de gusanos: al abrir toda la lata de gusanos. La Inyección de Dependencia requiere que todos los detalles de implementación privada de mis objetos estén expuestos. El consumidor de mi clase de Car
ahora tiene que entender y lidiar con todas las complejidades internas previamente ocultas de mi clase:
public Car {
public constructor(
Gps gps,
Engine engine,
Transmission transmission,
Tire frontLeftTire, Tire frontRightTire, Tire rearLeftTire, Tire rearRightTire,
Seat driversSeat, Seat passengersSeat, Seat rearBenchSeat,
SeatbeltPretensioner seatBeltPretensioner,
Alternator alternator,
Distributor distributor,
Chime chime,
ECM computer,
TireMonitoringSystem tireMonitor
);
public float getSpeed();
}
¿Cómo puedo usar las virtudes de Dependency Injection para ayudar a las pruebas unitarias, sin romper las virtudes de la encapsulación para ayudar a la usabilidad?
Ver también
- ¿Debe la inyección de dependencia venir a expensas de la encapsulación? (Debe, en lugar de cómo)
En aras de la diversión, puedo recortar el ejemplo getSpeed
a solo lo que se necesita:
public Car {
public constructor(
Engine engine,
Transmission transmission,
Tire frontLeftTire, Tire frontRightTire
TireMonitoringSystem tireMonitor,
UnitConverter unitsConverter
);
public float getSpeed()
{
float tireRpm = m_engine.CurrentRpm *
m_transmission.GetGearRatio( m_transmission.CurrentGear);
float effectiveTireRadius =
(
(m_frontLeftTire.RimSize + m_frontLeftTire.TireHeight / 25.4)
+
(m_frontRightTire.RimSize + m_frontRightTire.TireHeight / 25.4)
) / 2.0;
//account for over/under inflated tires
effectiveTireRadius = effectiveTireRadius *
((m_tireMonitor.FrontLeftInflation + m_tireMontitor.FrontRightInflation) / 2.0);
//speed in inches/minute
float speed = tireRpm * effetiveTireRadius * 2 * Math.pi;
//convert to mph
return m_UnitConverter.InchesPerMinuteToMilesPerHour(speed);
}
}
Actualización: ¿ Tal vez alguna respuesta puede seguir la pista de la pregunta y dar un código de muestra?
public Car {
public float getSpeed();
}
Otro ejemplo es cuando mi clase depende de otro objeto:
public Car {
private float m_speed;
}
En este caso, float
es una clase que se usa para representar un valor de coma flotante. Según lo que leo, todas las clases dependientes deben ser inyectadas, en caso de que quiera burlarse de la clase float
. Esto aumenta el espectro de tener que inyectar a cada miembro privado, ya que todo es fundamentalmente un objeto:
public Car {
public Constructor(
float speed,
float weight,
float wheelBase,
float width,
float length,
float height,
float headRoom,
float legRoom,
DateTime manufactureDate,
DateTime designDate,
DateTime carStarted,
DateTime runningTime,
Gps gps,
Engine engine,
Transmission transmission,
Tire frontLeftTire, Tire frontRightTire, Tire rearLeftTire, Tire rearRightTire,
Seat driversSeat, Seat passengersSeat, Seat rearBenchSeat,
SeatbeltPretensioner seatBeltPretensioner,
Alternator alternator,
Distributor distributor,
Chime chime,
ECM computer,
TireMonitoringSystem tireMonitor,
...
}
Estos realmente son detalles de implementación que no quiero que el cliente tenga que mirar.
Creo que estás rompiendo la encapsulación con tu constructor de Car
. Específicamente, usted está dictando que se debe inyectar un Engine
al Car
lugar de utilizar algún tipo de interfaz para determinar su velocidad ( IVelocity
en el ejemplo siguiente).
Con una interfaz, el Car
puede obtener su velocidad actual independientemente de lo que determine esa velocidad. Por ejemplo:
public Interface IVelocity {
public float getSpeed();
}
public class Car {
private m_velocityObject;
public constructor(IVelocity velocityObject) {
m_velocityObject = velocityObject;
}
public float getSpeed() { return m_velocityObject.getSpeed(); }
}
public class Engine : IVelocity {
private float m_rpm;
private float m_currentGearRatio;
public float getSpeed( return m_rpm * m_currentGearRatio; );
}
public class GPS : IVelocity {
private float m_foo;
private float m_bar;
public float getSpeed( return m_foo * m_bar; );
}
Un motor o GPS puede tener múltiples interfaces basadas en el tipo de trabajo que realiza. La interfaz es clave para DI, sin ella DI rompe la encapsulación.
Fábricas e interfaces.
Tienes un par de preguntas aquí.
- ¿Cómo puedo tener múltiples implementaciones de las mismas operaciones?
- ¿Cómo puedo ocultar los detalles de construcción de un objeto del consumidor de un objeto?
Entonces, lo que necesita es ocultar el código real detrás de una interfaz ICar
, crear un EnginelessCar
separado si alguna vez lo necesita, y usar una interfaz ICarFactory
y una clase CarFactory
para ocultar los detalles de construcción al consumidor del automóvil.
Es probable que esto termine pareciéndose mucho a un marco de inyección de dependencia, pero no es necesario que use uno.
Según mi respuesta en la otra pregunta, si esto rompe o no la encapsulación depende completamente de cómo defina la encapsulación. Hay dos definiciones comunes de encapsulación que he visto:
- Todas las operaciones en una entidad lógica están expuestas como miembros de la clase, y un consumidor de la clase no necesita usar nada más.
- Una clase tiene una responsabilidad única, y el código para administrar esa responsabilidad está contenido dentro de la clase. Es decir, al codificar la clase, puede aislarla efectivamente de su entorno y reducir el alcance del código con el que está trabajando.
(El código como la primera definición puede existir en una base de código que funcione con la segunda condición; simplemente tiende a limitarse a fachadas, y esas fachadas tienden a tener una lógica mínima o nula).
No he usado Delphi en mucho tiempo. La forma en que DI funciona en Spring, tus setters y constructor no son parte de la interfaz. De modo que puede tener múltiples implementaciones de una interfaz, una podría usar inyección basada en el constructor y otra podría usar una inyección basada en setter, a su código que usa la interfaz no le importa. Lo que se inyecta está en el XML de contexto de la aplicación, y ese es el único lugar donde están expuestas sus dependencias.
EDITAR: si usa un marco o no, está haciendo lo mismo, tiene una fábrica que conecta sus objetos. Por lo tanto, sus objetos exponen estos detalles en el constructor o en los instaladores, pero su código de aplicación (fuera de la fábrica, y sin contar las pruebas) nunca los usa. De cualquier forma, elige obtener su gráfico de objetos de la fábrica en lugar de crear instancias sobre la marcha, y elige no hacer cosas como usar arreglos en el código que están allí para ser inyectados. Es un cambio de mentalidad de la filosofía de "clavar todo abajo" que veo en el código de algunas personas.
Si entiendo tus preocupaciones correctamente, estás tratando de evitar que cualquier clase que necesite instanciar un nuevo objeto de Auto tenga que inyectar todas esas dependencias manualmente.
He usado un par de patrones para hacer esto. En los lenguajes con encadenamiento de constructores, he especificado un constructor predeterminado que inyecta los tipos concretos en otro constructor con inyección de dependencias. Creo que esta es una técnica de DI manual bastante estándar.
Otro enfoque que he usado, que permite un acoplamiento más flexible, es crear un objeto de fábrica que configure el objeto DI con las dependencias apropiadas. Luego inyecté esta fábrica en cualquier objeto que necesite "actualizar" algunos automóviles en tiempo de ejecución; esto le permite inyectar implementaciones de Car completamente falsas durante sus pruebas, también.
Y siempre está el enfoque de inyección setter. El objeto tendría valores predeterminados razonables para sus propiedades, que podrían reemplazarse con dobles de prueba según sea necesario. Aunque prefiero la inyección de constructor.
Editar para mostrar un ejemplo de código:
interface ICar { float getSpeed(); }
interface ICarFactory { ICar CreateCar(); }
class Car : ICar {
private Engine _engine;
private float _currentGearRatio;
public constructor(Engine engine, float gearRatio){
_engine = engine;
_currentGearRatio = gearRatio;
}
public float getSpeed() { return return _engine.getRpm*_currentGearRatio; }
}
class CarFactory : ICarFactory {
public ICar CreateCar() { ...inject real dependencies... }
}
Y luego las clases de consumidores solo interactúan con él a través de la interfaz, ocultando por completo a los constructores.
class CarUser {
private ICarFactory _factory;
public constructor(ICarFactory factory) { ... }
void do_something_with_speed(){
ICar car = _factory.CreateCar();
float speed = car.getSpeed();
//...do something else...
}
}
Ahora para algo completamente diferente...
Desea las virtudes de la inyección de dependencia sin romper la encapsulación. Un marco de inyección de dependencias lo hará por usted, pero también hay una "inyección de dependencia de los pobres" disponible para usted a través de un uso creativo de constructores virtuales, el registro de metaclase y la inclusión selectiva de unidades en sus proyectos.
Sin embargo, tiene una limitación importante: solo puede tener una única clase de motor específica en cada proyecto. No hay elección de un motor de elección, aunque pensándolo bien, es probable que te metas con el valor de la variable de clase meta para lograr eso. Pero me estoy adelantando a mí mismo.
Otra limitación es una sola línea de herencia: solo un tronco, sin ramas. Al menos con respecto a las unidades incluidas en un solo proyecto.
Parece que está utilizando Delphi y, por lo tanto, el siguiente método funcionará, ya que es algo que hemos estado utilizando desde D5 en proyectos que necesitan una sola instancia de la clase TBaseX, pero diferentes proyectos necesitan diferentes descendientes de esa clase base y queremos ser capaz de intercambiar clases simplemente descartando una unidad y agregando otra. La solución no está restringida a Delphi sin embargo. Funcionará con cualquier lenguaje que admita constructores virtuales y metaclases.
entonces ¿qué necesitas?
Bueno, cada clase que quiera intercambiar dependiendo de las unidades incluidas por proyecto, necesita tener una variable en algún lugar donde pueda almacenar el tipo de clase para instanciar:
var
_EngineClass: TClass;
Cada clase que implementa un motor debe registrarse en la variable _EngineClass utilizando un método que evite que los antepasados tomen el lugar de un descendiente (por lo que puede evitar la dependencia del orden de inicialización de la unidad):
procedure RegisterMetaClass(var aMetaClassVar: TClass; const aMetaClassToRegister: TClass);
begin
if Assigned(aMetaClassVar) and aMetaClassVar.InheritsFrom(aMetaClassToRegister) then
Exit;
aMetaClassVar := aMetaClassToRegister;
end;
El registro de las clases se puede hacer en una clase base común:
TBaseEngine
protected
class procedure RegisterClass;
class procedure TBaseEngine.RegisterClass;
begin
RegisterMetaClass(_EngineClass, Self);
end;
Cada descendiente se registra llamando al método de registro en la sección de inicialización de su unidad:
type
TConcreteEngine = class(TBaseEngine)
...
end;
initialization
TConcreteEngine.RegisterClass;
Ahora todo lo que necesita es algo para instanciar la clase registrada "más descendiente" en lugar de una clase específica codificada.
TBaseEngine
public
class function CreateRegisteredClass: TBaseEngine;
class function TBaseEngine.CreateRegisteredClass: TBaseEngine;
begin
Result := _EngineClass.Create;
end;
Por supuesto, ahora debería usar siempre esta función de clase para crear instancias de motores y no del constructor normal.
Si lo hace, su código siempre creará una instancia de la clase de motor "más descendiente" presente en su proyecto. Y puede cambiar entre clases al incluir y no las unidades específicas. Por ejemplo, puede asegurarse de que sus proyectos de prueba utilicen las clases simuladas convirtiendo a la clase simulada en un ancestro de la clase real y sin incluir la clase real en el proyecto de prueba; o haciendo que la clase simulada sea un descendiente de la clase real y no incluya la simulación en su código normal; o, incluso más simple, al incluir ya sea el simulacro o la clase real en sus proyectos.
Las clases simuladas y reales tienen un constructor sin parámetros en este ejemplo de implementación. No es necesario que sea el caso, pero necesitará usar una metaclase específica (en lugar de TClass) y algo de conversión en la llamada al procedimiento RegisterMetaClass debido al parámetro var.
type
TBaseEngine = class; // forward
TEngineClass = class of TBaseEngine;
var
_EngineClass: TEngineClass
type
TBaseEngine = class
protected
class procedure RegisterClass;
public
class function CreateRegisteredClass(...): TBaseEngine;
constructor Create(...); virtual;
TConcreteEngine = class(TBaseEngine)
...
end;
TMockEngine = class(TBaseEngine)
...
end;
class procedure TBaseEngine.RegisterClass;
begin
RegisterMetaClass({var}TClass(_EngineClass), Self);
end;
class function TBaseEngine.CreateRegisteredClass(...): TBaseEngine;
begin
Result := _EngineClass.Create(...);
end;
constructor TBaseEngine.Create(...);
begin
// use parameters in creating an instance.
end;
¡Que te diviertas!
No creo que un automóvil sea un ejemplo particularmente bueno de la utilidad real de la inyección de dependencia.
Creo que en el caso de su último ejemplo de código, el propósito de la clase Car no está claro. ¿Es una clase que contiene datos / estado? ¿Es un servicio calcular cosas como la velocidad? ¿O es una mezcla, lo que le permite construir su estado y luego llamar a servicios para hacer cálculos basados en ese estado?
Tal como yo lo veo, la clase Car sería un objeto con estado, cuyo propósito es contener los detalles de su composición, y el servicio para calcular la velocidad (que se podría inyectar, si se desea) sería una clase separada (con un método como " getSpeed(ICar car)
"). Muchos desarrolladores que usan DI tienden a separar los objetos de estado y de servicio, aunque hay casos en los que un objeto tendrá estado y servicio, la mayoría tiende a separarse. Además, la gran mayoría del uso de DI tiende a ser del lado del servicio.
La siguiente pregunta sería: ¿cómo debería componerse la clase de automóvil? ¿La intención es que cada automóvil específico sea solo una instancia de una clase de Automóvil, o hay una clase separada para cada marca y modelo que herede de CarBase o ICar? Si es el primero, entonces debe haber algún medio para establecer / inyectar estos valores en el automóvil: no hay forma de evitar esto, incluso si nunca antes hubieras oído hablar de inversión de dependencia. Si es el último, entonces estos valores son simplemente parte del automóvil, y no veo ninguna razón para querer volverlos settable / inyectables. Todo se reduce a si cosas como Engine y Tires son específicas para la implementación (dependencias duras) o si son composable (dependencias débilmente acopladas).
Entiendo que el automóvil es solo un ejemplo, pero en el mundo real usted será quien sepa si la inversión de dependencias en sus clases viola la encapsulación. Si lo hace, la pregunta que debería hacerse es "¿por qué?" y no "¿cómo?" (que es lo que estás haciendo, por supuesto).
Debe dividir su código en dos fases:
- Construcción del gráfico de objetos para una vida en particular a través de fábrica o solución DI
- Ejecutar estos objetos (lo que implicará entrada y salida)
En la fábrica de automóviles, necesitan saber cómo construir un automóvil. Saben qué tipo de motor tiene, cómo está conectado el claxon, etc. Esta es la fase 1 anterior. La fábrica de automóviles puede construir diferentes automóviles.
Cuando conduce el automóvil, puede conducir cualquier cosa que coincida con la interfaz del automóvil que espera. por ejemplo, pedales, volante, cuerno. Cuando conduce, no conoce los detalles internos cuando presiona el freno. Sin embargo, puede ver el resultado (cambio en la velocidad).
La encapsulación se mantiene ya que nadie que maneje un automóvil necesita saber cómo fue construido. Por lo tanto, puede usar el mismo controlador con muchos autos diferentes. Cuando el disco necesita un automóvil, se les debe dar uno. Si construyen la suya propia cuando se dan cuenta de que la necesitan, la encapsulación se romperá.
Muchas de las otras respuestas lo insinúan, pero voy a decir más explícitamente que sí, las implementaciones ingenuas de la inyección de dependencia pueden romper la encapsulación.
La clave para evitar esto es que el código de llamada no debe instanciar directamente las dependencias (si no se preocupa por ellas). Esto se puede hacer de varias maneras.
El más simple es simplemente tener un constructor predeterminado que haga la inyección con valores predeterminados. Siempre que el código de llamada solo use el constructor predeterminado, puede cambiar las dependencias detrás de las escenas sin afectar el código de llamada.
Esto puede comenzar a salirse de control si sus dependencias tienen dependencias, y así sucesivamente. En ese momento, el patrón de fábrica podría implementarse (o puede usarlo desde el principio para que el código de llamada ya esté usando la fábrica). Si presenta la fábrica y no quiere romper los usuarios existentes de su código, siempre puede llamar a la fábrica desde su constructor predeterminado.
Más allá de eso, está usando Inversion of Control. No he utilizado la IoC lo suficiente como para hablar mucho al respecto, pero hay muchas preguntas aquí, así como artículos en línea que lo explican mucho mejor que yo.
Si realmente debe encapsularse para que el código de llamada no pueda conocer las dependencias, existe la opción de hacer que la inyección (ya sea el constructor con los parámetros de dependencia o los ajustadores) sea internal
si el idioma lo admite o hacer que sean privados y tener su las pruebas unitarias usan algo como Reflection si su lenguaje lo admite. Si tu lenguaje no admite ninguno, entonces supongo que una posibilidad sería que la clase que llama código esté instanciando una clase ficticia que simplemente encapsule la clase, haga el trabajo real (creo que este es el patrón Fachada, pero nunca recuerdo los nombres correctamente) )
public Car {
private RealCar _car;
public constructor(){ _car = new RealCar(new Engine) };
public float getSpeed() { return _car.getSpeed(); }
}