如何在不破坏封装的情况下使用依赖注入?

时间:2010-09-29 15:24:18

标签: unit-testing language-agnostic oop dependency-injection encapsulation

如何在不破坏封装的情况下执行依赖注入?

使用Dependency Injection example from Wikipedia

public Car {
    public float getSpeed();
}
  

注意:其他方法和属性(例如PushBrake(),PushGas(),   SetWheelPosition())省略   清晰度

效果很好;你不知道我的对象如何实现getSpeed - 它是“封装的”。

实际上,我的对象将getSpeed实现为:

public Car {
    private m_speed;
    public float getSpeed( return m_speed; );
}

一切都很好。有人构造了我的Car物体,捣碎踏板,喇叭,方向盘和汽车响应。

现在让我说我改变了我的汽车的内部实现细节:

public Car {
    private Engine m_engine;
    private float m_currentGearRatio;
    public float getSpeed( return m_engine.getRpm*m_currentGearRatio; );
}

一切都很好。 Car遵循正确的OO原则,隐藏 如何完成某些事情的细节。这使得呼叫者可以解决他的问题,而不是试图了解汽车的工作原理。它还让我可以自由地改变我的实现。

但依赖注入会迫使我将我的类暴露给我没有创建或初始化的Engine对象。更糟糕的是,我现在已经曝光了我的Car甚至引擎:

public Car {
   public constructor(Engine engine);
   public float getSpeed();
}

现在外面的单词意识到我使用的是Engine。我并不总是使用引擎,我可能希望将来不使用Engine,但我不能再改变我的内部实现:

public Car {
    private Gps m_gps;
    public float getSpeed( return m_gps.CurrentVelocity.Speed; )
}

不打破来电者:

public Car {
   public constructor(Gps gps);
   public float getSpeed();
}

但依赖注入会打开一大堆蠕虫:通过打开整个蠕虫病毒。依赖注入要求公开我的所有对象私有实现细节。我Car班的消费者现在必须理解并处理我班上所有以前隐藏的内部错综复杂的事情:

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();
}

我如何使用依赖注入的优点来帮助单元测试,同时又不破坏封装的优点以帮助实现?

另见


为了好玩,我可以将getSpeed示例减少到恰当的位置:

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);
   }
}

更新:也许某些答案可以按照问题的主导,并提供示例代码?

public Car {
    public float getSpeed();
}

另一个例子是当我的课依赖另一个对象时:

public Car {
    private float m_speed;
}

在这种情况下,float是一个用于表示浮点值的类。从我读到的,应该注入每个依赖类 - 如果我想模拟float类。这引发了必须注入每个私有成员的幽灵,因为一切都基本上是一个对象:

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

这些确实是我不希望客户看到的实施细节。

9 个答案:

答案 0 :(得分:13)

其他许多答案暗示了这一点,但我会更明确地说是的,依赖注入的天真实现可以打破封装。

避免这种情况的关键是调用代码不应该直接实例化依赖项(如果它不关心它们)。这可以通过多种方式完成。

最简单的是使用默认构造函数来执行使用默认值进行注入。只要调用代码只使用默认构造函数,就可以在不影响调用代码的情况下更改幕后的依赖关系。

如果您的依赖项本身具有依赖性等,这可能会开始失控。此时,Factory模式可以到位(或者您可以从一开始就使用它,以便调用代码已经在使用工厂)。如果您介绍工厂并且不想破坏代码的现有用户,则可以始终从默认构造函数调用工厂。

除此之外,还有使用控制反转。我没有足够多地使用IoC来谈论它,但是这里有很多问题以及在线文章可以解释它比我做得更好。

如果它应该真正封装到调用代码不能知道依赖关系的地方那么可以选择进行注入(带有依赖参数的构造函数或者setter){{1}如果语言支持它,或者将它们设为私有,并且如果您的语言支持,则让您的单元测试使用像Reflection这样的东西。如果你的语言既不支持,那么我想可能有一种可能是让调用代码的类实例化一个虚拟类,它只是封装了实际工作的类(我相信这是Facade模式,但我从来没有正确记住这些名称) ):

internal

答案 1 :(得分:7)

如果我正确理解您的问题,那么您正试图阻止任何需要实例化新Car对象的类必须手动注入所有这些依赖项。

我使用了几种模式来做到这一点。在使用构造函数链接的语言中,我已经指定了一个默认构造函数,它将具体类型注入到另一个依赖注入构造函数中。我认为这是一种非常标准的手动DI技术。

