Introduction
Definition: “Inversion of Control is an abstract principal describing an aspect of some software architecture design in which the flow of control
of a system is inverted in comparison to procedural programming.”
What happens in procedural programming is a chunk of code that uses or consumes another chunk of code is in control of
the process.
It knows what piece of code, which method, in which class, it uses and this way
it knows about the implementation details in the code it uses.
Example
Here is an example of such a scenario when a class X uses another class Y. The consumer class X needs to consume the class Y to accomplish
something. This is a very obvious example, but wait and think for a minute, does X really need to know that it uses Y? Isn’t it enough that X knows that
it uses something that has the behavior, the methods, the properties, etc., of Y without knowing who actually implements the behavior?
Extract the abstract
definition of the behavior used by X in Y and let the consumer X to use an instance of that behavior instead of Y. This way X does
the same thing without knowing the specifics about Y.
As illustrated above, class Y implements interface I and class X uses I. An interesting
thing is though class X still uses class Y through interface I,
X doesn’t know that it uses Y, it just knows that it uses something that implements I.
Right now, it’s Y, but it could be A, B, or C that implements interface I.
This way we are reducing the dependency of X over Y. Now you might have a question in your mind, what is dependency? And how is
it dangerous for software?
What is Dependency in terms of classes?
The example mentioned in the start of this article is an example of dependent classes. When class X uses a method of class Y or class Y as
a whole, we can say that
class X has some level of dependency over class Y. Dependency may extend to several levels. X uses Y, Y uses A and B, A uses C, and more. This way this chain
goes on and on. The problem with this is if we have any change in these classes, it may spawn to multiple classes.
The solution to this dependency problem is define a loose relation
between classes. One way to achieve this is the Inversion of Control pattern. This pattern uses Dependency Injection to eliminate tight coupling between objects.
Let’s see dependency with some practical example.
A sample scenario
We have a class called LoggerEngine
which is used for logging messages. It has a method called
Log
which receives a string object as an argument and logs it into
a file
using another class named FileLogger
.
The code for LoggerEngine
looks like this:
Notice the above code in which the Log
method creates an instance of
the FileLogger
class and then logs the message using its own Log
method. Here you may define all three methods
to be static or there might be a singleton instance ofthe FileLogger
class. What matters
is, the dependency of the LoggerEngine
class over the FileLogger
class.
We can solve this by creating an abstract definition of the behavior in FileLogger
that
LoggerEngine
needs in the form of an interface ILogger
and making
the Log
method
use an instance of ILogger
instead of an instance of FileLogger
.
Now LoggerEngine
no longer knows about the FileLogger
class. It just uses
an instance of the ILogger
interface of which FileLogger
is one of many possibly implementations.
Now in fact we could easily change it so that LoggerEngine
logs messages on
the console instead of a file without changing a single line of code in LoggerEngine
.
This all sounds good, but the main question is how does LoggerEngine
get
an instance of ILogger
? Here we need a way for LoggerEngine
to get an instance of
ILogger
at runtime without knowing about the concrete implementation. There are a couple of ways to do that, one of which is through Dependency Injection.
Dependency Injection
This is the basic concept for implementing the Inversion of Control (IoC) pattern.
- It eliminates tight coupling between objects.
- Makes objects and application more flexible.
- It facilitates creating more loosely coupled objects and their dependencies.
Constructor Injection
Here the object reference would get passed to the constructor of the business class
LoggerEngine
. In this case since the LoggerEngine
class depends on
the FileLogger
class,
a reference of the FileLogger
class will pass steps to implement Constructor Injection.
Step-1 Create an Interface:
namespace IoCSample
{
public interface ILogger
{
void OpenLog();
void CloseLog();
void Log(string message);
}
}
Only a method of the interface will be exposed in the business class.
Step-2 Implement an interface to the FileLogger
class.
namespace IoCSample
{
public class FileLogger : ILogger
{
StreamWriter streamWriter = null;
public string LogFileName { get; private set; }
public FileLogger(string logFileName) { LogFileName = logFileName; }
public FileLogger() : this(@"C:\Windows\Temp\AppLog.log") { }
public void OpenLog()
{
if (!Directory.Exists(Path.GetDirectoryName(LogFileName)))
Directory.CreateDirectory(Path.GetDirectoryName(LogFileName));
streamWriter = new StreamWriter(File.Open(LogFileName, FileMode.Append));
streamWriter.AutoFlush = true;
}
public void CloseLog()
{
if (streamWriter != null)
{
streamWriter.Close();
streamWriter.Dispose();
streamWriter = null;
}
}
public void Log(string message)
{
if (streamWriter != null)
{
streamWriter.WriteLine("------------------------------------------" +
"-----------------------------------------------");
streamWriter.WriteLine(string.Format("Log Message : {0}", message));
streamWriter.WriteLine(string.Format("Log Date : {0:dd}/{0:MM}/{0:yyyy}", DateTime.Now));
streamWriter.WriteLine(string.Format("Log Time : {0:hh}:{0:mm}:{0:ss} {0:tt}", DateTime.Now));
streamWriter.WriteLine("------------------------------------------" +
"-----------------------------------------------");
streamWriter.WriteLine();
}
}
}
}
An object of the FileLogger
class is going to be referenced by
the LoggerEngine
class. So this class needs to implement the interface.
Step-3 Make a reference of the interface in the LoggerEngine
class.
namespace IoCSample.Constructor_Dependency_Injection
{
public class LoggingEngine
{
private ILogger logger = null;
public LoggingEngine(ILogger logger)
{
this.logger = logger;
}
public void Log(string message)
{
logger.OpenLog();
logger.Log(message);
logger.CloseLog();
}
}
}
Step-4 Create a third party class, which creates an instance of all these objects.
namespace IoCSample
{
public class IoCClass
{
ILogger logger = null;
public string FileName { get; set; }
public void WriteLogUsingConstructorInjection(string message)
{
logger = new FileLogger(FileName);
Constructor_Dependency_Injection.LoggingEngine cEngine =
new Constructor_Dependency_Injection.LoggingEngine(logger);
cEngine.Log(message);
cEngine = null;
}
}
}
Step-5 Use this third party class at the client side.
namespace IoCSample
{
class Program
{
static void Main(string[] args)
{
string fileName = @"C:\Windows\Temp\AppLog.log";
IoCClass obj = new IoCClass() { FileName = fileName };
obj.WriteLogUsingConstructorInjection("Hi, hetu how r u?");
Console.Read();
}
}
}
Shortcoming of Constructor Injection
- In constructor injection, the business logic class doesn’t have a default constructor.
- Once the class is instantiated, object dependency cannot be changed.
Setter Injection
This uses properties to inject a dependency. Here rather than creating a reference and assigning
it in the constructor, it is done in properties.
This way the LoggingEngine
class could have a default constructor.
Step-1 Same as Constructor Injection
Step-2 Same as Constructor Injection
Step-3 LoggerEngine.cs
namespace IoCSample.Setter_Dependency_Injection
{
public class LoggerEngine
{
public ILogger Logger { get; set; }
public LoggerEngine() { }
public LoggerEngine(ILogger logger) { Logger = logger; }
public void Log(string message)
{
if (Logger != null)
{
Logger.OpenLog();
Logger.Log(message);
Logger.CloseLog();
}
}
}
}
In the LoggerEngine
class, there is a property logger, which sets and gets
the value of reference of the interface.
Step-4 There are some changes in the third party class:
namespace IoCSample
{
public class IoCClass
{
ILogger logger = null;
public string FileName { get; set; }
public void WriteLogUsingSetterInjection(string message)
{
logger = new FileLogger(FileName);
Setter_Dependency_Injection.LoggerEngine sEngine =
new Setter_Dependency_Injection.LoggerEngine();
sEngine.Logger = logger;
sEngine.Log(message);
sEngine = null;
}
}
}
Step-5 Same as Constructor Injection.
Advantages of Setter Injection
- It's more flexible than Constructor Injection
- Dependency of object can be changed without creating an instance
- Dependency of object can be changed without changing constructor
- Setters have constructive and self descriptive meaningful names that simplify understanding and using them
Interface Injection
In this, instance creation is transferred to an additional interface. This interface
is passed as an argument in the calling method.
Step-1 Same as Constructor Injection
Step-2 Same as Constructor Injection
Step-3 Create an interface and its implemented class, which creates an instance.
namespace IoCSample.Interface_Dependency_Injection
{
public interface ILoggerInject
{
ILogger Construct();
}
public class FileLoggerInject : ILoggerInject
{
public string FileName { get; set; }
public ILogger Construct()
{
return new FileLogger(FileName);
}
}
}
Here one more interface and its implemented class has been defined which are responsible for instance creation.
Step-4 LoggerEngine.cs
namespace IoCSample.Interface_Dependency_Injection
{
public class LoggerEngine
{
private ILogger logger;
public void Log(ILoggerInject loggerInject, string message)
{
logger = loggerInject.Construct();
logger.OpenLog();
logger.Log(message);
logger.CloseLog();
}
}
}
Step-5 Our third party class will be:
namespace IoCSample
{
public class IoCClass
{
ILogger logger = null;
public string FileName { get; set; }
public void WriteLogUsingInterfaceInjection(string message)
{
Interface_Dependency_Injection.LoggerEngine iEngine =
new Interface_Dependency_Injection.LoggerEngine();
Interface_Dependency_Injection.ILoggerInject loggerInject =
new Interface_Dependency_Injection.FileLoggerInject() { FileName = FileName };
iEngine.Log(loggerInject, message);
loggerInject = null;
iEngine = null;
}
}
}
Step-6 Same as Step-5 in Constructor Injection.
Service Locator Injection
Here a Service class is introduced which contains either a static method or a Service class may be used
as in the Singleton pattern.
Step-1 Same as Constructor Injection.
Step-2 Same as Constructor Injection.
Step-3 Define service class LoggerService
as follows:
namespace IoCSample.Service_Dependency_Injection
{
class LoggerService
{
public static ILogger Logger { private get; set; }
public static ILogger getLoggerService()
{
if (Logger == null)
Logger = new ConsoleLogger();
return Logger;
}
}
}
Here we have a static write-only property Logger
which is used for setting
the value of reference of the ILogger
instance and a static method
which is used
for getting the value of reference of the ILogger
instance.
Step-4 LoggerEngine.cs:
namespace IoCSample.Service_Dependency_Injection
{
public class LoggerEngine
{
private ILogger logger;
public void Log(string message)
{
logger = LoggerService.getLoggerService();
logger.OpenLog();
logger.Log(message);
logger.CloseLog();
}
}
}
Step-5 Our third party class will be:
namespace IoCSample
{
public class IoCClass
{
ILogger logger = null;
public string FileName { get; set; }
public void WriteLogUsingServiceInjection(string message)
{
Service_Dependency_Injection.LoggerEngine slEngine =
new Service_Dependency_Injection.LoggerEngine();
Service_Dependency_Injection.LoggerService.Logger = new FileLogger(FileName);
slEngine.Log(message);
slEngine = null;
}
}
}
Step-6 Same as Step-5 in Constructor Injection.
Generic-Type Injection
In this method, either we can define a Generic LoggerEngine
class or
a Generic method in the LoggerEngine
class. A Generic method would be more appropriate because
if we use a Generic class then it will cause the same problems which are identified in Constructor Injection.
Step-1 Same as Constructor Injection.
Step-2 Same as Constructor Injection.
Step-3 Let’s define the LoggerEngine
class which contains
a Generic method for writing logs:
namespace IoCSample.Generic_Dependency_Injection
{
public class LoggerEngine
{
public void Log<TLogger>(string message) where TLogger : ILogger, new()
{
ILogger logger = Activator.CreateInstance<TLogger>();
logger.OpenLog();
logger.Log(message);
logger.CloseLog();
}
}
}
Here we explicitly mentioned that our TLogger
would be a reference implementation of ILogger
.
Step-4 Our third party class will be:
namespace IoCSample
{
public class IoCClass
{
ILogger logger = null;
public string FileName { get; set; }
public void WriteLogUsingGenericTypeInjection(string message)
{
Generic_Dependency_Injection.LoggerEngine gEngine =
new Generic_Dependency_Injection.LoggerEngine();
gEngine.Log<FileLogger>(message);
gEngine = null;
}
}
}
Step-5 Same as Constructor Injection.
Benefits of Inversion of Controls
- We can reduce the dependency between objects and can design more flexible systems.
- We can isolate our code at the time of unit testing.