Freigeben über


Dependency Injection / Inversion of Control–A Concrete Example-Roll your own

 Who’s the wizard behind the curtain?

I’ve been reading Martin Fowler’s post about Inversion of Control / Dependency Injection at https://www.martinfowler.com. I looked long and hard for very simple examples that were easy to follow. I wanted to explore how they work, not just how to use an existing library.

How exactly does Dependency Injection work? It seemed rather mysterious to me from the implementation point of view. Some people look at a compiler and think, “Wow, that’s cool how it could translate source code into machine code.”

I’m the type of person that wants to know how exactly it works.

So in this post I show you how you might start to think about building a Dependency Injection container.   

I focus on code here. If you want to read all the friendly narrative, here is Martin’s post:

Inversion of Control Containers and the Dependency Injection pattern https://martinfowler.com/articles/injection.html

The difficult way to learn how things work – using pre-built libraries

But the basic guidance is to use pre-built libraries. Microsoft historically supported the enterprise library, Unity. There is also Castle Windsor as an available library.

The Unity Application Block https://msdn.microsoft.com/en-us/library/ff648512.aspx
Castle Windsor https://www.castleproject.org/castle/download.html

Roll your own

But I was curious. What could I create in C# that mimicked the basic principles using some of features of C# just to illustrate the basic point with a simple, concrete example?

I’m borrowing heavily from Martin Fowler here, since he is highly regarded in software architecture space.

The definition in 2 sentences

In the Dependency Injection pattern, this decision is delegated to the "injector" which can choose to substitute different concrete class implementations of a dependency contract interface at run-time rather than at compile time. Being able to make this decision at run-time rather than compile time is the key advantage of dependency injection.

Conventional OO – The dependent object is in control

With conventional software development the dependent object decides for itself what concrete classes it will use.

Referring to Figure 1, this means the constructor for TextFileLister decides what concrete classes it will use.

The dependent object will typically choose from among the two classes in Figure 2

Figure 2 demonstrates that TextFileLister could choose either CommaTextFileReader or TabTextFileReader to process and read text files.

This is where the term Dependency Injection comes into play.

Figure 1 – The Dependency Injection Diagram

utu15abf

Figure 2 – The interface and the corresponding implementation

rnoxfijc

Let the assembler (MutableContainer) make the decision

The basic idea of the Dependency Injection sample we are writing is to have a separate object, an assembler, that populates a field in the TextFileLister class with an appropriate implementation for the IFileReader interface, resulting in a dependency diagram along the lines of Figure 1.

In a nutshell, the assembler makes the call. It chooses either CommaTextFileReader or TabTextFileReader. The key point is that TextFileLister doesn’t make the decision.

Figure 3 illustrates the Visual Studio project I created to demonstrate the points.

Figure 3 – Visual Studio

ln4vue31

CommaDelimitedData.txt Contains a comma-delimited list of strings
CommaTextFileReader.cs Reads and parses CommaDelimitedData.txt, using commas as the delimeter.
IFileReader.cs The interface to CommaTextFileReader and TabTextFileReader
MutableContainer.cs The assembler which will build and configure objects at runtime. In a nutshell, it can be used to use either CommaTextFileReader or TabTextFileReader. This is the secret sauce to dependency injection.
Program.cs The main driver program containing main()
TabDelimitedData.txt Contains a tab-delimited list of strings
TabTextFileReader.cs Reads and parses TabDelimitedData.txt, using tabs as the delimeter
TextFileLister.cs The dependent object that uses the interface IFileReader. The TextFileLister will rely on CommaTextFileReader or TabTextFileReader, depending what the assembler decides (MutableContainer)

The code

Let’s start to explore the code. We’ll start with the basic objects and work our way up to the assembler.

Figure 4 is pretty straightforward and shows us the interface used by the two implementing classes in Figures 5 and 6. This is critical that we have an interface. Interfaces allow us to swap out the implementations without runtime or compile time errors.

Figure 4: IFileReader.cs - The interface to CommaTextFileReader and TabTextFileReader

 using System;
namespace IocForDummies
{
    interface IFileReader
    {
        void Close();
        int CountColumns();
        int CountLines();
        void FillData(string[,] data);
        bool HasData();
        void ReadLine(string s);
    }
}

This is one of the two implementation files that use IFileReader. The code is self-explanatory. It opens a text file and parses it out. In this case it is simply parsing a comma-delimited text file.