我使用的另一种允许更松散耦合的方法是创建一个工厂对象,该对象将使用适当的依赖关系配置DI'ed对象。然后我将这个工厂注入任何需要在运行时“新”某些汽车的对象;这允许您在测试期间注入完全伪造的Car实现。

并且始终存在二传手注射方法。该对象的属性具有合理的默认值,可以根据需要替换为test-double。不过,我更喜欢构造函数注入。


编辑以显示代码示例:

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

然后,消费者类只通过界面与它进行交互,完全隐藏任何构造函数。

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

答案 2 :(得分:3)

我认为你打破了Car构造函数的封装。具体而言,您要求Engine必须注入Car,而不是用于确定速度的某种类型的界面(IVelocity,如下例所示。)

通过界面,Car能够获得当前速度,而不依赖于确定速度的速度。例如:

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; ); 
}
然后,引擎或GPS可以根据其工作类型拥有多个接口。接口是DI的关键,没有DI就会打破封装。

答案 3 :(得分:2)

我认为您必须使用依赖注入容器来封装汽车的创建,而不会让您的客户调用者知道如何创建它。以下是symfony如何解决这个问题(即使它不是同一种语言,原则保持不变):

http://components.symfony-project.org/dependency-injection/documentation

有一个关于依赖注入容器的部分。

简而言之,直接从文档页面引用所有内容:

  

使用容器时,我们只是问   对于邮件对象[这将是您示例中的汽车],我们不需要   了解有关如何创建的任何信息   它了所有的知识   如何创建一个实例   邮件[car]现已嵌入到   容器

希望它可以帮助你

答案 4 :(得分:2)

工厂和接口。

你在这里有几个问题。

  1. 如何进行相同操作的多个实现?
  2. 如何从对象的使用者隐藏对象的构造细节?
  3. 因此,您需要隐藏ICar接口背后的真实代码,如果需要,可以创建单独的EnginelessCar,并使用ICarFactory接口和{{ 1}}类隐藏汽车消费者的施工细节。

    这可能最终看起来很像依赖注入框架,但您不必使用它。

    根据我在另一个问题中的答案,这是否打破了封装,完全取决于你如何定义封装。我看到有两种常见的封装定义:

    1. 逻辑实体上的所有操作都作为类成员公开,并且该类的使用者不需要使用任何其他操作。
    2. 一个类只有一个责任,管理该责任的代码包含在类中。也就是说,在对类进行编码时,您可以有效地将其与环境隔离,并缩小您正在使用的代码的范围。
    3. (像第一个定义的代码可以存在于与第二个条件一致的代码库中 - 它只是局限于外观,而这些外观往往具有最小或没有逻辑)。

答案 5 :(得分:1)

我很长时间没有使用过Delphi。 DI在Spring中的工作方式,你的setter和构造函数不是接口的一部分。因此,您可以拥有多个接口实现,一个可能使用基于构造函数的注入,另一个可能使用基于setter的注入,您使用该接口的代码无关紧要。注入的是在application-context xml中,这是你的依赖项暴露的唯一地方。

编辑: 如果您使用框架或不使用框架,那么您将拥有一个将对象连接在一起的工厂。因此,您的对象会在构造函数或setter中公开这些细节,但您的应用程序代码(工厂外部,不计算测试)从不使用它们。无论哪种方式,您选择从工厂获取对象图,而不是动态实例化,并且您选择不执行在注入的代码中使用setter等操作。从一些人的代码中看到的“钉一切”的理念,这是一种思维转变。

答案 6 :(得分:1)

我不认为汽车是依赖注入现实世界有用性的一个特别好的例子。

我认为在你上一个代码示例的情况下,Car类的目的并不清楚。是一个持有数据/状态的类吗?这是一项计算速度等服务的服务吗?或者是混合,允许您构建其状态,然后在其上调用服务以根据该状态进行计算?

我看到它的方式,Car类本身可能是一个有状态的对象,其目的是保持其组成的细节,并且计算速度的服务(如果需要可以注入)将是一个单独的class(使用类似“getSpeed(ICar car)”的方法)。许多使用DI的开发人员倾向于分离有状态和服务对象 - 尽管有些对象同时具有状态和服务,但大多数往往是分开的。此外,绝大多数DI使用都倾向于服务方面。

