Delphi, injection control and memory management

Injection of Dependency is by far one of the most important concepts when trying to write testable code. But while Java and C # have garbage collection, Delphi does not, and usually, the removal of objects is controlled using the ownership principle (the one who creates the object destroys it). This is well supported by the try..finally construct

 Obj := TObject.Create; try ... finally Obj.Free; end; 

Now, if you use dependency injection:

 constructor TFileLister.Create(FileSystem: TFileSystem); 

Who should now be responsible for destroying the FileSystem object? Does ownership still work here?

I know that interfaces are a solution to this problem (due to the fact that they are counted by reference). But what if there are no interfaces (say, in some outdated code)? What are other approaches or guidelines for managing memory when using dependency injection?

+4
source share
3 answers

You must come up with the owner of the FileSystem object. It can either be an object that instantiates TFileLister, or you can transfer ownership of the lister file by documenting that it will free the file system that was passed to the constructor.

The right approach depends on the course of your particular application. For example, if other objects will also use the same file system object, it should not belong to one of them, for example, a file lister, but an object that ties it all together. You can even make the file system object global, if only it makes sense to have one of them.

In short, you will need to think a little bit than in Java, but this is not necessarily bad.

+7
source

It is almost always preferable to consider the object that creates the object, also to be its owner (i.e. responsible for its destruction).

To understand why I say this, consider an alternative. Suppose object A creates object B. At some point, it passes B to object C, which becomes the owner.

Between the creation of B and its transfer to C, A is responsible for destroying in case of exceptions, or possibly for choosing a branch that bypasses C. On the other hand, once it passes B, A should not try to destroy C.

All this can be handled with sufficient care. One approach is to use VCL with TComponent.Owner .

However, if you can find a way to stick to two standard ownership patterns, do it.

What are the two standard templates?

  • Create in the constructor and assign a field; destroy in the appropriate destructor.
  • Create and destroy within one method, with the protection provided by try / finally .

I highly recommend that you try building your code so that all resource acquisitions use one of these two options.

How can you do this in your example? The option that appears to me is to use factory to create your FileSystem object. This allows TFileLister to control the lifetime of the FileSystem object, but gives you the flexibility to introduce different behaviors into TFileLister .

+2
source

I do not agree that the object must be destroyed by those who created it. Many times this is a natural choice, but this, of course, is not the only way to manage memory. The best way to look at this is that the life of an object should end when it is no longer needed.

So what are your options?

Use interface reference count

In many cases, it is trivial to extract an interface from an existing class, so do not put off this idea just because you are working with legacy code.

Use an IoC container that supports lifecycle management.

There are several IoC containers for Delphi, and all this time appears. Spring for Delphi is the one I know that supports lifecycle management. Note. Most of these containers target Delphi 2010 or later, so it can be difficult to find one for legacy code.

Use the garbage collector.

The GC Boehm memory manager is the only thing I know.

All three of these can be combined with poor dependency injection to get the benefits of testing with minimal changes to your legacy code. For those unfamiliar with the term, you use the constructor chain to create default dependencies.

 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 may introduce a fake / layout / stub to understand that the class that is running plays well. Once your test coverage is high enough, you can remove the default constructor.

+1
source

Source: https://habr.com/ru/post/1339801/


All Articles