dependency injection - Forum - OpenEdge General - Progress Community
 Forum

dependency injection

  • reading up on dependency Injection (http://stevescodingblog.co.uk/dependency-injection-beginners-guide/) we eventually got to the point of having a container that maps the types and queries for the appropriate service.

    class Program
    {
     static SimpleDIContainer Container = new SimpleDIContainer();

    static void Main(string[] args)
    {
    // Map ILogger to TextFileLogger
    Container.Map<ILogger, TextFileLogger>();

    // Create a DirectoryWatcher using whatever implementation for ILogger contained in the DI container
    DirectoryWatcher watcher = new DirectoryWatcher(Container.GetService<ILogger>());
    }
    }

    I don't understand the point of this. Why not have the directorywatcher program get the logger service from the container directly ? If the container class is a static , then any test framework could map the appropriate service in the container (either a test logger or normal logger for example).

    why is it that the main program passes the logger type into the directory watcher ?

  • See that all is not that obvious? ;-) I already missed interfaces (& DI) in your OO presentation in Brussels, which was a pleasure to visit by the way. You are a good entertainer! If you gave a presentation about cooking a good meal or so I would be pleased to attend it too. Good counterweight to some of the American commercial talks (sorry guys).

     Looks like you just read my message on the peg. If you want to be or let's say stay among the cool guys you have get a grasp on all this, eh? :-) Class program passes the ILogger reference into the new DirectoryWatcher, that is constructor injection.  "This happens without any changes to the implementation of DirectoryWatcher , and this is the key concept. ". I have nothing to add I'm afraid, just let the text simmer a bit overnight in the back of your braincase and see what  boils down. And be sure to read the other links I sent:

    tutorials.jenkov.com/.../index.html

    and gunnarpeipman.com/.../refactoring-extract-interface.

    I found them very clear.

  • Ah, sorry, read your post too quick. Good question, I have no answer though.

  • simple_di.zip

    Julian asked:

    >I don't understand the point of this. Why not have the directorywatcher program get the logger service from the

    >container directly ? If the container class is a static , then any test framework could map the appropriate service in the

    >container (either a test logger or normal logger for example).

    >

    >why is it that the main program passes the logger type into the directory watcher ?

    The short answer is to ensure loose coupling between the functional programs and the infrastructure in which they run. Why loose coupling? Well, because if you want to change one thing you don't have to change many things (note this last statement is more effective when interfaces are used).

    There's a (quite long) answer below. It starts with the basics and works to an answer to your question starting at around, I think, line 142. I hope anyway :)

    Using the logger example from that link, transcribed into ABL. (I've attached a zip containing the related ABL to this post).

    Start with a traditional example: I've called this Tight.DirectoryWatcher.

    {abl}

    class Tight.DirectoryWatcher:

       define private variable moLogger as ILogger no-undo.

       define private variable moWatcher as FileSystemWatcher no-undo.

       constructor public DirectoryWatcher():

           /* [PJ] Note that we've hard-coded the TextFileLogger here */

           assign moLogger  = new TextFileLogger()

                  moWatcher = new FileSystemWatcher("c:/ftp").

           moWatcher:Changed:Subscribe(this-object:FileSystemEventHandler).

       end constructor.

       destructor DirectoryWatcher():

           moWatcher:Changed:Unsubscribe(this-object:FileSystemEventHandler).

       end destructor.

       method public void FileSystemEventHandler (input pcDirectory as character):

           moLogger:LogMessage(pcDirectory + " was changed").

       end method.

    end class.

    {abl}

    To run this, we use code similar to the below. Case 1 is simple: we start our watcher and it logs something, right? Well, maybe. We can't tell from this whether it logs anything, nor how. Now that may not be important (what with separation of concerns and suchlike) but it may be good to know.

    However, if we do care about logging, what happens if we want to change the logging? That's Case 2 and now we have to inspect, and change, and test, the DirectoryWaytcher code. We have tightly coupled our logging to our directory watcher.

    {abl}

    using Tight.*.

    define variable oWatcher as DirectoryWatcher no-undo.

    /* Case 1: maybe uses some logger - we have to inspect the DirectoryWatcher code to find out which one it is */

    oWatcher = new DirectoryWatcher().

    /* Case 2: we have to modify the DirectoryWatcher code to change it. */

    oWatcher = new DirectoryWatcher().

    {abl}

    With DI we can loosen that coupling. Lets change the DirectoryWatcher to pass in the logger, as below.

    {abl}

    class Loose.DirectoryWatcher:

       define private variable moLogger as ILogger no-undo.

       define private variable moWatcher as FileSystemWatcher no-undo.

       constructor public DirectoryWatcher(poLogger as ILogger):

           assign moLogger  = poLogger

                  moWatcher = new FileSystemWatcher("c:/ftp").

           moWatcher:Changed:Subscribe(this-object:FileSystemEventHandler).

       end constructor.

       destructor DirectoryWatcher():

           moWatcher:Changed:Unsubscribe(this-object:FileSystemEventHandler).

       end destructor.

       method public void FileSystemEventHandler (input pcDirectory as character):

           moLogger:LogMessage(pcDirectory + " was changed").

       end method.

    end class.

    {abl}

    Now our runner code must change. In Case 1 we know that there's logging, as well as the what it is. Since we use the ILogger interface we can infer that the DirectoryWatcher doesn't really care about which logger is used (not necessarily the case above). Even better, in Case 2 when we now want to change our logging implementation, we don't need to touch the DirectoryWatcher.

    {abl}

    using Loose.*.

    define variable oLogger as ILogger no-undo.

    define variable oWatcher as DirectoryWatcher no-undo.

    /* Case 1 */

    oLogger = new TextFileLogger().

    oWatcher = new DirectoryWatcher(oLogger).

    /* Case 2: Or, by just changing the right-hand side of the first line we can use our other implementation: */

    oLogger = new JsonFileLogger().

    oWatcher = new DirectoryWatcher(oLogger).

    {abl}

    That's much better - I can now tell that the DirectoryWatcher depends on logging, and I can easily change the type of logging.

    So that's basic dependency injection.  Now to the Inversion of control container. The container replaces the right-hand side of the logger instantiation lines and, instead of having hard-coded NEW statements, basically stores the left- and right-hand-side types as key/value pairs. Why? So that you can change the value without changing all instances of NEW xxxLogger().  If I have multiple instances of DirectoryWatcher in my application, I will have multiple lines of code to change.

    A simple sample container shown below. As you can see, it's basically a temp-table of key/value pairs and methods to get and set data into that temp-table. there's clearly more complexity that could be added - for instance, imposing some sort of singleton behaviour on the returned type - but the idea is largely the same.

    {abl}

    class Container.IoCContainer:

       define private temp-table ttTypeMap no-undo

           field InterfaceType as Object

           field ImplementingType as Object

           index idx1 as unique InterfaceType.

       constructor public IoCContainer():

       end constructor.

       method public void AddType(input poInterfaceType as class Class,

                                  input poImplementingType as class Class):

           find ttTypeMap where ttTypeMap.InterfaceType eq poInterfaceType no-error.

           if not available ttTypeMap then

           do:                                      

               create ttTypeMap.

               assign ttTypeMap.InterfaceType = poInterfaceType.

           end.

           ttTypeMap.ImplementingType = poImplementingType.                          

       end method.

       method public Object ResolveType(input poInterfaceType as class Class):

           find ttTypeMap where ttTypeMap.InterfaceType eq poInterfaceType no-error.

           /* [PJ] Here we instantiate the implementing type */

           return cast(ttTypeMap.ImplementingType, Class):New().

       end method.

    end class.

    {abl}

    Now we change our calling code slightly.

    {abl}

    /* system startup */

    using Container.*.

    oContainer = new IoCContainer().

    /* 11.4 style: oContainer:AddType(get-class(ILogger), get-class(TextFileLogger)). */

    /* earlier style */

    oContainer:AddType(

       Class:GetClass('ILogger'),

       Class:GetClass('TextFileLogger')).

    /* Case 1. */

    oLogger = cast(oContainer:ResolveType(Class:GetClass('ILogger')), ILogger).

    oWatcher = new DirectoryWatcher(oLogger).

    /* Add a new map for Case 2 */

    oContainer:AddType(Class:GetClass('ILogger'),

                      Class:GetClass('JsonFileLogger')).

    /* Case 1 and Case 2 now look the same */

    oLogger = cast(oContainer:ResolveType(Class:GetClass('ILogger')), ILogger).

    oWatcher = new DirectoryWatcher(oLogger).

    {abl}

    Everything OK so far? Now we get to the point of answering Julian's question, which is "Why not have the directorywatcher program get the logger service from the container directly?". In other words, why not do this in the constructor?

    {abl}

    constructor public DirectoryWatcher(poContainer as IocContainer):

       /* Use the Container directly */

       assign moLogger  = cast(poContainer:ResolveType(Class:GetClass('ILogger')), Ilogger)

              moWatcher = new FileSystemWatcher("c:/ftp").

       moWatcher:Changed:Subscribe(this-object:FileSystemEventHandler).

    end constructor.

    {abl}

    And indeed, why not? Simply put, we have not introduced a strong/tight dependency between our application objects (the DirectoryWatcher) and our IoC Container. So strong, that we now *must* use that container for resolving types and solving dependencies. Furthermore, we add back some opacity - we cannot tell externally what types the DirectoryWatcher needs (ie does it need an ILogger?). I am a big fan of highlighing the mandatory dependencies an object has via constructor arguments - that way you can be sure that if a DirectoryWatcher needs an ILogger to work, it will get one. Specifically if the DirectoryWatcher needsan ILogger, it should not care where that ILogger came from or how it was instantiated. It should just get something usable.

    This blog post has some detail on the why not to use the service locator approach (the author has some strong opinions on the matter): blog.ploeh.dk/.../ServiceLocatorisanAnti-Pattern

    You may have already read this page, but the DI container that AutoEdge uses is described at community.progress.com/.../998.oeri-dependency-injection-container-injectabl.aspx . There are a couple of links there that talk about the what (www.jamesshore.com/.../Dependency-Injection-Demystified.html) and why (github.com/.../Dependency-Injection-By-Hand)

    Hope this clarifies matters somewhat.

    -- peter