接下来的问题是:如何组成汽车类?每个特定汽车的意图是Car类的实例,还是从CarBase或ICar继承的每个品牌和型号都有一个单独的类?如果它是前者,则必须有一些方法将这些值设置/注入汽车 - 即使您从未听说过依赖倒置,也无法解决这个问题。如果它是后者,那么这些值只是汽车的一部分,我认为没有理由想让它们可设置/可注射。它归结为Engine和Tires之类的东西是否特定于实现(硬依赖)或它们是否可组合(松耦合依赖)。

我理解汽车只是一个例子,但在现实世界中,你将会知道是否反对对类的依赖性违反了封装。如果确实如此,你应该问的问题是“为什么?”而不是“怎么样?” (当然,这就是你正在做的事情)。

答案 7 :(得分:1)

您应该将代码分为两个阶段:

  1. 通过工厂或DI解决方案构建特定寿命的对象图
  2. 运行这些对象(涉及输入和输出)
  3. 在汽车工厂,他们需要知道如何制造汽车。他们知道它有什么样的引擎,喇叭如何接线等。这是上面的第1阶段。汽车厂可以制造不同的汽车。

    当您驾驶汽车时,您可以驾驶任何符合您期望的汽车界面的物品。例如踏板,方向盘,喇叭。当您开车时,您在按下制动器时不知道内部细节。但是,您可以看到结果(速度变化)。

    保持封装,因为没有人驾驶汽车需要知道它是如何构建的。因此,您可以使用与许多不同汽车相同的驱动程序。当驱动器需要汽车时,应该给它一个。如果他们在意识到自己需要的时候自己建立,那么封装就会被打破。

答案 8 :(得分:0)

现在,对于一些完全不同的东西......

你想要依赖注入的优点而不破坏封装。依赖注入框架将为您做到这一点,但通过虚拟构造函数的创造性使用,元类注册以及在项目中选择性地包含单元,还可以使用“穷人的依赖注入”。

但确实有一个严重的限制:每个项目中只能有一个特定的Engine类。没有选择引擎的选择,虽然想到它,你可能会搞乱元类变量的值来实现这一点。但我领先于自己。

另一个限制是单行继承:只是一个主干,没有分支。至少对于单个项目中包含的单位而言。

您似乎正在使用Delphi,因此下面的方法将起作用,因为它是我们自D5以来在需要TBaseX类的单个实例的项目中使用的东西,但是不同的项目需要该基类的不同后代而我们希望能够通过简单地移出一个单元并添加另一个单元来交换类。但解决方案并不仅限于Delphi。它适用于支持虚拟构造函数和元类的任何语言。

那你需要什么?

那么,根据每个项目所包含的单位,您希望能够交换的每个类都需要在某个位置存储可以存储类类型的变量来实例化:

var
  _EngineClass: TClass;

实现Engine的每个类都应该使用一种方法在_EngineClass变量中注册自己,该方法可以防止祖先取代后代(因此可以避免依赖于单元初始化顺序):

procedure RegisterMetaClass(var aMetaClassVar: TClass; const aMetaClassToRegister: TClass);
begin
  if Assigned(aMetaClassVar) and aMetaClassVar.InheritsFrom(aMetaClassToRegister) then
    Exit;

  aMetaClassVar := aMetaClassToRegister;
end;

可以在公共基类中完成类的注册:

  TBaseEngine
  protected
    class procedure RegisterClass; 

class procedure TBaseEngine.RegisterClass;
begin
  RegisterMetaClass(_EngineClass, Self);
end;

每个后代通过在其单元的初始化部分中调用注册方法来注册自己:

type
  TConcreteEngine = class(TBaseEngine)
  ...
  end;

initialization
  TConcreteEngine.RegisterClass;

现在您只需要实例化“后代最多”注册类而不是硬编码的特定类。

  TBaseEngine
  public
    class function CreateRegisteredClass: TBaseEngine; 

class function TBaseEngine.CreateRegisteredClass: TBaseEngine;
begin
  Result := _EngineClass.Create;
end;

当然,您现在应该始终使用此类函数来实例化引擎而不是正常的构造函数。

如果这样做,您的代码现在将始终实例化项目中存在的“最后代”引擎类。您可以通过包含但不包括特定单位在类之间切换。例如,通过使mock类成为实际类的祖先而不包括测试项目中的实际类,可以确保测试项目使用模拟类。或者让mock类成为实际类的后代,而不是在普通代码中包含mock;或者 - 甚至更简单 - 在项目中包含 模拟实际的类。


在此实现示例中,模拟和实际类具有无参数构造函数。不一定是这种情况,但是由于var参数的原因,你需要使用一个特定的元类(而不是TClass)和一些对RegisterMetaClass过程的调用。

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;

玩得开心!