Être «nouveau» ou ne pas l'être ...

Rebonjour. En prévision du début des cours de base et avancés sur le développement Android, nous avons préparé pour vous une autre traduction intéressante.




L'injection de dépendances nous oblige à séparer les nouveaux opérateurs et la logique d'application . Cette séparation vous encourage à utiliser dans votre code des usines chargées de lier votre application . Cependant, plutôt que d'écrire des usines, nous préférerions utiliser l'injection automatique de dépendances , comme GUICE, pour prendre en charge la liaison. Mais l'injection de dépendances peut-elle vraiment nous sauver de tous les nouveaux opérateurs?
Regardons deux extrêmes. Supposons que vous ayez une classe MusicPlayer qui devrait obtenir AudioDevice. Ici, nous voulons utiliser l'injection de dépendances et demander un AudioDevice dans le constructeur MusicPlayer. Cela nous permettra d'ajouter un AudioDevice compatible avec les tests, que nous pouvons utiliser pour affirmer que le son correct sort de notre MusicPlayer. Si nous utilisions le nouvel opérateur pour créer une instance de BuiltInSpeakerAudioDevice, nous aurions des difficultés de test. Appelons donc des objets comme AudioDevice ou MusicPlayer «Injectable». Injectable sont les objets que vous demanderez dans les constructeurs et attendez-vous à ce que le cadre d'implémentation des dépendances vous les fournisse.

Maintenant à l'autre extrême. Supposons que vous ayez une primitive int int, mais que vous souhaitiez la compresser automatiquement dans Integer, le plus simple est d'appeler new Integer (5), et c'est la fin. Mais si l'injection de dépendance est le nouveau «nouveau», pourquoi appelons-nous nouveau en ligne? Est-ce que cela nuira à nos tests? Il s'avère que les cadres d’injection de dépendances ne peuvent pas vous fournir l’entier dont vous avez besoin, car ils ne comprennent pas de quel type d’entier il s’agit. Ceci est un peu un exemple de jouet, alors regardons quelque chose de plus complexe.

Supposons qu'un utilisateur ait entré une adresse e-mail dans le champ de connexion et que vous devez appeler un nouvel e-mail («a@b.com») Pouvons-nous le laisser comme ça, ou devrions-nous demander un e-mail à notre constructeur? Encore une fois, le cadre d'injection de dépendances ne peut pas vous fournir d'e-mail, car vous devez d'abord obtenir la chaîne dans laquelle se trouve l'e-mail. Et il y a beaucoup de chaînes parmi lesquelles choisir. Comme vous pouvez le voir, il existe de nombreux objets que l'infrastructure d'injection de dépendances ne peut jamais fournir. Appelons-les « Newable », comme vous serez obligé d'appeler nouveau pour eux manuellement.

Tout d'abord, définissons quelques règles de base. Une classe Injectable peut interroger un autre Injectable dans son constructeur. (Parfois, j'appelle Injectable as a Service Object, mais ce terme est surchargé.) Injectable a tendance à avoir des interfaces, car il est possible que nous devions les remplacer par une implémentation qui est pratique pour les tests. Cependant, Injectable ne peut jamais interroger de non-Injectable (Newable) dans son constructeur. En effet, le framework d'injection de dépendances ne sait pas comment créer un Newable. Voici quelques exemples de classes que j'attendrais de mon infrastructure d'injection de dépendances: CreditCardProcessor, MusicPlayer, MailSender, OfflineQueue. De même, Newable peut être demandé par d'autres Newable dans leur constructeur, mais pas Injectable (parfois j'appelle Newable en tant qu'objet valeur, mais encore une fois,ce terme est surchargé). Quelques exemples de Newable: Email, MailMessage, User, CreditCard, Song. Si vous suivez ces distinctions, votre code sera facile à tester et à utiliser. Si vous enfreignez ces règles, votre code sera difficile à tester.

Regardons l'exemple de MusicPlayer et Song

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

Notez que Song interroge uniquement les objets qui sont Newable. Cela rend très facile l'instanciation d'un morceau dans un test. MusicPlayer est complètement injectable, comme son argument AudioDevice, il peut donc être obtenu à partir du cadre pour l'injection de dépendances.

