成为“新”或不成为...

再一次问好。期待着Android开发基础高级课程的开始,我们已经为您准备了另一篇有趣的翻译。




依赖注入要求我们新的运算符和应用程序逻辑分开。这种分离鼓励您在代码中使用负责链接应用程序的工厂。但是,与其编写工厂,不如使用自动依赖项注入(例如GUICE)来接管绑定。但是,依赖注入真的能使我们摆脱所有新的运营商吗?
让我们看两个极端。假设您有一个MusicPlayer类,该类应获取AudioDevice。在这里,我们要使用依赖注入,并在MusicPlayer构造函数中请求AudioDevice。这将使我们能够添加一个易于测试的AudioDevice,可以用来声明MusicPlayer发出了正确的声音。如果我们使用new运算符创建BuiltInSpeakerAudioDevice的实例,那么我们将面临一些测试困难。因此,让我们将诸如AudioDevice或MusicPlayer之类的对象称为“ Injectable”。 Injectable是您将在构造函数中请求的对象,并期望用于实现依赖关系的框架将为您提供这些对象。

现在到另一个极端。假设您有一个int int原语,但是您想将其自动打包到Integer中,最简单的方法是调用新的Integer(5),到此为止。但是,如果依赖项注入是新的“新”特性,那么为什么我们将其称为内联新特性呢?会伤害我们的测试吗?事实证明,注入依赖项的框架无法为您提供所需的Integer,因为它们不了解它是哪种Integer。这只是一个玩具示例,所以让我们看一些更复杂的东西。

假设用户在登录字段中输入了电子邮件地址,并且您需要调用新的电子邮件地址(«a@b.com»)我们可以这样保留它吗,还是应该在构造函数中请求Email?同样,依赖项注入框架无法为您提供电子邮件,因为您必须首先获取电子邮件所在的字符串。并且有很多String可供选择。如您所见,依赖注入框架无法提供许多对象。我们称它们为“新的”,因为您将被迫手动为其命名

首先,让我们设置一些基本规则。一个Injectable类可以查询其构造函数中的其他Injectable。 (有时我称Injectable作为服务对象,但这个术语很重载。)Injectable倾向于具有接口,因为我们有可能必须用便于测试的实现来替换它们。但是,Injectable永远无法在其构造函数中查询non-Injectable(Newable)。这是因为依赖项注入框架不知道如何创建Newable。以下是一些我的依赖关系注入框架期望的类示例:CreditCardProcessor,MusicPlayer,MailSender,OfflineQueue。同样,Newable可以由其构造函数中的其他Newable请求,但不能被Injectable请求(有时我将Newable称为值对象,但同样,此术语超载)。可更新的一些示例:电子邮件,MailMessage,用户,信用卡,歌曲。如果遵循这些区别,您的代码将易于测试和使用。如果您违反这些规则,您的代码将很难测试。

让我们看一下MusicPlayer和Song的示例

class Song {
  Song(String name, byte[] content);
}
class MusicPlayer {
  @Injectable
  MusicPlayer(AudioDevice device);
  play(Song song);
}

请注意,Song仅查询可更新的对象。这使得在测试中实例化乐曲非常容易。就像其AudioDevice参数一样,MusicPlayer是完全可注入的,因此可以从依赖项注入的框架中获取它。

现在,让我们看看MusicPlayer违反规则并在其构造函数中请求Newable会发生什么。

class Song {
  String name;
  byte[] content;
  Song(String name, byte[] content);
}
class MusicPlayer {
  AudioDevice device;
  Song song;
  @Injectable
  MusicPlayer(AudioDevice device, Song song);
  play();
}

在这里,Song仍然是可更新的,并且可以在测试或代码中轻松创建。 MusicPlayer已经是一个问题。如果您从框架要求MusicPlayer进行依赖项注入,则它将崩溃,因为框架将不知道它是关于哪首Song的。大多数依赖依赖注入框架的人很少会犯此错误,因为很容易注意到:您的代码将无法工作。

现在让我们看看如果Song违反规则并在其构造函数中请求Injectable会发生什么。

class MusicPlayer {
  AudioDevice device;
  @Injectable
  MusicPlayer(AudioDevice device);
}
class Song {
  String name;
  byte[] content;
  MusicPlayer palyer;
  Song(String name, byte[] content, MusicPlayer player);
  play();
}
class SongReader {
  MusicPlayer player
  @Injectable
  SongReader(MusicPlayer player) {
    this.player = player;
  }
  Song read(File file) {
    return new Song(file.getName(),
                    readBytes(file),
                    player);
  }
}

乍一看,一切都很好。但是请考虑如何创建歌曲。大概,歌曲存储在磁盘上,因此我们需要SongReader。 SongReader将必须请求MusicPlayer,以便在为Song调用new时,它可以满足Song对MusicPlayer的依赖性。您注意到这里有什么问题吗? SongReader对MusicPlayer有什么恐惧?这违反了得墨meter耳法则。 SongReader应该不了解MusicPlayer。至少因为SongReader不使用MusicPlayer调用方法。他只了解MusicPlayer,因为Song违反了Newable / Injectable分隔。 SongReader支付Song中的错误。由于发生错误的地方和显示后果的地方不是同一件事,因此此错误非常微妙且难以诊断。这也意味着许多人可能会犯此错误。

在测试方面,这确实是一个痛苦。假设您有SongWriter,并希望确保它正确地将Song序列化到磁盘。您需要创建一个MockMusicPlayer,以便可以将其传递给Song,从而可以将其传递给SongWritter。为什么我们在这里甚至遇到MusicPlayer?让我们换一种方式来看。 Song是您可能想要序列化的东西,而最简单的方法是使用Java序列化。因此,我们不仅序列化Song,还序列化MusicPlayer和AudioDevice。 MusicPlayer和AudioDevice均不应序列化。如您所见,小的更改极大地促进了可测试性。

如您所见,如果我们将这两种对象分开,则使用代码会更容易。如果将它们混合使用,则代码将很难测试。可更新的是位于应用程序对象图末尾的对象。可更新商品可能依赖于其他可更新商品,例如,信用卡如何依赖于地址,而依赖地址可能取决于城市-这些都是应用程序图表的表格。因为它们是工作表,并且不与任何外部服务进行通信(外部服务是可注入的),所以它们不需要执行存根。没有什么比String本身更像String了。为什么我应该为用户创建一个存根,如果我只能给新用户打电话,为什么还要为以下任何一个存根:电子邮件,MailMessage,用户,信用卡,歌曲?只需调用new并结束即可。

现在,让我们注意一些非常细微的问题。Newable了解Injectable是很正常的。不正常的是,Newable引用了Injectable作为字段。换句话说,Song可以了解MusicPlayer。例如,将Injectable MusicPlayer通过堆栈传递到Newable Song是正常的。因为通过堆栈是独立于依赖注入框架的。像这个例子:

class Song {
  Song(String name, byte[] content);
  boolean isPlayable(MusicPlayer player);
}

当Song具有指向MusicPlayer的链接字段时,会发生此问题。链接字段是通过构造函数设置的,这将导致调用者违反Demeter法则,并给我们的测试带来困难。

了解有关课程的更多信息



All Articles