Figure 5: CommaTextFileReader.cs - Reads and parses CommaDelimitedData.txt, using commas as the delimeter

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace IocForDummies
{
    public class CommaTextFileReader : IocForDummies.IFileReader
    {
        FileStream fsReader = null;
        StreamReader streamReader = null;

        public CommaTextFileReader(string path)
        {
            fsReader = File.Open(path, FileMode.Open);
            streamReader = new StreamReader(fsReader, System.Text.Encoding.ASCII);

        }
        public void ReadLine(string s)
        {
            s = streamReader.ReadLine();
        }
        public bool HasData()
        {
            return (streamReader.EndOfStream == false);
        }
        public void FillData(string[,] data)
        {
            string[] s = null;
            for (int i = 0; i < data.GetLength(0); i++)
            {
                s = streamReader.ReadLine().Split(',');
                for (int j = 0; j < data.GetLength(1); j++)
                {
                    if (s[j] != "")
                        data[i, j] = s[j];
                }
            }
        }
        public int CountLines()
        {
            int i = 0;
            string s = null;
            while ((s = streamReader.ReadLine()) != null)
            {
                i++;
            }
            fsReader.Seek(0, SeekOrigin.Begin);
            streamReader.DiscardBufferedData();
            return i;
        }
        public int CountColumns()
        {
            string s = streamReader.ReadLine();
            string[] columns = s.Split(',');
            fsReader.Seek(0, SeekOrigin.Begin);
            streamReader.DiscardBufferedData();
            return columns.Length;
        }
        public void Close()
        {
            streamReader.Close();
            fsReader.Close();
        }
    }
}

  

TabTextFileReader

This is the second of the two implementation files that use IFileReader. The code is self-explanatory. It opens a text file and parses it out. In this case it is simply parsing a tab-delimited text file.

Figure 6: TabTextFileReader.cs - Reads and parses TabDelimitedData.txt, using tabs as the delimeter

 public class TabTextFileReader : IocForDummies.IFileReader
{
    FileStream fsReader = null;
    StreamReader streamReader = null;

    public TabTextFileReader(string path)
    {
        fsReader = File.Open(path, FileMode.Open);
        streamReader = new StreamReader(fsReader);

    }
    public void ReadLine(string s)
    {
        s = streamReader.ReadLine();
    }
    public bool HasData()
    {
        return (streamReader.EndOfStream == false);
    }
    public void FillData(string[,] data)
    {
        string[] s = null;
        for (int i = 0; i < data.GetLength(0); i++)
        {
            s = streamReader.ReadLine().Split(',');
            for (int j = 0; j < data.GetLength(1); j++)
            {
                if (s[j] != "")
                    data[i, j] = s[j];
            }
        }
    }
    public int CountLines()
    {
        int i = 0;
        string s = null;
        while ((s = streamReader.ReadLine()) != null)
        {
            i++;
        }
        fsReader.Seek(0, SeekOrigin.Begin);
        streamReader.DiscardBufferedData();
        return i;
    }
    public int CountColumns()
    {
        string s = streamReader.ReadLine();
        string[] columns = s.Split(',');
        fsReader.Seek(0, SeekOrigin.Begin);
        streamReader.DiscardBufferedData();
        return columns.Length;

    }
    public void Close()
    {
        streamReader.Close();
        fsReader.Close();
    }
}

The class in Figure 7 (TextFileLister) simply calls the code in the implementation files in Figures 5 and 6. The key point is IFileReader can contain either CommaTextFileReader or TabTextFileReader, depending on the assembler’s wishes. This is the key point in this post – that TextFileLister doesn’t determine the classes it will use.

Figure 7: TextFileLister.cs - The dependent object that uses the interface IFileReader. The TextFileLister will rely on CommaTextFileReader or TabTextFileReader, depending what the assembler (MutableContainer) decides

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace IocForDummies
{
    class TextFileLister
    {
        IFileReader reader = null;
        string[,] data = null;

        public TextFileLister(IFileReader fileReader)
        {
            reader = fileReader;
        }
        public void ReadData()
        {
            int lines = reader.CountLines();
            int columns = reader.CountColumns();
            data = new string[lines, columns];
            reader.FillData(data);
        }
        public void ShowData()
        {
            for (int i = 0; i < data.GetLength(0); i++) 
            {
                for (int j = 0; j < data.GetLength(1); j++)
                    Console.Write(string.Format("[{0}]", data[i, j]));
                Console.WriteLine("");
            }
        }
        ~TextFileLister()
        {
            reader.Close();

        }
    }
}

The Assembler – Where the magic happens

Figure 8 has some tricky code. The _typeToCreateCode field is a dictionary. The dictionary contains object creation code that is mapped to an object type. For example, we can ask the MutableContainer code to go and retrieve the previously added constructor code for any type. Figure 9 illustrates how we add entries to the Dictionary _typeToCreateCode field.

Using lamdas, it is very easy to pass in a type and a delegate. For example, the AddComponent(CreateCode createCode) gets called in Program.cs, where a type and some object construction code gets passed in.

The code in Figure 8 allows you to later retrieve an object type and it’s creation code at runtime, supporting the whole Dependency Injection paradigm. The MutableContainer code below is at the heart of the dependency injection capabilities.

