In Part 1 we covered the beginnings of rolling your own Dependency Injection framework as an alternative to using the Delphi Spring Framework. We discussed the patterns and objects involved and described using RTTI to perform the actual injection. Now we’ll look at an example of using DI to inject logging functionality into an object.
So, here is what DI looks like in action. Let’s say you build a car and all the salient action on that car, including construction, is logged in a logger. In Listing 1, ILogger
is an interface to some event logger like CodeSite, SmartInspect or maybe just writes lines to a text file. ICar
uses an instance of ILogger
on the inside to record vehicle events.
Listing 1
type ILogger = interface [bla bla bla] procedure WriteLn( const Line: string); end; ICar = interface [bla bla bla] procedure Drive; procedure TurnLeft; procedure RightRight; ... etc ... end;
We might implement ICar
something like listing 2. The [Injection]
attribute marks the data member for injection. We will see what this means soon.
Listing 2
type TCar = class( TInterfacedObject, ICar) private [Injection] FLogger: ILogger; procedure Drive; procedure TurnLeft; procedure RightRight; public constructor Create; end;
Ordinarily, at some higher level, we might use a car thus defined, as in listing 3.
Listing 3
procedure MakeAndDriveCar; // Uses unit in which TCar is defined. var MyCar: ICar; begin MyCar := TCar.Create; MyCar.Drive; MyCar.TurnLeft; .. etc end;
With DI, we do something slightly different (and already better), as in listing 4.
Listing 4
procedure MakeAndDriveCar; // Does not require visibility to TCar. var MyCar: ICar; begin MyCar := ServiceProvider.Acquire( ICar); // ServiceProvider is an "abstract factory" pattern. MyCar.Drive; MyCar.TurnLeft; .. etc end;
Basic stuff right? Now for the magic. List 4 show the constructor for TCar
.
Listing 5
constructor TCar.Create; begin FLogger.WriteLn( 'This car is created.'); end;
Whoah! Does it look like an AV right there on the first line of the constructor? Actually, this code works and works well thanks to DI. By the time of entry into the constructor, dependent data members and properties are already connected to the appropriate service. The car’s interconnection with the outside world is no longer a conceptual burdon on the Car. DI gives us the optimal level of object isolation and simplification.
Compare with the typical alternative constructors that we were used to before DI, in listing 6
Listing 6
constructor TCar.Create; begin FLogger := TMyLogger.Create( params); // Requires visibility to and dependency on TMyLogger FLogger.WriteLn( 'This car is created.'); end; ... or maybe .... constructor TCar.Create( const Logger1: ILogger); begin FLogger := Logger1; // Not so bad, but fattens up TCar's interface width. FLogger.WriteLn( 'This car is created.'); end;
Further Reading
View Nick Hodge’s excellent presentation on Dependency Injection and Spring Framework from CodeRage 6. Nick’s very popular blog includes many entries on DI.
As for rolling your own, you can light-weight DI demo here. Listing 6 shows the public interface to it.
Listing 6
IServiceProvider = interface; TServiceFactory = reference to function( Obj: TObject; const Config: string; const ServiceProvider: IServiceProvider): IInterface; IServiceProvider = interface ['{1C46BAEA-B723-4E07-B1A1-1F1E286AC421}'] function RegisterLiveService ( const ServiceIID: TGUID; const Service: IInterface; const Config: string=''): integer; // ^-- Register an already-created-object as a service. function RegisterFlyweightService( const ServiceIID: TGUID; CreateBase: TClass; Factory: TServiceFactory; const Config: string = ''): integer; // ^-- Register a service to be created on demand by a flyweight abstract factory. function RegisterServiceClass( Registrant: TClass; const ServiceIID: TGUID): TList; // ^-- Register a service to be created on demand with constructors decorated with [Configuration]. procedure DeregisterService( RegistrationCookie: integer); function Acquire( const ServiceIID: TGUID; out Intf; const Config: string = ''): boolean; // ^-- Acquire a service, with Dependency Injection. procedure ShutDown; end;
The code don’t compile with Delphi XE2 (Update 3)
I think this is due to :
function Acquire( const ServiceIID: TGUID; out Intf; const Config: string = ”):
Because this function is declared in an interface.
I changed the declaration to “out Intf: IInterface” and it works, but at user level it becomes ugly because you have to cast the out parameter to IInterface explicitly.
procedure TmfmDIYDI.actCreateCarExecute(Sender: TObject);
begin
if not Assigned(FCar) then
begin
FServiceProvider.Acquire( ICar, IInterface(FCar));
FCar.TurnLeft;
FCar.TurnRight;
end
end;
I think I find a better answer. When converted your Delphi 2010 project files, Delphi activated the option to explicitely emit runtime type information. I disabled the option and your code compile as-is now.