Pregunta ¿Cómo usar la Inyección de Dependencia sin romper la encapsulación?


¿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 ()) omitido para   claridad

Esto funciona bien; no sabes cómo se implementa mi objeto getSpeed - es "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 Car objeto, aplasta los pedales, la bocina, el volante y el automóvil 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. los Car sigue los principios OO correctos, ocultando detalles de cómo algo está hecho. 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 una Engine objeto 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. Inyección de Dependencia requiere que todos mis objetos privado los detalles de implementación estarán expuestos. El consumidor de mi Car la clase ahora tiene que entender y manejar 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


Por el bien de la diversión, puedo recortar el getSpeed ejemplo a 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);
   }
}

Actualizar: Tal vez alguna respuesta puede seguir el ejemplo 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. Por lo que leí, cada clase dependiente debe ser inyectada, en caso de que quiera burlarse de la float clase. 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.


32
2017-09-29 15:24


origen


Respuestas:


Muchas de las otras respuestas lo insinúan, pero voy a decir más explícitamente que sí, implementaciones ingenuas de inyección de dependencia poder 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 hace 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 demasiado al respecto, pero hay muchas preguntas aquí, así como artículos en línea que lo explican mucho mejor que yo.

Si debería estar realmente encapsulado en donde el código de llamada no poder saber sobre las dependencias, entonces existe la opción de hacer la inyección (ya sea el constructor con los parámetros de dependencia o los establecedores) internal si el idioma lo admite, o haciéndolos privados y con las pruebas de su unidad, use algo como Reflection si su idioma 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(); }
}

13
2017-09-29 19:37



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 de 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...
  }
}

7
2017-09-29 16:22



Creo que estás rompiendo la encapsulación con tu Car constructor. Específicamente estás dictando que un Engine debe ser inyectado a la Car en lugar de algún tipo de interfaz utilizada para determinar tu velocidad (IVelocity en el siguiente ejemplo)

Con una interfaz, el Car es capaz de 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.


3
2017-09-29 20:19



Aquí es donde creo que debes usar contenedores de inyección de dependencia que le permiten encapsular la creación de su automóvil, sin permitir que las personas que llaman lo necesiten para 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 preguntamos   para un objeto de correo [Este sería su coche en su ejemplo], y no necesitamos   saber algo sobre cómo crear   ya no más; todo el conocimiento sobre   cómo crear una instancia de   mailer [car] ahora está integrado en el   envase.

Es la esperanza de que te ayude


2
2017-09-29 16:13



Fábricas e interfaces.

Tienes un par de preguntas aquí.

  1. ¿Cómo puedo tener múltiples implementaciones de las mismas operaciones?
  2. ¿Cómo puedo ocultar los detalles de construcción de un objeto del consumidor de un objeto?

Entonces, lo que necesitas es esconder el código real detrás de un ICar interfaz, crea un EnginelessCar si alguna vez necesita uno, y use un ICarFactory interfaz y una CarFactory clase para ocultar los detalles de construcción del 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:

  1. 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.
  2. 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).


2
2017-09-29 16:36



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 inyección basada en el sistema, su código que usa la interfaz no le importa. Lo que se inyecta está en el xml de contexto de 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.


1
2017-09-29 15:59



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 podría inyectarse, 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 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 en el 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 algunos medio de establecer / inyectar estos valores en el automóvil: no hay forma de evitar esto, incluso si nunca antes había 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).


1
2017-09-30 01:22



Debe dividir su código en dos fases:

  1. Construcción del gráfico de objetos para una vida en particular a través de fábrica o solución DI
  2. 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. p.ej. 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á.


1
2017-10-04 22:52



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 tus 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!


0
2017-09-30 14:18