visit
Dependency Injection (DI), Inversion of Control (IoC), and IoC Containers are our friends, but like everything in life, if you abuse using them, you would get what you don’t ever wish for.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TaxesCalculator.Abstractions
{
public interface ILogger
{
void LogMessage(string message);
}
}
ILogger
.double void LogMessage(string message);
.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TaxesCalculator.Abstractions
{
public interface ITaxCalculator
{
double CalculateTaxPerMonth(double monthlyIncome);
}
}
ITaxCalculator
.double CalculateTaxPerMonth(double monthlyIncome);
.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TaxesCalculator.Abstractions;
namespace TaxesCalculator.Implementations.Loggers
{
public class ConsoleLogger : ILogger
{
public void LogMessage(string message)
{
Console.WriteLine(message);
}
}
}
ConsoleLogger
class implementing ILogger
.System.Console
class and uses it to write to the Console. This is not the perfect implementation but it would be enough for now in order to drive your focus on the current scope.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TaxesCalculator.Abstractions;
namespace TaxesCalculator.Implementations.TaxCalculators
{
public class IncomeTaxCalculator : ITaxCalculator
{
private readonly ILogger m_Logger;
public IncomeTaxCalculator(ILogger logger)
{
m_Logger = logger;
}
public double CalculateTaxPerMonth(double monthlyIncome)
{
// Do some interesting calculations
var tax = monthlyIncome * 0.5;
// Don't forget to log the message
m_Logger.LogMessage($"Calculated Income Tax per month for Monthly Income: {monthlyIncome} equals {tax}");
return tax;
}
}
}
IncomeTaxCalculator
class implementing ITaxCalculator
.ILogger
to be able to log some important messages about the calculations.ILogger
is injected into the constructor.CalculateTaxPerMonth
method implementation, we just do the calculations and log the message using the injected ILogger
.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TaxesCalculator.Abstractions;
namespace TaxesCalculator.Implementations.TaxCalculators
{
public class VatTaxCalculator : ITaxCalculator
{
private readonly ILogger m_Logger;
public VatTaxCalculator(ILogger logger)
{
m_Logger = logger;
}
public double CalculateTaxPerMonth(double monthlyIncome)
{
var tax = 0.0;
// Do some complex calculations on more than one step
// Step1
tax += monthlyIncome * 0.0012;
m_Logger.LogMessage($"VAT Calculation Step 1, Factor: {monthlyIncome * 0.0012}, Total: {tax}");
// Step2
tax += monthlyIncome * 0.003;
m_Logger.LogMessage($"VAT Calculation Step 2, Factor: {monthlyIncome * 0.003}, Total: {tax}");
// Step3
tax += monthlyIncome * 0.00005;
m_Logger.LogMessage($"VAT Calculation Step 3, Factor: {monthlyIncome * 0.00005}, Total: {tax}");
// Don't forget to log the final message
m_Logger.LogMessage($"Calculated Vat Tax per month for Monthly Income: {monthlyIncome} equals {tax}");
return tax;
}
}
}
VatTaxCalculator
class implementing ITaxCalculator
.ILogger
to be able to log some important messages about the calculations.ILogger
is injected into the constructor.CalculateTaxPerMonth
method implementation, we just do the calculations and log the message using the injected ILogger
.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Autofac;
using TaxesCalculator.Abstractions;
using TaxesCalculator.Implementations.Loggers;
using TaxesCalculator.Implementations.TaxCalculators;
namespace TaxesCalculator
{
class Program
{
private static IContainer Container;
static void Main(string[] args)
{
var builder = new ContainerBuilder();
builder.RegisterType<ConsoleLogger>().As<ILogger>();
builder.RegisterType<IncomeTaxCalculator>().As<ITaxCalculator>();
builder.RegisterType<VatTaxCalculator>().As<ITaxCalculator>();
Container = builder.Build();
using (var scope = Container.BeginLifetimeScope())
{
var logger = scope.Resolve<ILogger>();
var taxCalculators = scope.Resolve<IEnumerable<ITaxCalculator>>();
var monthlyIncome = 2000.0;
var totalTax = 0.0;
foreach (var taxCalculator in taxCalculators)
{
totalTax += taxCalculator.CalculateTaxPerMonth(monthlyIncome);
}
logger.LogMessage($"Total Tax for Monthly Income: {monthlyIncome} equals {totalTax}");
}
Console.ReadLine();
}
}
}
Program
class. It is the main entry point to the whole application, which is, by the way, a C# Console Application.Main
method, the first thing we are doing is that we are initializing the IoC container, defining our abstractions-implementations pairs, and creating our IoC container scope.ILogger
, and a list of all available ITaxCalculator
implementations.
What if…
We have a lot of “What ifs”, and don’t get me wrong, I understand that these were not a part of the requirements in the first place. So, I am not blaming you for not considering these into the design.
Yes, we know that a dependency is coupled with an implementation, not with an abstraction. But still, do you think that the implementation IncomeTaxCalculator
should be dependent on a logger?
I can understand that an SQLDatabaseRepository
class implementation, which implements IRepository
interface, would -by definition- depend on some module that opens and closes an SQL database connection. This is something that you can easily say with full trust.
However, you can’t easily say, with the same level of trust, that the same SQLDatabaseRepository
class implementation depends on a logger module, right?
namespace TaxesCalculator.Abstractions
{
public delegate void TaxCalculationReportReadyEventHandler(object sender, string report);
public interface ITaxCalculator
{
event TaxCalculationReportReadyEventHandler TaxCalculationReportReady;
double CalculateTaxPerMonth(double monthlyIncome);
}
}
ITaxCalculator
interface.TaxCalculationReportReadyEventHandler
.TaxCalculationReportReadyEventHandler
.
using TaxesCalculator.Abstractions;
namespace TaxesCalculator.Implementations.TaxCalculators
{
public abstract class TaxCalculatorBase : ITaxCalculator
{
public event TaxCalculationReportReadyEventHandler TaxCalculationReportReady;
public abstract double CalculateTaxPerMonth(double monthlyIncome);
protected void OnTaxCalculationReportReady(string report)
{
TaxCalculationReportReady?.Invoke(this, report);
}
}
}
TaxCalculatorBase
for all ITaxCalculators
implementations.OnTaxCalculationReportReady
method which is responsible for internal triggering the TaxCalculationReportReady
event. This is one of the best practices advised by Microsoft.
namespace TaxesCalculator.Implementations.TaxCalculators
{
public class IncomeTaxCalculator : TaxCalculatorBase
{
public override double CalculateTaxPerMonth(double monthlyIncome)
{
// Do some interesting calculations
var tax = monthlyIncome * 0.5;
// Don't forget to report
OnTaxCalculationReportReady(
$"Calculated Income Tax per month for Monthly Income: {monthlyIncome} equals {tax}");
return tax;
}
}
}
IncomeTaxCalculator
class extends the TaxCalculatorBase
class instead of the ITaxCalculator
interface.ILogger
interface as it used to be in the old implementation.TaxCalculationReportReady
event instead of directly using an instance of the ILogger
interface.
namespace TaxesCalculator.Implementations.TaxCalculators
{
public class VatTaxCalculator : TaxCalculatorBase
{
public override double CalculateTaxPerMonth(double monthlyIncome)
{
var tax = 0.0;
// Do some complex calculations on more than one step
// Step1
tax += monthlyIncome * 0.0012;
OnTaxCalculationReportReady($"VAT Calculation Step 1, Factor: {monthlyIncome * 0.0012}, Total: {tax}");
// Step2
tax += monthlyIncome * 0.003;
OnTaxCalculationReportReady($"VAT Calculation Step 2, Factor: {monthlyIncome * 0.003}, Total: {tax}");
// Step3
tax += monthlyIncome * 0.00005;
OnTaxCalculationReportReady($"VAT Calculation Step 3, Factor: {monthlyIncome * 0.00005}, Total: {tax}");
// Don't forget to log the final message
OnTaxCalculationReportReady(
$"Calculated Vat Tax per month for Monthly Income: {monthlyIncome} equals {tax}");
return tax;
}
}
}
The same kind of changes as in IncomeTaxCalculator
class.
using System;
using System.Collections.Generic;
using Autofac;
using TaxesCalculator.Abstractions;
using TaxesCalculator.Implementations.Loggers;
using TaxesCalculator.Implementations.TaxCalculators;
namespace TaxesCalculator
{
class Program
{
private static ILogger Logger;
private static IContainer Container;
static void Main(string[] args)
{
var builder = new ContainerBuilder();
builder.RegisterType<ConsoleLogger>().As<ILogger>();
builder.RegisterType<IncomeTaxCalculator>().As<ITaxCalculator>();
builder.RegisterType<VatTaxCalculator>().As<ITaxCalculator>();
Container = builder.Build();
using (var scope = Container.BeginLifetimeScope())
{
Logger = scope.Resolve<ILogger>();
var taxCalculators = scope.Resolve<IEnumerable<ITaxCalculator>>();
var monthlyIncome = 2000.0;
var totalTax = 0.0;
foreach (var taxCalculator in taxCalculators)
{
taxCalculator.TaxCalculationReportReady += (sender, report) => LogTaxReport(report);
totalTax += taxCalculator.CalculateTaxPerMonth(monthlyIncome);
}
Logger.LogMessage($"Total Tax for Monthly Income: {monthlyIncome} equals {totalTax}");
}
Console.ReadLine();
}
private static void LogTaxReport(string report)
{
Logger.LogMessage(report);
}
}
}
TaxCalculationReportReady
event for each ITaxCalculator
interface implementation.LogTaxReport
method.