Delphi, Dependency Injection and Memory Management
Dependency Injection is certainly one of the most important concepts when trying to write testable code. But while Java and C# have garbage collection, Delphi has not and normally, object disposal is managed using the ownership-principle (the one who creates the object destroys it). This is nicely supported by the try..finally
construct
Obj := TObject.Create;
try
...
finally
Obj.Free;
end;
Now what if one uses dependency injection:
constructor TFileLister.Create(FileSystem: TFileSystem);
Who should now be responsible for destroying the FileSystem
object? Does the ownership-principle still work here?
I know that interfaces are a solution to this problem (thanks to the fact that they are reference-counted). But what if there are no interfaces (say in some legacy code)? What other approaches or best practices are there to handle memory management when using depende开发者_运维技巧ncy injection?
You have to come up with an owner for the FileSystem object. This can be either the entity that creates the TFileLister instances, or you could pass ownership to the file lister, documenting that it will free the file system that was passed to the constructor.
The right approach depends on course on your particular application. For example, if other objects would also use the same file system object, it shouldn't be owned by one of these such as the file lister, but by the object that ties it all together. You could even make the file system object global if it only makes sense to have one of it.
In short, you'll have to do a little more thinking than in Java but that's not necessarily a bad thing.
It's almost always preferable to regard the entity that create an object also to be its owner (i.e. responsible for destroying it).
To understand why I say this, consider the alternative. Suppose that object A creates object B. At some point later it passes B to object C which becomes the owner.
In the period between creating B and handing it over to C, A is responsible for destruction in case of exceptions, or perhaps the selection of a branch that bypasses C. On the other hand, once it has handed off B, A must not attempt to destroy C.
All this can be handled with sufficient care. One approach is that taken by the VCL
with TComponent.Owner
.
However, if you can find a way to stick to the two standard patterns of ownership then do so.
What are the two standard patterns?
- Create in a constructor and assign to a field; destroy in the matching destructor.
- Create and destroy inside a single method, with protection provided by
try
/finally
.
I would strongly recommend that you try to shape your code so that all resource acquisition uses one of these two options.
How can you do so in your example? The option that leaps out at me is to use a factory to create your FileSystem
object. This allows TFileLister
to manage the lifetime of the FileSystem
object, but gives you the flexibility of injecting different behaviour into TFileLister
.
I disagree with the notion that an object must be destroyed by the one that created it. Many times this is the natural choice but its certainly not the only way of managing memory. A better way to look at it is that an object's lifetime should end when it is no longer needed.
So what options do you have?
Use Interface reference counting
In many cases it is trivial to extract an interface from an existing class so don't shelve this idea just because your working with legacy code.
Use an IoC container that supports lifetime management.
There are a number of IoC containers for Delphi and more are popping up all the time. Spring for Delphi is one that I know of that supports lifetime management. Note: most of these containers target Delphi 2010 or newer so it may be difficult to find one for legacy code.
Use a garbage collector.
The Boehm GC memory manager is the only one I'm aware.
All three of these can be combined with poor man's dependency injection to get the testing benefits while making minimal changes to your legacy code. For those unfamiliar with the term you use constructor chaining to instantiate a default dependency.
constructor TMyClass.Create;
begin
Create(TMyDependency.Create);
//Create(IoCContaineror.Resolve(TMyDependency));
end;
constructor TMyClass.Create(AMyDependency: TMyDependency)
begin
FMyDependency := AMyDependency;
end;
Your production code continues to use the default constructor with the real object while your tests can inject a fake/mock/stub to sense what the class being exercised is playing nicely. Once your test coverage is high enough you can remove the default constructor.
精彩评论