To be “new” or not to be …

Hello again. In anticipation of the start base and advanced courses on Android development We have prepared another interesting translation for you.

Dependency injection requires us split operators new and application logic. This separation encourages you to use factories in your code that are responsible for linking your application. However, rather than writing factories, we’d better use automatic dependency injectionsuch as GUICE, which would take over the binding. But can dependency injection really save us from all operators new?

Let’s look at two extremes. Say you have a MusicPlayer class that should get AudioDevice. Here we want to use dependency injection and request an AudioDevice in the MusicPlayer constructor. This will allow us to add a test-friendly AudioDevice, which we can use to claim that the correct sound comes out of our MusicPlayer. If we used the new operator to create an instance of BuiltInSpeakerAudioDevice, then we would have some testing difficulties. So let’s call objects like AudioDevice or MusicPlayer “Injectable”. Injectable are the objects that you will request in the constructors and expect that the framework for implementing dependencies will provide them to you.

Now to the other extreme. Suppose you have an int int primitive, but you want to auto-pack it into Integer, the simplest is to call new Integer (5), and that’s the end. But if dependency injection is the new “new”, why do we call new in-line? Will it hurt our testing? It turns out that frameworks for injecting dependencies cannot give you the Integer that you need, because they don’t understand what kind of Integer it is. This is a somewhat toy example, so let’s look at something more complex.

Suppose a user has entered an email address in the login field, and you need to call new Email («a@b.com») Can we leave it like this, or should we request Email in our constructor? Again, the dependency injection framework cannot provide you with Email, because you must first get the String in which the email is located. And there are a lot of String s to choose from. As you can see, there are many objects that the dependency injection framework can never provide. Let’s call them “Newable”, as you will be forced to call for them new manually.

First, let’s set some basic rules. An Injectable class can query other Injectable in its constructor. (Sometimes I call Injectable as a Service Object, but this term is overloaded.) Injectable tend to have interfaces, since there is a chance that we will have to replace them with an implementation that is convenient for testing. However, Injectable can never query non-Injectable (Newable) in its constructor. This is because the dependency injection framework does not know how to create a Newable. Here are a few examples of classes that I would expect to get from my dependency injection framework: CreditCardProcessor, MusicPlayer, MailSender, OfflineQueue. Similarly, Newable can be requested by other Newable in their constructor, but not Injectable (sometimes I call Newable as a Value Object, but again, this term is overloaded). Some examples of Newable: Email, MailMessage, User, CreditCard, Song. If you follow these distinctions, your code will be easy to test and work with. If you break these rules, your code will be difficult to test.

Let’s look at the example of MusicPlayer and Song

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

Note that Song only queries objects that are Newable. This makes it very easy to instantiate a Song in a test. MusicPlayer is fully Injectable, like its AudioDevice argument, so it can be obtained from the framework for dependency injection.

Now let’s see what happens if MusicPlayer breaks the rule and requests Newable in its constructor.

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

Here, Song is still Newable, and it’s easy to create in your test or in your code. MusicPlayer is already a problem. If you ask MusicPlayer from your framework for dependency injection, it will crash because the framework will not know which Song it is about. Most people new to dependency injection frameworks rarely make this mistake, as it is easy to notice: your code will not work.
Now let’s see what happens if Song breaks the rule and requests Injectable in its constructor.

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

At first glance, everything is fine. But think about how Song will be created. Presumably, the songs are stored on disk, so we need SongReader. SongReader will need to request MusicPlayer so that when calling new for Song, it can satisfy Song’s dependencies on MusicPlayer. Have you noticed anything wrong here? What fright does SongReader need to know about MusicPlayer? it violation of the law of Demeter. SongReader should not know about MusicPlayer. At least because SongReader does not call methods with MusicPlayer. He only knows about MusicPlayer because Song has violated the Newable / Injectable separation. SongReader pays for an error in Song. Since the place where the error is made and where the consequences are manifested is not the same thing, this error is very subtle and difficult to diagnose. It also means that many people are likely to make this mistake.

In terms of testing, this is a real pain. Suppose you have SongWriter and want to make sure it serializes Song to disk correctly. You need to create a MockMusicPlayer so that you can pass it to Song so that you can pass it to SongWritter. Why do we even come across MusicPlayer here? Let’s look at it the other way. Song is what you might want to serialize, and the easiest way to do this is to use Java serialization. Thus, we serialize not only Song, but also MusicPlayer and AudioDevice. Neither MusicPlayer nor AudioDevice should be serialized. As you can see, small changes greatly facilitate testability.

As you can see, working with code is easier if we separate these two kinds of objects. If you mix them, your code will be difficult to test. Newable are the objects that are at the end of the object graph of your application. Newable may depend on other Newable, for example, how CreditCard may depend on Address, which may depend on City – these things are sheets of the application graph. Since they are sheets and do not communicate with any external services (external services are Injectable), they do not need to do stubs. Nothing looks like String anymore than String itself. Why should I make a stub for User, if I can just call new User, why make stubs for any of this: Email, MailMessage, User, CreditCard, Song? Just call new and end it.

Now let’s pay attention to something very subtle. It is normal for Newable to know about Injectable. What is not normal is that Newable has a reference to Injectable as a field. In other words, Song can know about MusicPlayer. For example, it is normal for Injectable MusicPlayer to be passed through the stack to Newable Song. Because passing through the stack is independent of the framework for dependency injection. Like in this example:

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

The problem occurs when Song has a link field to MusicPlayer. Link fields are set through the constructor, which will cause violation of the law of Demeter for the caller and difficulties for our tests.

Learn more about courses:

Similar Posts

Leave a Reply Cancel reply