Affordances: Dependency Injection

In an object-oriented system, many objects work with other objects to
do their jobs. When working with these collaborators, one of the
first questions is always, “How does this object find out who its
collaborators are?”

If the collaborator is a simple helper object, the most direct
solution is for the object to create the collaborator itself.

Direct Construction of Collaborator

classWorker

definitialize

@helper=Helper.new

end

defoperation

@helper.help_me

end

end

The direct solution can work well in many cases. However, if the
collaborator performs expensive computation or accesses external
resources, the owning object can become difficult or expensive to
test. When that happens, it is helpful for the tests to provide an
alternate collaborator, such as a stub or mock object. This keeps the
tests running fast and avoids dependencies on external resources.

Injecting a Collaborator

classWorker

definitialize(helper)

@helper=helper

end

defoperation

@helper.help_me

end

end

classWorkerTest<Minitest::Test

defsetup

@helper=Minitest::Mock.new

@worker=Worker.new(@helper)

end

# ...

end

defdomain_code

worker=Worker.new(Helper.new)

worker.operation

end

This pattern of providing a collaborator is called
Dependency Injection.
Martin Fowler has written
a very good introduction to the topic.
I won’t debate the pros and cons of using Dependency Injection here.
Instead, my focus is on the affordances provided by various
programming languages to support this pattern.

In a dynamically-typed language like Ruby, explicit interfaces are
not necessary. We can just take advantage of “duck-typing”. However,
even though we don’t have to create an explicit interface, it’s good
to remember that we are still defining an implicit interface that
consists of all of the messages the owning object sends to the
collaborator.

In an explicitly-typed language like C++, C#, or Java, it is
necessary to define an explicit interface for the injected collaborator.
The real collaborator and the substitute both have to implement the
interface:

Dependency Injection in C++

classHelper

{

public:

virtual~Helper(){}

virtualvoidhelpMe()const=0;

};

classRealHelper:publicHelper

{

public:

virtualvoidhelpMe()const

{

// Implementation

}

};

classWorker

{

public:

Worker(constHelper&helper):

helper{helper}

{

}

voidoperation()

{

helper.helpMe();

}

private:

constHelper&helper;

};

classFakeHelper:publicHelper

{

public:

virtualvoidhelpMe()const

{

// Implementation

}

};

voidtestCode()

{

FakeHelperhelper;

Workerworker{fakeHelper};

worker.operation();

}

voiddomainCode()

{

RealHelperhelper;

Workerworker{realHelper};

worker.operation();

}

One of the criticisms of Dependency Injection is that it makes the
client code more verbose, because all clients have to specify the
dependencies. In the case of a single real implementation of the
injected collaborator, this can get noisy. A possible solution is to
make use of
argument-passing affordances
to provide a default implementation.

Smalltalk requires two methods to specify the default collaborator,
but it means that we can write any Smalltalk code we like to create
the default collaborator before passing it on:

Default Collaborator in Smalltalk

Workerclass>>new

^selfhelper:Helpernew

Workerclass>>helper:aHelper

^selfbasicNewinitializeHelper:aHelper

Worker>>initializeHelper:aHelper

helper:=aHelper

Ruby is simpler than Smalltalk because we don’t need the second
method, but it is slightly less flexible in that we only get a single
Ruby expression to create the default collaborator. If more complex
creation is necessary, we might have extract a helper method or
similar:

Default Collaborator in Ruby

classWorker

definitialize(helper=Helper.new)

@helper=helper

end

# ...

end

The Java community has developed several Dependency Injection
frameworks that use external metadata, like XML files, to specify how
collaborators should be wired together. I won’t dig deeper into
those here, but will mention that reflection facilities are a required
affordance for that kind of solution.

Programming languages provide several different kinds of affordances
that impact the implementation of the Dependency Injection pattern,
and those affordances impact the kinds of solutions we think about in
those languages.