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.
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.