C# Interfaces Extensibility


This entry is part 4 of 8 in the series C# Interfaces

Let’s use an example to illustrate how interfaces can be used in our own code. We have a class that is responsible for migrating databases called DbMigrator. It needs to log its activities somewhere. We decide to create a Logger class to do this job. We decide that we will log all of the activities to the console. Later on, we may decide to log everything to a database or a text file, but for now we will use the console. We want to set this up so that the application is loosely coupled so that when we do decide to make changes, it will work.

We could write logging code right inside DbMigrator. However, we know that we will have many classes that will require logging. We don’t want to violate the Don’t Repeat Yourself (DRY) principle.

This example is from Mosh Hamedani‘s course at Udemy.com. It’s from his C# Intermediate course, section 6 on Interfaces, in the video called Interfaces and Extensibility.

Make DbMigrator Extensible

To accomplish this we need to create an interface. We will call it ILogger. After that, we need to have DbMigrator get an ILogger interface. How do we do that? In DbMigrator we need to create a constructor and inject that interface into the constructor. We can use the code snippet ctor.

Dependency Injection

This technique is called dependency injection, which means that in the constructor we are specifying the dependencies of our DbMigrator class.

Below is our full code. In the next post we will work backwards and show how we got to this point.

using System;
namespace InterfacesExtensibility
{
    public class ConsoleLogger : ILogger
    {
        public void LogError(string message) {
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine(message);
        }
        public void LogInfo(string message) {
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine(message);
        }
    }
    public interface ILogger
    {
        void LogError(string message);  // method
        void LogInfo(string message);  // method
    }
    public class DbMigrator
    {
        private readonly ILogger _logger;
        public DbMigrator(ILogger logger) {
            _logger = logger;
        }
        public void Migrate() {
            _logger.LogInfo($"Migrating started at {DateTime.Now}");
            _logger.LogInfo($"Migrating ended at {DateTime.Now}");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            var dbMigrator = new DbMigrator(new ConsoleLogger());
            dbMigrator.Migrate();
        }
    }
}

The best way to read and understand the above code listing is from the bottom up, except for the Main program code, which was written last.

DbMigrator does not care who Logger is as long as he/she can do the job. Also, the name is not important.

New Scenario – File Logger

Noe let’s suppose we don’t want to log to the console anymore. Instead of that we want to output a file to the C: drive in the temp directory. We’ll call it C:\temp.log.txt. How do we do that? DbMigrator needs a logger in the constructor but it really don’t care where it comes from. It doesn’t care what concrete class it comes from and it doesn’t care. Below we have added the class FileLogger that implements ILogger.

Final Code

using System;
using System.IO;
    public class FileLogger : ILogger
    {
        private readonly string _path;
        public FileLogger(string path)
        {
            _path = path;
        }
        public void LogError(string message)
        {
            Log(message, "ERROR");
        }
        public void LogInfo(string message)
        {
            Log(message, "INFO");
        }
        private void Log(string message, string messageType)
        {
            using (var streamWriter = new StreamWriter(_path, true))
            {
                streamWriter.WriteLine(messageType + ": " + message);
            }
        }
    }
    public class ConsoleLogger : ILogger
    {
        public void LogError(string message) {
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine(message);
        }
        public void LogInfo(string message) {
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine(message);
        }
    }
    public interface ILogger
    {
        void LogError(string message);  // method
        void LogInfo(string message);  // method
    }
    public class DbMigrator
    {
        private readonly ILogger _logger;
        public DbMigrator(ILogger logger) {
            _logger = logger;
        }
        public void Migrate() {
            _logger.LogInfo($"Migrating started at {DateTime.Now}");
            _logger.LogInfo($"Migrating ended at {DateTime.Now}");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            var dbMigrator = new DbMigrator(new FileLogger("D:\\temp\\log.txt"));
            dbMigrator.Migrate();
        }
    }
}

Open Closed Principle (OCP)

In object-oriented programming, the open/closed principle states “software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification”.

Series Navigation<< C# Interfaces TestabilityC# Interfaces Extensibility 2 >>