QR code

Operator new() is Toxic

  • Moscow, Russia
  • comments

OOPoop

To instantiate objects, in most object-oriented languages, including Java, Ruby, and C++, we use operator new(). Well, unless we use static factory methods, which we don't use because they are evil. Even though it looks so easy to make a new object any time we need it, I would recommend to be more careful with this rather toxic operator.

The Gift (2015) by Joel Edgerton
The Gift (2015) by Joel Edgerton

I'm sure you understand that the problem with this operator is that it couples objects, making testing and reuse very difficult or even impossible. Let's say there is a story in a file that we need to read as a UTF-8 text (I'm using TextOf from Cactoos):

class Story {
  String text() {
    return new TextOf(
      new File("/tmp/story.txt")
    ).asString();
  }
}

It seems super simple, but the problem is obvious: class Story can't be reused. It can only read one particular file. Moreover, testing it will be rather difficult, since it reads the content from exactly one place, which can't be changed at all. More formally this problem is known as an unbreakable dependency—we can't break the link between Story and /tmp/story.txt—they are together forever.

To solve this we need to introduce a constructor and let Story accept the location of the content as an argument:

class Story {
  private final File file;
  Story(File f) {
    this.file = f;
  }
  String text() {
    return new TextOf(this.file).asString();
  }
}

Now, each user of the Story has to know the name of the file:

new Story(new File("/tmp/story.txt"));

It's not really convenient, especially for those users who were using Story before, knowing nothing about the file path. To help them we introduce a secondary constructor:

class Story {
  private final File file;
  Story() { // Here!
    this(new File("/tmp/story.txt"));
  }
  Story(File f) {
    this.file = f;
  }
  String text() {
    return new TextOf(this.file).asString();
  }
}

Now we just make an instance through a no-arguments constructor, just like we did before:

new Story();

I'm sure you're well aware of this technique, which is also known as dependency injection. I'm actually not saying anything new. What I want you to pay attention to here is the location and the amount of new operators in all three code snippets.

In the first snippet both new operators are in the method text(). In the second snippet we lost one of them. In the third snippet one operator is in the method, while the second one moved up, to the constructor.

Remember this fact and let's move on.

What if the file is not in UTF-8 encoding but in KOI8-R? Class TextOf and then method Story.text() will throw an exception. However, class TextOf is capable of reading in any encoding, it just needs to have a secondary argument for its constructor:

new TextOf(this.file, "KOI8_R").asString();

In order to make Story capable of using different encodings, we need to introduce a few additional secondary constructors and modify its primary constructor:

class Story {
  private final Text text;
  Story() {
    this(new File("/tmp/story.txt"));
  }
  Story(File f) {
    this(f, StandardEncodings.UTF_8);
  }
  Story(File f, Encoding e) {
    this(new TextOf(f, e));
  }
  Story(Text t) {
    this.text = t;
  }
  String text() {
    return this.text.asString();
  }
}

It's just dependency injection, but pay attention to the locations of the operator new. They are all in the constructors now and none of them are left in the method text().

The tendency here is obvious to me: the more the new operators stay in the methods, the less reusable and testable is the class.

In other words, operator new is a rather toxic thing, so try to keep its usage to a minimum in your methods. Make sure you instantiate everything or almost everything in your secondary constructors.

sixnines availability badge