I want to give a fairly simple Delphi example that will expose the dependency injection pattern. No framework, no third-party library will be needed here: just plain Delphi code.
I won’t dig into the different forms of dependency injection. I will explain the idea of the pattern as simply as possible.
Instead of giving you bunch of definitions, I will present you with some code. The need to inject a dependency will come naturally. You’ll see:
type
IDependency = interface
['{618030A2-DB17-4532-81D0-D5AA6F73DC66}']
procedure GetType;
end;
TDependencyA = class(TInterfacedObject, IDependency)
public
procedure GetType;
end;
TDependencyB = class(TInterfacedObject, IDependency)
public
procedure GetType;
end;
TConsumer = class
private
FDependency: IDependency;
public
constructor Create(aDependencyClassName: string);
procedure GetDependencyType;
end;
implementation
procedure TDependencyA.GetType;
begin
WriteLn('Instance of type TDependencyA');
end;
procedure TDependencyB.GetType;
begin
WriteLn('Instance of type TDependencyB');
end;
constructor TConsumer.Create(aDependencyClassName: string);
begin
if aDependencyClassName = 'TDependencyA' then
FDependency:= TDependencyA.Create
else if aDependencyClassName = 'TDependencyB' then
FDependency:= TDependencyB.Create;
end;
procedure TConsumer.GetDependencyType;
begin
if FDependency <> nil then
FDependency.GetType;
end;
It is a good and recommended practice in OOP to decrease coupling as much as possible. This means that each component (class) should avoid knowing implementation details of the other components (classes).
In our example, the TConsumer
class has a dependency of type IDependency
. So far so good, since we are abstracting any implementation specifics by using an interface
(contract). The problem becomes obvious when you take a look at the constructor of TConsumer
.
TConsumer.Create
is instantiating the concrete classes TDependencyA
or TDependencyB
depending on the string
parameter aDependencyClassName
. As you can see, the design is not well decoupled here, because the consumer class (TConsumer
) is hard-coding implementation details about its dependency.
The question is: can we decouple this design even more? Yes, the dependency injection pattern will do it for us.
It’s now time to refactor our code a little bit. We’ll start by changing the signature of the constructor of the TConsumer
class:
constructor TConsumer.Create(aDependency: IDependency);
begin
FDependency:= aDependency;
end;
Instead of choosing the concrete dependency to instantiate within the constructor, we are now injecting the dependency through the aDependency
parameter. Now the class TConsumer
is completely independent of the dependency concrete class.
OK, OK, but we still need to create the concrete dependency instance somewhere, right? Yes, we do. For that, we will create a new class TDependencyInjector
whose only purpose is to return the right dependency instance. This class will use reflection in order to create the right instance of IDependency
. It will use just a string
parameter that contains the dependency class name.
uses
RTTI,
Dependencies;
type
TDependencyInjector = class
public
class function GetDependency(aDependencyClassName: string): IDependency;
end;
implementation
class function TDependencyInjector.GetDependency(aDependencyClassName: string): IDependency;
var
RttiContext: TRttiContext;
RttiType: TRttiInstanceType;
begin
RttiType := RttiContext.FindType(aDependencyClassName) as TRttiInstanceType;
if RttiType <> nil then
Result:= RttiType.GetMethod('Create').Invoke(RttiType.MetaclassType, []).AsInterface as IDependency;
end;
Finally, let's put all the pieces together. Consider this console application that puts all the pieces in place:
program DependencyInjection;
uses
...
var
SomeConsumerObj: TConsumer;
Dependency: IDependency;
begin
Dependency:= TDependencyInjector.GetDependency('Dependencies.TDependencyA');
SomeConsumerObj:= TConsumer.Create(Dependency);
try
SomeConsumerObj.GetDependencyType;
finally
SomeConsumerObj.Free;
end;
Readln;
end.
In the code above, we get the Dependency
instance at runtime by using the TDependencyInjector
class. Then, we inject that dependency using the constructor of the class TConsumer
. We have got a more decoupled design by using dependency injection. Don't you agree? ;-)
Get the full source code of this example here (written in Delphi XE 2).