Voyons maintenant ce qui se passe si MusicPlayer enfreint la règle et demande Newable dans son constructeur.

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

Ici, Song est toujours Newable, et il est facile de créer dans votre test ou dans votre code. MusicPlayer est déjà un problème. Si vous demandez à MusicPlayer de votre framework une injection de dépendance, il se bloquera car le framework ne saura pas de quel morceau il s'agit. La plupart des personnes novices dans les frameworks d'injection de dépendances font rarement cette erreur, car il est facile de le remarquer: votre code ne fonctionnera pas.

Voyons maintenant ce qui se passe si Song enfreint la règle et demande Injectable dans son constructeur.

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

À première vue, tout va bien. Mais réfléchissez à la façon dont les morceaux seront créés. Vraisemblablement, les chansons sont stockées sur le disque, nous avons donc besoin de SongReader. SongReader devra demander à MusicPlayer de sorte que lors de l'appel de new pour Song, il puisse satisfaire les dépendances de Song sur MusicPlayer. Avez-vous remarqué quelque chose de mal ici? Quelle frayeur SongReader doit-il savoir sur MusicPlayer? Ceci est une violation de la loi de Demeter. SongReader ne devrait pas connaître MusicPlayer. Du moins parce que SongReader n'appelle pas de méthodes avec MusicPlayer. Il ne connaît MusicPlayer que parce que Song a violé la séparation Newable / Injectable. SongReader paie l'erreur dans Song. Étant donné que le lieu où l'erreur est commise et où les conséquences se manifestent n'est pas la même chose, cette erreur est très subtile et difficile à diagnostiquer. Cela signifie également que de nombreuses personnes sont susceptibles de commettre cette erreur.

En termes de tests, c'est une vraie douleur. Supposons que vous ayez SongWriter et que vous vouliez vous assurer qu'il sérialise correctement le morceau sur le disque. Vous devez créer un MockMusicPlayer afin de pouvoir le passer à Song afin de pouvoir le passer à SongWritter. Pourquoi rencontrons-nous même MusicPlayer ici? Regardons les choses dans l'autre sens. La chanson est ce que vous pourriez vouloir sérialiser, et la façon la plus simple de le faire est d'utiliser la sérialisation Java. Ainsi, nous sérialisons non seulement Song, mais aussi MusicPlayer et AudioDevice. Ni MusicPlayer ni AudioDevice ne doivent être sérialisés. Comme vous pouvez le voir, de petits changements facilitent grandement la testabilité.

Comme vous pouvez le voir, travailler avec du code est plus facile si nous séparons ces deux types d'objets. Si vous les mélangez, votre code sera difficile à tester. Les objets qui se trouvent à la fin du graphique d'objet de votre application sont nouveaux. Newable peut dépendre d'autres Newable, par exemple, comment CreditCard peut dépendre de Address, qui peut dépendre de City - ces choses sont des feuilles du graphique d'application. Puisqu'ils sont des feuilles et ne communiquent avec aucun service externe (les services externes sont injectables), ils n'ont pas besoin de faire de stubs. Rien ne ressemble plus à String que String lui-même. Pourquoi devrais-je créer un talon pour l'utilisateur, si je peux simplement appeler un nouvel utilisateur, pourquoi faire des talons pour tout cela: Email, MailMessage, Utilisateur, CreditCard, Song? Appelez simplement nouveau et terminez.

Faisons maintenant attention à quelque chose de très subtil. Il est normal que Newable connaisse Injectable. Ce qui n'est pas normal, c'est que Newable a une référence à Injectable en tant que champ. En d'autres termes, Song peut connaître MusicPlayer. Par exemple, il est normal que Injectable MusicPlayer passe à travers la pile vers Newable Song. Parce que le passage à travers la pile est indépendant du cadre d'injection de dépendance. Comme dans cet exemple:

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

Le problème se produit lorsque Song a un champ de lien vers MusicPlayer. Les champs de lien sont définis par le constructeur, ce qui entraînera une violation de la loi Demeter pour l'appelant et des difficultés pour nos tests.

En savoir plus sur les cours



All Articles