如何在不破坏封装的情况下使用依赖注入?
如何在不破坏封装的情况下执行依赖注入?
使用来自维基百科的依赖注入示例:
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
对象。更糟糕的是,我现在暴露了我的汽车
甚至有引擎:
public Car {
public constructor(Engine engine);
public float getSpeed();
}
现在外界知道我使用引擎
。我并不总是使用引擎,我可能不想在将来使用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,
...
}
这些确实是我不希望客户必须查看的实现细节。
How can i perform dependency injection without breaking encapsulation?
Using a Dependency Injection example from Wikipedia:
public Car {
public float getSpeed();
}
Note: Other methods and properties (e.g. PushBrake(), PushGas(),
SetWheelPosition() ) omitted for
clarity
This works well; you don't know how my object implements getSpeed
- it is "encapsulated".
In reality my object implements getSpeed
as:
public Car {
private m_speed;
public float getSpeed( return m_speed; );
}
And all is well. Someone constructs my Car
object, mashes pedals, the horn, the steering wheel, and the car responds.
Now lets say i change an internal implementation detail of my car:
public Car {
private Engine m_engine;
private float m_currentGearRatio;
public float getSpeed( return m_engine.getRpm*m_currentGearRatio; );
}
All is well. The Car
is following proper OO-principles, hiding details of how something is done. This frees the caller to solve his problems, rather than trying to understand how a car works. It also gives me the freedom to change my implementation as i see fit.
But dependency injection would force me to expose my class to an Engine
object that i didn't create or initialize. Even worse is that I've now exposed that my Car
even has an engine:
public Car {
public constructor(Engine engine);
public float getSpeed();
}
And now the outside word is aware that i use an Engine
. I didn't always use an engine, i may want to not use an Engine
in the future, but i can no longer change my internal implementation:
public Car {
private Gps m_gps;
public float getSpeed( return m_gps.CurrentVelocity.Speed; )
}
without breaking the caller:
public Car {
public constructor(Gps gps);
public float getSpeed();
}
But dependency injection opens a whole can of worms: by opening the whole can of worms. Dependency Injection requires that all my objects private implementation details be exposed. The consumer of my Car
class now has to understand, and deal with, all of the previously hidden internal intricacies of my class:
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();
}
How can i use the virtues of Dependency Injection to help unit testing, while not breaking the virtues of encapsulation to help usability?
See also
- Must Dependency Injection come at the expense of Encapsulation? (Must, rather than how)
For the sake of fun, i can trim down the getSpeed
example to just what is needed:
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);
}
}
Update: Perhaps some answer can follow the question's lead, and give sample code?
public Car {
public float getSpeed();
}
Another example is when my class depends on another object:
public Car {
private float m_speed;
}
In this case float
is a class that is used to represent a floating-point value. From what i read, every dependant class should be injected - in case i want to mock the float
class. This raises the spectre of having to inject every private member, since everything is fundamentally an object:
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,
...
}
These really are implementation details that i don't want the customer to have to look at.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(9)
许多其他答案都暗示了这一点,但我要更明确地说,是的,依赖注入的幼稚实现可以破坏封装。
避免这种情况的关键是调用代码不应直接实例化依赖项(如果它不关心它们)。这可以通过多种方式完成。
最简单的是有一个默认构造函数来使用默认值进行注入。只要调用代码仅使用默认构造函数,您就可以在幕后更改依赖项,而不会影响调用代码。
如果您的依赖项本身具有依赖项等,那么这可能会开始失控。那时工厂模式就可以到位(或者您可以从一开始就使用它,以便调用代码已经在使用工厂)。如果您引入工厂并且不想破坏代码的现有用户,则始终可以从默认构造函数调用工厂。
除此之外,还有控制反转的使用。我对 IoC 的使用还不够多,无法对其进行太多讨论,但这里有很多关于它的问题,而且网上的文章比我能更好地解释它。
如果它应该真正封装到调用代码无法知道依赖项的地方,那么可以选择进行注入(带有依赖项参数的构造函数或设置器)
内部
如果语言支持它,或者将它们设为私有,并让您的单元测试使用诸如反射之类的东西(如果您的语言支持它)。如果你的语言都不支持,那么我想可能有一个类,调用代码正在实例化一个虚拟类,该类只是封装了真正工作的类(我相信这是外观模式,但我从来没有正确记住这些名称) ):Many of the other answers hint at it, but I'm going to more explicitly say that yes, naive implementations of dependency injection can break encapsulation.
The key to avoiding this is that calling code should not directly instantiate the dependencies (if it doesn't care about them). This can be done in a number of ways.
The simplest is simply have a default constructor that does the injecting with default values. As long as calling code is only using the default constructor you can change the dependencies behind the scenes without affecting calling code.
This can start to get out of hand if your dependencies themselves have dependencies and so forth. At that point the Factory pattern could come into place (or you can use it from the get-go so that calling code is already using the factory). If you introduce the factory and don't want to break existing users of your code, you could always just call into the factory from your default constructor.
Beyond that there's using Inversion of Control. I haven't used IoC enough to speak too much about it, but there's plenty of questions here on it as well as articles online that explain it much better than I could.
If it should be truly encapsulated to where calling code cannot know about the dependencies then there's the option of either making the injecting (either the constructor with the dependency parameters or the setters)
internal
if the language supports it, or making them private and have your unit tests use something like Reflection if your language supports it. If you language supports neither then I suppose a possibility might be to have the class that calling code is instantiating a dummy class that just encapsulates the class the does the real work (I believe this is the Facade pattern, but I never remember the names correctly):如果我正确理解您的担忧,那么您正在尝试阻止任何需要实例化新 Car 对象的类必须手动注入所有这些依赖项。
我使用了几种模式来做到这一点。在具有构造函数链接的语言中,我指定了一个默认构造函数,它将具体类型注入另一个依赖注入的构造函数中。我认为这是一种非常标准的手动 DI 技术。
我使用的另一种方法(允许更松散的耦合)是创建一个工厂对象,该对象将使用适当的依赖项配置 DI 对象。然后我将这个工厂注入到任何需要在运行时“新”一些汽车的对象中;这也允许您在测试期间注入完全伪造的 Car 实现。
并且总是有 setter 注入方法。该对象的属性将具有合理的默认值,可以根据需要用测试替身替换。不过,我确实更喜欢构造函数注入。
编辑以显示代码示例:
然后消费者类只需通过接口与其交互,完全隐藏任何构造函数。
If I understand your concerns correctly, you're trying to prevent any class that needs to instantiate a new Car object from having to inject all those dependencies manually.
I've used a couple patterns to do this. In languages with constructor chaining, I've specified a default constructor that injects the concrete types into another, dependency-injected constructor. I think this is a pretty standard manual DI technique.
Another approach I've used, which allows some looser coupling, is to create a factory object that will configure the DI'ed object with the appropriate dependencies. Then I inject this factory into any object that needs to "new" up some Cars at runtime; this allows you to inject completely faked Car implementations during your tests, too.
And there's always the setter-injection approach. The object would have reasonable defaults for its properties, which could be replaced with test-doubles as needed. I do prefer constructor-injection, though.
Edit to show a code example:
And then consumer classes just interact with it through the interface, completely hiding any constructors.
我认为您正在破坏
Car
构造函数的封装。具体来说,您规定必须将Engine
注入到Car
中,而不是使用某种类型的接口来确定您的速度(IVelocity
中的如下例。)通过接口,
Car
能够获取其当前速度,而与决定该速度的因素无关。例如:引擎或 GPS 可以根据其工作类型拥有多个接口。接口是 DI 的关键,没有它 DI 就会破坏封装。
I think you're breaking encapsulation with your
Car
constructor. Specifically you're dictating that anEngine
must be injected to theCar
instead of some type of interface used to determine your speed (IVelocity
in the below example.)With an interface, the
Car
is able to get it's current speed independent of what's determining that speed. For example:An Engine or GPS can then have multiple interfaces based upon the type of work that it does. The interface is key to DI, without it DI does break encapsulation.
这是我认为您必须使用依赖注入容器的地方,它可以让您封装汽车的创建,而不让您的客户端调用者需要知道如何创建它。下面是 symfony 解决这个问题的方法(尽管不是同一种语言,但原理是一样的):
http://components.symfony-project.org/dependency-injection/documentation
有一个关于依赖注入容器的部分。
简而言之,总结一下直接从文档页面引用的内容:
希望对您有帮助
This is where I think you must use dependency injection containers that let you encapsulate the creation of your car, without letting your client callers need to know how to create it whatsoever. Here's how symfony solved this problem (even though it is not the same language, principles remain the same):
http://components.symfony-project.org/dependency-injection/documentation
there is a section on dependency injection containers.
To make it short and summarize it all quoted from the documentation page directly:
It the hope that it helps you
工厂和接口。
您有几个问题。
因此,您需要的是将真正的代码隐藏在
ICar
接口后面,如果需要的话创建一个单独的EnginelessCar
,并使用ICarFactory
> 接口和一个CarFactory
类,用于向汽车消费者隐藏构造细节。这可能最终看起来很像依赖注入框架,但您不必使用它。
根据我在另一个问题中的回答,这是否会破坏封装完全取决于您如何定义封装。我见过两种常见的封装定义:
(像第一个定义这样的代码可以存在于适用于第二个条件的代码库中 - 它往往仅限于外观,并且这些外观往往具有最少的逻辑或没有逻辑)。
Factories and interfaces.
You've got a couple of questions here.
So, what you need is to hide the real code behind an
ICar
interface, create a separateEnginelessCar
if you ever need one, and use anICarFactory
interface and aCarFactory
class to hide the construction details from the consumer of the car.This will likely end up looking a lot like a dependency injection framework, but you do not have to use one.
As per my answer in the other question, whether or not this breaks encapsulation depends entirely on how you define encapsulation. There are two common definitions of encapsulation that I've seen:
(Code like the first definition can exist in a codebase that works with the second condition - it just tends to be limited to facades, and those facades tend to have minimal or no logic).
我已经很久没有使用Delphi了。 DI 在 Spring 中的工作方式,您的 setter 和构造函数不是接口的一部分。因此,您可以拥有一个接口的多个实现,一个可能使用基于构造函数的注入,另一个可能使用基于设置器的注入,使用该接口的代码并不关心。注入的内容位于应用程序上下文 xml 中,这是唯一暴露依赖项的地方。
编辑:
无论您是否使用框架,您都在做同样的事情,您都有一个将对象连接在一起的工厂。因此,您的对象在构造函数或设置器中公开这些详细信息,但您的应用程序代码(在工厂之外,不包括测试)从不使用它们。无论哪种方式,您都选择从工厂获取对象图,而不是即时实例化内容,并且您选择不执行诸如在要注入的代码中使用设置器之类的操作。这是我从某些人的代码中看到的“钉住一切”哲学的思维转变。
I haven't used Delphi in a long time. The way DI works in Spring, your setters and constructor aren't part of the interface. So you can have multiple implementations of an interface, one might use constructor-based injection and another might use setter-based injection, your code that uses the interface doesn't care. What's injected is in the application-context xml, and that is the only place that your dependencies are exposed.
EDIT:
If you use a framework or not you're doing the same thing, you have a factory that wires together your objects. So your objects expose these details in the constructor or in setters, but your application code (outside of the factory, and not counting tests) never uses them. Either way you choose to get your object graph from the factory rather than instantiate stuff on the fly, and you choose to not do things like use setters in the code that are there to be injected into. It is a mind-shift from the "nail-everything-down" philosophy I see from some people's code.
我不认为汽车是依赖注入在现实世界中的实用性的一个特别好的例子。
我认为在您的最后一个代码示例中,Car 类的目的尚不清楚。是一个保存数据/状态的类吗?它是计算速度之类的服务吗?或者它是一个混合体,允许您构建其状态,然后调用其上的服务以基于该状态进行计算?
在我看来,Car 类本身可能是一个有状态对象,其目的是保存其组成的详细信息,并且计算速度的服务(如果需要,可以注入)将是一个单独的类(具有类似“
getSpeed(ICar car)
”的方法)。许多使用 DI 的开发人员倾向于将有状态对象和服务对象分开——尽管在某些情况下一个对象同时具有状态和服务,但大多数倾向于分开。此外,绝大多数 DI 使用往往是在服务端。下一个问题是:汽车类别应该如何组成?意图是每辆特定汽车只是 Car 类的一个实例,还是为从 CarBase 或 ICar 继承的每个品牌和型号都有一个单独的类?如果是前者,那么一定有一些方法可以将这些值设置/注入到汽车中——即使您从未听说过依赖倒置,也没有办法解决这个问题。如果是后者,那么这些值只是汽车的一部分,我认为没有理由让它们可设置/可注入。这取决于诸如引擎和轮胎之类的东西是否特定于实现(硬依赖项)或者它们是否是可组合的(松散耦合依赖项)。
我知道汽车只是一个例子,但在现实世界中,您将成为知道反转对类的依赖关系是否违反封装的人。如果确实如此,您应该问的问题是“为什么?”而不是“如何?” (当然,这就是你正在做的事情)。
I don't think a car is a particularly good example of the real world usefulness of dependency injection.
I think in the case of your last code example, the purpose of the Car class is not clear. Is is a class that holds data/state? Is it a service to calculate things like speed? Or is it a mix, allowing you to construct its state and then call services on it to make calculations based on that state?
The way I see it, the Car class itself would likely be a stateful object, whose purpose is to hold the details of its composition, and the service to calculate speed (which could be injected, if desired) would be a separate class (with a method like "
getSpeed(ICar car)
"). Many developers who use DI tend to separate stateful and service objects--although there are cases where an object will have both state and service, the majority tend to be separated. In addition, the vast majority of DI usage tends to be on the service side.The next question would be: how should the car class be composed? Is the intent that every specific car is just an instance of a Car class, or is there a separate class for each make and model that inherit from CarBase or ICar? If it's the former, then there must be some means of setting/injecting these values into the car--there is no way around this, even if you'd never heard of dependency inversion. If it's the latter, then these values are simply part of the car, and I would see no reason to ever want to make them settable/injectable. It comes down to whether things like Engine and Tires are specific to the implementation (hard dependencies) or if they are composable (loosely coupled dependencies).
I understand the car is just an example, but in the real world you are going to be the one who knows whether inverting dependencies on your classes violates encapsulation. If it does, the question you should be asking is "why?" and not "how?" (which is what you are doing, of course).
您应该将代码分为两个阶段:
在汽车工厂,他们需要知道如何制造汽车。他们知道它有什么样的发动机,喇叭是如何接线的等等。这是上面的第一阶段。汽车厂可以制造不同的汽车。
当你驾驶汽车时,你可以驾驶任何符合你期望的汽车接口的东西。例如踏板、方向盘、喇叭。当您开车时,踩刹车时您不知道内部细节。但是,您可以看到结果(速度变化)。
封装得以保留,因为驾驶汽车的人不需要知道它是如何制造的。因此,您可以使用同一个驱动程序来驾驶许多不同的汽车。当司机需要一辆车时,应该给他们一辆。如果他们在意识到需要时构建自己的封装,那么封装就会被破坏。
You should break your code into two phases:
At the car factory, they need to know how to build a car. They know what sort of engine it has, how the horn is wired in etc. This is phase 1 above. The car factory can build different cars.
When you are driving the car, you can drive anything that meets the car interface you expect. e.g. pedals, steering wheel, horn. When you're driving you don't know the internal details when you press the brake. You can, however, see the result (change in speed).
Encapsulation is maintained as no one driving a car needs to know how it was built. Therefore, you can use the same driver with many different cars. When the drive needs a car, they should be given one. If they build their own when they realise they need one, then encapsulation will be broken.
现在,对于完全不同的东西......
您想要依赖注入的优点而不破坏封装。依赖注入框架可以为您做到这一点,但是通过创造性地使用虚拟构造函数、元类注册和在项目中选择性包含单元,您还可以使用“穷人的依赖注入”。
但它确实有一个严重的限制:每个项目中只能有一个特定的引擎类。没有选择引擎的选择,但仔细想想,您可能会弄乱元类变量的值来实现这一点。但我有点超前了。
另一个限制是单行继承:只有主干,没有分支。至少对于单个项目中包含的单元而言是这样。
您似乎正在使用 Delphi,因此下面的方法将起作用,因为它是我们自 D5 以来一直在需要类 TBaseX 的单个实例的项目中使用的方法,但不同的项目需要该基类的不同后代,我们希望能够通过简单地删除一个单元并添加另一个单元来交换课程。不过,该解决方案并不局限于 Delphi。它将与任何支持虚拟构造函数和元类的语言一起使用。
那么你需要什么?
好吧,您希望能够根据每个项目包含的单元进行交换的每个类都需要有一个变量,您可以在其中存储要实例化的类类型:
每个实现 Engine 的类都应该使用以下方法在 _EngineClass 变量中注册自己:一种防止祖先取代后代的方法(这样您就可以避免对单元初始化顺序的依赖):
可以在公共基类中完成类的注册:
每个后代通过在其单元的初始化中调用注册方法来注册自己部分:
现在您所需要的只是实例化“最后代”注册类,而不是硬编码的特定类。
当然,您现在应该始终使用此类函数来实例化引擎,而不是普通的构造函数。
如果您这样做,您的代码现在将始终实例化项目中存在的“最后代”引擎类。您可以通过包含或不包含特定单元来在课程之间进行切换。例如,您可以通过使模拟类成为实际类的祖先并且不在测试项目中包含实际类来确保您的测试项目使用模拟类;或者使模拟类成为实际类的后代,并且不在正常代码中包含模拟;或者 - 甚至更简单 - 通过在项目中包含或者模拟或实际类。
在此实现示例中,模拟类和实际类具有无参数构造函数。情况不一定如此,但由于 var 参数,您将需要使用特定的元类(而不是 TClass)并在对 RegisterMetaClass 过程的调用中进行一些转换。
玩得开心!
Now, for something completely different...
You want the virtues of dependency injection without breaking encapsulation. A dependency injection framework will do that for you, but there is also a "poor man's dependency injection" available to you through some creative use of virtual constructors, meta class registration and selective inclusion of units in your projects.
It does have a serious limitation though: you can only have a single specific Engine class in each project. There is no picking an choosing engine's, though come to think of it, you could probably mess with the value of the meta class variable to achieve just that. But I am getting ahead of myself.
Another limitation is a single line of inheritance: just a trunk, no branches. At least with regard to the units included in a single project.
You seem to be using Delphi and therefore the method below will work as it is something that we have been using since D5 in projects that need a single instance of class TBaseX, but different projects need different descendants of that base class and we want to be able to swap classes by simply chucking out one unit and adding another. The solution isn't restricted to Delphi though. It will work with any language that supports virtual constructors and meta classes.
So what do you need?
Well, every class that you want to be able to swap depending on units included per project, needs to have a variable somewhere in which you can store the class type to instantiate:
Every class that implements an Engine should register itself in the _EngineClass variable using a method that prevents ancestors from taking the place of a descendant (so you can avoid dependence on unit initialisation order):
Registration of the classes can be done in a common base class:
Each descendant registers itself by calling the registration method in its unit's initialization section:
Now all you need is something to instantiate the "descendent most" registered class instead of a hard coded specific class.
Of course you should now always use this class function to instantiate engines and not the normal constructor.
If you do that, your code will now always instantiate the "most descendant" engine class present in your project. And you can switch between classes by including and not including the specific units. For example you can ensure your test projects use the mock classes by making the mock class an ancestor of the actual class and not including the actual class in the test project; or by making the mock class a descendant of the actual class and not including the mock in your normal code; or - even simpler - by including either the mock or the actual class in your projects.
Mock and actual classes have a parameter-less constructor in this implementation example. Doesn't need to be the case, but you will need to use a specific meta class (instead of TClass) and some casting in the call to the RegisterMetaClass procedure because of the var parameter.
Have fun!