依赖注入和抽象
原文地址:http://blogs.unity3d.com/2014/05/07/dependency-injection-and-abstractions/
原文作者未做版权声明,视为共享知识产权进入公共领域,自动获得授权
对于任何软件产品来说,可测试性都是一个重要的功能,游戏开发也不例外。要拥有可测试性,所有组件都应该是独立的并且可以隔离测试。
当我们要独立测试某个东西意味着我们需要对它进行解耦。松耦合是我们需要的。在游戏开发中很容易出现隐藏的松散耦合,并且很难打破它。这篇文章将帮助你理解松散耦合以及Unity项目的依赖诸如,使用了github上的一个例子。
让我们以处理输入为例。
public class SpaceshipMotor : MonoBehaviour
{
void MoveHorizontally ()
{
var horizontal = Input.GetAxis ("Horizontal");
// ...
}
}
MoveHorizontally方法使用了静态Unity API(输入类)而没有告诉你。它认为这是它的私有事务,你不改控制或者影响它。它使得SpaceshipMotor与静态Unity API紧密耦合起来,如果不按下键盘上的键,你无法验证SpaceshipMotor的行为。
现在让我们来解决这个问题。
SpaceshipMotor只使用了水平轴,所以我们定义了一个需要何种输入的简单描述。
public interface IUserInputProxy
{
float GetAxis(string axisName);
}
现在你已经用我们的抽象来进行真实的调用。
public class SpaceshipMotor : MonoBehaviour
{
public IUserInputProxy UserInputProxy {get;set;}
void MoveHorizontally ()
{
var horizontal = UserInputProxy.GetAxis (“Horizontal”);
// …
}
}
现在是你控制整个态势了。除非你提供一个IUserInputProxy的实现,没法操作这个类。
这被成为依赖注入。当依赖(在我们的例子中是输入)被传递给依赖的物体(SpaceshipMotor类)并且变成状态的一部分(在我们的例子中是一个变量)。
传递依赖有几个选项:构造函数注入、属性注入、方法注入。
构造函数注入被认为是最受欢迎和最可靠的方法,因为依赖是在构建阶段传入的,未初始化的概率最低。
public class SpaceshipMotor : MonoBehaviour
{
private readonly IUserInputProxy userInputProxy;
public SpaceshipMotor(IUserInputProxy userInputProxy)
{
this.userInputProxy = userInputProxy;
}
}
但是Unity引擎负责调用MonoBehaviours的初始化,我们没有办法控制这个过程。
不过,属性和方法注入都是可用的。
最容易的手动诸如方法是使用脚本进行依赖注入。
在 “Growing Games Guided by Tests”我们使用接口来导出属性依赖。
public interface IRequireUserInput
{
IUserInputProxy InputProxy { get; set;}
}
建一个允许设置假输入参数的脚本,并且在测试开始的时候把它注入进去。
public class ArrangeFakeUserInput : MonoBehaviour
{
public GameObject Spaceship;
public FakeUserInput FakeInput;
void Start () {
var components = Spaceship.GetComponents
var dependents = components.Where(c=>c is IRequireUserInput)
.Cast
foreach(var dependent in dependents)
dependents.InputProxy = FakeInput;
}
}
这对于可测试性有什么贡献?
我们在“Growing GamesGuided by Test”里面有很多例子,通过帮助脚本注入一个输入,它会帮助我们测试行为。
另一方面,我们可以写一些依赖于抽闲的单元测试类。
[Test]
public void ChangesStateToIsFiringOnFire1ButtnPressed()
{
// Arrange
// Setting test double for user input
IUserInputProxy userInput = Substitute.For
// Telling GetButton method of test double to return true
// if state of “Fire1” was requested
userInput.GetButton(Arg.Is("Fire1")).Returns (true);
// Passing the dependency to Gun object on creation
Gun gun = new Gun(userInput);
// Act
gun.ProcessInput ();
// Assert
Assert.That(gun.IsFiring, Is.True);
}
现在你可以看到其实依赖注入并不神秘。就是一个替换具体依赖并且让它依赖一个外部抽象的过程。
如果你要在一个大规模的项目中使用依赖注入,你需要一个自动化工具,这将是我们下一篇文章的主题。