5
votes

I thought the whole reason for Interfaces, Polymorphism, and a pattern like Inversion of Control via Dependency Injection was to avoid tight coupling, separation, modularity, and so on.

Why then do I have to explicitly "wire up" an Interface to a concrete class like in ASP.NET? Won't me having the registry be some sort of coupling? Like,

services.AddTransient<ILogger, DatabaseLogger>();

What if I take a logger, ILogger, and create a file and database class that implement that interface.

In my IoC,

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

namespace ConsoleApp1
{
    public interface ILogger
    {
        void logThis(string message);

    }
    public class DatabaseLogger : ILogger
    {
        public void logThis(string message)
        {
            //INSERT INTO table.....
            System.Console.WriteLine("inserted into a databse table");
        }

    }
    public class FileLogger : ILogger
    {
        public void logThis(string message)
        {
            System.Console.WriteLine("logged via file");
        }

    }
    public class DemoClass
    {
        public ILogger myLogger;


        public DemoClass(ILogger myLogger)
        {
            this.myLogger = myLogger;
        }
        public void logThis(string message)
        {
            this.myLogger.logThis(message);
        }

    }
    class Program
    {

        static void Main(string[] args)
        {
            DemoClass myDemo = new DemoClass(new DatabaseLogger());  //Isn't this Dependency Injection?
            myDemo.logThis("this is a message");
            Console.ReadLine();

        }
    }
}

So why do I have to register or "wire up" anything? Isn't this Dependency Injection via the Constructor (Do I have fundamental misunderstanding)? I could put any logger in there that implemented ILogger.

Would I create two of these?

services.AddTransient<ILogger, DatabaseLogger>();
services.AddTransient<ILogger, FileLogger>();
2
Well unless the interface only has one implementation how is the program supposed to know which. You would need to key it to one of them or you can get unexpected behaviour.Alex Krupka
@AlexKrupka I am not sure I understand. I have two implementations. I only instantiated one of them.johnny
For DI to be really valuable (in my experience), you need to look at more complicated scenarios. For example, Company A and Company B both use your website, but Company A is Canadian and Company B is American. So you bind an implementation of tax calculation based on which company the user belongs to, rather than have one impl. that handles both with If statementsMatt R
@MattR I was starting to think the same. IoC may not be needed, even though one still uses the DI pattern.johnny

2 Answers

3
votes

You would actually abstract it a bit further. In your code, you would create a factory.

Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.

Example Factory Method

So you would in essence have the following:

// Interface:
public interface ILogger : IDisposable
{
     void Log<TEntity>(TEntity entity);
}

// Concrete Implementation:
public class DatabaseLogger : ILogger
{
     public void Log<TEntity>(TEntity entity)
     {
          throw new NotImplementedException();
     }
}

public class TextLogger : ILogger
{
     public void Log<TEntity>(TEntity entity)
     {
          throw new NotImplementedException();
     }
}

With the current way this is defined, you would receive the following IEnumerable<ILogger> when you wired both dependencies to your container. But, to correctly wrap this we would do the following:

public interface ILoggerFactory
{
     ILogger CreateDbLogger();

     ILogger CreateLogger();
}

public class LoggerFactory : ILoggerFactory
{
     public ILogger CreateDbLogger() => new DatabaseLogger();   

     public ILogger CreateLogger() =>   new TextLogger();
}

So when we register our Factory within our dependency injection, we would simply write services.AddTransient<ILoggerFactory, LoggerFactory>();. When you inject the factory, you would simply be able to use:

public class Example
{
     public ILoggerFactory Factory { get; }
     public Example(ILoggerFactory factory)
     {
          Factory = factory;
     }


     // Utilize in a method.
     using(var logger = Factory.CreateDbLogger())
          logger.Log(...);

     // Utilize in a method.
     using(var logger = Factory.CreateLogger())
          logger.Log(...);
}

Now, this can introduce over exposure. But hopefully this clarifies a usage with Dependency Injection which is utilized frequently. You can read a bit more on it by typing "Factory Pattern" or "Factory Method".

2
votes

Your example is Dependency Injection too, however, it's a very simple scenario. While you could do it manually, it gets out of hand on any sizable real-world scenario.

Imagine your DemoClass had 10 dependencies, and each of those dependencies had a few dependencies of their own, which can easily be the case in a real-world application. Now imagine instantiating all those manually to create your higher level service:

DemoClass myDemo = new DemoClass(new DatabaseLogger(new DbLoggerDependency1(), new DbLoggerDependency2(), new DbLoggerDependency3(), new DbLoggerDependency4()), new OtherDemoClassDependency(new OtherDependencyDependency1(), new OtherDependencyDependency2()));

And you have to do that everywhere you need your DemoClass. Gets ugly pretty fast right? and that's just a few dependencies.

Now imagine working on a test harness while developing your code-base, you have to do it all over again with mock versions whereas with wiring you can just introduce a new config for your test harness.

This wiring up lets you keep all the injection clean, separate and maintainable in a config file and lets you swap out implementations from a single point of control easily:

services.AddTransient<ILogger, DatabaseLogger>();
services.AddTransient<IDbLoggerDependency1, DbLoggerDependency1>();
services.AddTransient<IDbLoggerDependency2, DbLoggerDependency2>();
services.AddTransient<IDbLoggerDependency3, DbLoggerDependency3>();
services.AddTransient<IDbLoggerDependency4, DbLoggerDependency4>();
services.AddTransient<IOtherDemoClassDependency, OtherDemoClassDependency>();
services.AddTransient<IOtherDependencyDependency1, OtherDependencyDependency1>();
services.AddTransient<IOtherDependencyDependency2, OtherDependencyDependency2>();
services.AddTransient<IDemoClass, DemoClass>();

And then anywhere you need your DemoClass you can just let your IoC take care of instantiating and injecting the correct dependencies based on your config so you won't need to write all the boilerplate code:

class Foo
{
    private readonly IDemoClass _myDemo;

    public Foo(IDemoClass myDemo)
    {
        // myDemo is instantiated with the required dependencies based on your config
        _myDemo = myDemo;
    }

    public SomeMethod()
    {
        _myDemo.logThis("this is a message");
    }
}

Some containers like AutoFac even support property injection so you won't even need to include the constructor.