Notice that AddComponent() will replace an existing object, not necessarily always add one. This is important because mutableContainer.AddComponent could either pass in CommaTextFileReader or TabTextFileReader, but not both.

Figure 8: MutableContainer.cs - The assembler which will build construct objects at runtime. In a nutshell, it can be used to use either CommaTextFileReader or TabTextFileReader

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace IocForDummies
{
    public class MutableContainer
    {
        public delegate object CreateCode();

        private readonly Dictionary<Type, CreateCode> _typeToCreateCode
                        = new Dictionary<Type, CreateCode>();

        public T Create<T>()
        {
            // Do a look up in the dictionary. Use the object type to do the lookup "typeof(T)".
            // The lookup will yield the object creation code.
            // Execute the object creation code.
            return (T)_typeToCreateCode[typeof(T)]();
        }
        internal void AddComponent<T>(CreateCode CreateCode)
        {
            // Remove previous entry, if it exists
            if (_typeToCreateCode.ContainsKey(typeof(T)))
                _typeToCreateCode.Remove(typeof(T));
            // Add the new entry
            _typeToCreateCode.Add(typeof(T), CreateCode);
        }
    }
}

Using MutableContainer

This is the code where the assembler is called to perform its work.

The key point of Dependency Injection is that the assembler determines how the TextFileLister gets constucted, whether TextFileLister uses CommaTextFileReader or TabTextFileReader. TextFileLister does not determine how it reads text files.

Figure 9: Program.cs – The main driver loop which illustrates our points

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace IocForDummies
{
    class Program
    {
        static void Main(string[] args)
        {
            string commaFileName = Environment.CurrentDirectory +
                                        @"..\..\..\" + @"CommaDelimitedData.txt";

            string tabFileName = Environment.CurrentDirectory +
                                        @"..\..\..\" + @"TabDelimitedData.txt";


            MutableContainer mutableContainer = new MutableContainer();
            
            mutableContainer.AddComponent<IFileReader>(() =>
            {
                // Embeded object is of type CommaTextFileReader
                IFileReader fileReader= new CommaTextFileReader(commaFileName);
                return fileReader;
            });

            mutableContainer.AddComponent<TextFileLister>(() =>
            {
                IFileReader fileReader = mutableContainer.Create<IFileReader>();
                return new TextFileLister(fileReader);
                
            });

            TextFileLister customCommaDataReader = mutableContainer.Create<TextFileLister>();
            customCommaDataReader.ReadData();
            customCommaDataReader.ShowData();

            // Now read a tab delimited file
            mutableContainer.AddComponent<IFileReader>(() =>
            {
                // Embeded object is of type TabTextFileReader
                IFileReader fileReader = new TabTextFileReader(tabFileName);
                return fileReader;
            });

            TextFileLister customTabDataReader = mutableContainer.Create<TextFileLister>();
            customTabDataReader.ReadData();
            customTabDataReader.ShowData();
        }
        }
}

Figure 10 – The text files

CommaDelimitedData.txt

row1col1,row1col2,row1col3

row2col1,row2col2,row2col3

row3col1,row3col2,row3col3

TabDelimitedData.txt

row1col1[tab]row1col2[tab]row1col3

row2col1[tab]row2col2[tab]row2col3

row3col1[tab]row3col2[tab]row3col3

* Note that [tab] is there instead of real tabs because they are invisible.

Conclusion

What I just described is Constructor Injection. There are 2 other types of injection, Setter Injection and Interface Injection. Martin Fowler describes the other forms of DI in at his blog.


Download for Azure SDK

Comments

  • Anonymous
    March 13, 2012
    The comment has been removed

  • Anonymous
    June 17, 2012
    Great site. A lot of useful information here. I’m sending it to some friends!

  • Anonymous
    December 16, 2012
    Indeed a good explanation. I also like example given in <a href="javarevisited.blogspot.com.au/.../inversion-of-control-dependency-injection-design-pattern-spring-example-tutorial.html">IOC and DI with real world example</a>

  • Anonymous
    December 28, 2012
    Great explanation. Thanks

  • Anonymous
    January 26, 2013
    OMG, don't use this in a real world program.  It wil slow your app by x4 and adds Code bloat to the application.  Plus make it harder to debug. The spirit of the article was exploratory. What this post investingates is the simple, principles that haven't been obscured by layers of error trapping, abstract code and the like. I like your OMG reaction - I hope you weren't debating it as your read the post :-)

    Bruno

  • Anonymous
    March 03, 2014
    I'm a complete newbie at C#/WPF/windows_programming_in_general so I may be completely wrong, but...isn't the .Split(',') call supposed to feature a tab (a 't' I guess) instead of a comma for the TabTextFileReader class? If one searches this page for .Split he/she will encounter it 4 times:

  • 2 times for CommaTextFileReader class
  • 2 times for TabTextFileReader class All 4 calls have a comma as parameter. I was expecting a comma in the first 2 instances and a tab in the other 2