如何在不破坏封装的情况下使用依赖注入?
我如何执行依赖注入而不破坏封装?
使用维基百科的依赖注入示例:
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
遵循适当的面向对象原则,隐藏了如何完成某些事情的细节。 这样可以让来电者解决他的问题,而不是试图了解汽车是如何工作的。 它也使我可以自由地改变我的实施,因为我认为合适。
但是依赖注入会迫使我将我的类暴露给我没有创建或初始化的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,
...
}
这些确实是我不希望客户必须查看的实施细节。
许多其他答案暗示它,但我会更明确地说,是的,依赖注入的天真实现可以破坏封装。
避免这种情况的关键是调用代码不应该直接实例化依赖关系(如果它不关心它们)。 这可以通过多种方式完成。
最简单的就是有一个默认的构造函数,用默认值进行注入。 只要调用代码只使用默认构造函数,您就可以在不影响调用代码的情况下更改后台依赖项。
如果您的依赖关系本身具有依赖关系等,则可能会失控。 此时工厂模式可以到位(或者您可以从一开始就使用它,以便调用代码已经在使用工厂)。 如果你引入了工厂并且不想破坏你的代码的现有用户,那么你总是可以从你的默认构造函数调用工厂。
除此之外,还有使用控制反转。 我还没有使用IoC来说得太多,但是这里有很多问题以及在线文章,它们比我更好地解释它。
如果它应该真正封装到调用代码无法知道依赖关系的位置,那么可以选择将注入(带有依赖参数的构造函数或setter)作为internal
语言(如果语言支持它),或者使它们变为私有的如果你的语言支持它,单元测试使用类似Reflection的东西。 如果你的语言不支持,那么我认为有可能让调用代码的类实例化一个虚拟类,它只是封装了类的实际工作(我相信这是Facade模式,但我永远不会记住名字) ):
public Car {
private RealCar _car;
public constructor(){ _car = new RealCar(new Engine) };
public float getSpeed() { return _car.getSpeed(); }
}
如果我正确地理解了你的担忧,你试图阻止任何需要实例化一个新Car对象的类来手动注入所有这些依赖关系。
我使用了几种模式来做到这一点。 在构造函数链接的语言中,我已经指定了一个默认的构造函数,将具体类型注入到另一个依赖注入的构造函数中。 我认为这是一个非常标准的手动DI技术。
我使用的另一种方法,允许一些松散的耦合,是创建一个工厂对象,它将配置DI'ed对象具有适当的依赖关系。 然后,我将这个工厂注入任何需要在运行时“新”启动一些Cars的对象; 这使您可以在测试过程中注入完全伪造的汽车实施。
总是有setter-injection方法。 该对象对其属性具有合理的默认值,根据需要可以用测试双打来替换。 不过,我更喜欢构造器注入。
编辑以显示代码示例:
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...
}
}
我认为你正在用你的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不会破坏封装。
链接地址: http://www.djcxy.com/p/82235.html上一篇: How to use Dependency Injection without breaking encapsulation?
下一篇: Events in an Inversion of Control (Dependency Inversion) system go which way?