My Thoughts: Double Constructor for Dependency Injection
I know I'll have disagreement on this; and I'm happy about that.
One of the mechanisms I use for... what I can only call "Dependency Injection" or "Inversion of Control"... is to have multiple constructors. In the ideal I just have a single constructor.
This constructor takes all the arguments... or in the example below; an argument.
class MyClass{
public MyClass(Thingy thingy){
...
}
}
This prevents MyClass
from having to use new
. new
ing things in a class is a code smell. It makes TDD harder. You can't fake/mock when you new
. There's PowerMock which has bytecode re-writing to allow it; but that exists as a solution to unchangable legacy code. Greenfield code should not need PowerMock.
Often there is a 'default' implementation of the desired class. In languages that support default params, something like this is possible.
class MyClass
{
public MyClass(Thingy thingy = new Thingy())
{
...
}
}
Where if not provided a value; it's given a default value. (Though I have issues with C#'s implementation of default values)
Not all languages support default values (Looking at you Java!). Some support it in ways I find wonderful (I <3 you, Kotlin).
When there are limits to default values; there's likely a needfor alternatives to inject dependencies.
There's a pretty active space around Dependency Injection. On C#, for a REST api; I recently used Unity
. Works great in that environment. It's not "preloaded" it just hooks itself into the flow and generates the correct types.
Allows TDD and mock/fake injection wonderfully.
On Android; Dagger serves a lot of the same purpose. It's a very popular DI tool; and I'm not a fan of using it. I feel that it's a workaround for poorly designed code. Things that get the Big Upfront Design tend to need things to account for a poorly testable design. Same as Robolectric and PowerMock; It's a crutch for something broken.
I very heavily try to pass in everything the class needs to consume. Either a builder; or factory; or passing it in. I don't follow this all the time; as long as I can TDD and tests are passing; I'm a little non-strict on it.
We do come across points in Android that I have no solution I like. Simplest example - an Activity.
I can't modify the constructor. This is the framework Android OS uses to interact with the app - the no arg constructor is required.
How do we do DI?
What I do; is a "Double Constructor'. Hopefully this isn't a shock given the title. :\
I find this to be aptly named. I use two constructors.
class MyActivity{
MyActivity(){
this(new Thing(), new OtherThing());
}
MyActivity(Thing thing, OtherThing other){
...
}
}
This conforms to the requirements of the otherside of the boundary (Android OS) and allows some dependency injection and test ability.
This doesn't actually work 100% of the time in Java; I've found work arounds. I don't recall the exact example; but java dislikes the use of "this" before the super constructor has been called.
class MyClass extends BaseClass{
MyClass(){
this(new Consumer(this));//Error
}
MyClass(Consumer consumer){
base();
...
}
}
Given this; it's very hard to do DI on objects that require a reference to the type creating them... This is also odd from a dependency graph perspective. ...
Let's assume there's an interface involved
interface Foo{
}
class Consumer{
Consumer(Foo foo){
...
}
}
class MyClass extends Base implements Foo{
MyClass(){
this(new Consumer(this));//Error
}
MyClass(Consumer consumer){
base();
...
}
}
Now there's no cyclic dependency concern. Still an error; still need a solution to make object instantiation possible for testing.
I think this pattern is applicable in a very narrow case (cough android system class no arg constructors cough) but it's something I've faced and I don't know a better option. OK, I do; use Kotlin... But we're limited at the moment.
Currently I've only faced this with the Activities.
Though... sigh I think writing this I've found a new way around it.
1/2 the reason I write; force my brain to work through a problem; to think about the arguments against it; of other ways around it.
...
I just spent about 30 minutes reviewing the HackerNews app code to see if my thoughts panned out. ... they don't.
There is a few ways I can modify the code to not ened the double constructor; none feel clean. This includes the double constructor. I feel that the double constructor is the minimal bad.
Do I pass objects in via the savedInstanceState
of the onCreate? I'll have code; or an entire method, dedicated to doing things for test. This will be intertwined with non-test code.
Do I have a factory for these non-variable objects that I can then fake and stuff in? This is an extra class for test to avoid just a constructor.
Do I weaken encapsulation and have test access to the no-longer-private class variables? Tests trump encapsulation; but that's not license to make everything public.
I know I have the double constructor pattern in code I don't NEED it... I'll hunt that down and correct it soon.
Lack of pairing comes up a lot as the reason for a lot of the "I'll fix it soon" I end up saying.
In the end; I thought I had ideas of how to resolve it for the Activities... and none feel clean. Some just feel less dirty.
It's all I really have on this - It's a tool that's the only one I have for certain conditions; and... maybe it leaks into other areas that lack a pair to complain about it.
I'm always looking for ways to improve the boundary, or bookend, classes and minimize the ... unclean code that allows testing functionality in the limited situation. I'm a huge proponent of "get it working then clean" The Green and Refactor parts. I don't always know how to get it as clean as I'd like; and I suspect there's not always going to be a way in all situations; but I'll find a way to get MOST of the way there.
If I learn better ways later; I'm going to jump onto that immediately. I want to utilize the best ways I know of to accomplish the task - I am with Double Constructor. ... Ish.
I think a better way to phrase it is;
This is the worst way I can imagine to do dependency injection for android Activities; except all others.
Give me another; and if it's better; then it'll be the "worst except all others".
Until then...