Saturday 21 May 2016

Dependency Inversion Principle: Part 4 - Refactoring Using DIP

You have known the basic concepts of DIP, now it is time to apply it to the code. If the code is written without DIP principles, this means the code needs a refactoring by abstracting and inverting the dependencies. The code will normally have many interactions between classes. This mean that a class may have dependencies, but in turn the class will be the dependency of other class.

Refactoring the code using DIP will eventually create a tree structure which represents the class composition. Normally there will be one composition root, which is the class at the top of the tree.

Let us start with an example of the code which is not adhere to DIP.
internal class Program
    {
        static void Main(string[] args)
        {
            var webAnalytic = new WebAnalytic();
            webAnalytic.Run();
        }
    }

    internal class WebAnalytic
    {        
        public void Run()
        {
            string webAddress = "http://www.google.com";
            FileLogger.Instance.WriteLine(string.Format("Web text from {0}", webAddress));

            using (var client = new HttpClient())
            {
                string webText = client.GetStringAsync(webAddress).Result;

                var textScanner = new TextScanner();
                var reportText = textScanner.Scan(webText);

                Console.Write(string.Format("Result of scanning web text from {0}\n{1}", webAddress, reportText));
            }
            Console.ReadKey();
        }
    }

    internal class FileLogger
    {
        private string _filePath = @"D:\log.txt";

        private static FileLogger _instance;
        public static FileLogger Instance
        {
            get
            {
                return _instance ?? (_instance = new FileLogger());
            }
        }

        private FileLogger()
        {
        }

        public void WriteLine(string text)
        {
            using (var stream = File.AppendText(_filePath))
            {
                stream.Write(text + Environment.NewLine);
            }
        }
    }

    internal class TextScanner
    {
        public string Scan(string text)
        {
            var reportText = string.Format("Number of characters scanned: {0}", text.Length);
            //can be extended with more analysis
            FileLogger.Instance.WriteLine(reportText);
            return reportText;
        }
    }

The code download a text from a website and perform some analysis, for the simplicity just counting the word. There is also a logging feature. For the sake of learning, I will refactor the code step by step.
  1. Inverting Dependencies
  2. Abstracting Remaining Dependencies
  3. Configure Class Composition
You can follow the steps for initial learning, but once you master it, you can just do it one go.

1. Inverting Dependencies

The first task is inverting dependencies. The candidates are:
  • any new operator, or any instantiation
  • singleton or any global states
  • external functionalities or long running methods
Any new operator, or any instantiation
Any new operator is a candidate for inversion, even though not all of them. Classes which are used for storing data, entity or model classes, and normally the return type are exception.

There is one class can be inverted in WebAnalytic, which is TextScanner.
  ..  
  internal class WebAnalytic
  {
    private TextScanner _textScanner;
    public WebAnalytic(TextScanner textScanner)
    {
       _textScanner = textScanner;
    }
    ...
  }
  .. 
Singleton or global states
Additionally, every singleton or global state pattern need to be converted to explicit dependency injection instead. In the code, there are FileLogger singletons in both WebAnalytic and TextScanner. So in those classes, FileLogger need to be injected instead.
  ..  
  internal class WebAnalytic
  {
    private FileLogger _fileLogger;
    private TextScanner _textScanner;
    public WebAnalytic(FileLogger fileLogger, TextScanner textScanner)
    {
      _fileLogger = fileLogger;
      _textScanner = textScanner;
    }
    ...
  }
  .. 
  internal class TextScanner
  {
    private FileLogger _fileLogger;
    public TextScanner(FileLogger fileLogger)
    {
      _fileLogger = fileLogger;
    }
  }  
External functionalities or long running methods
Moreover, if below functionalities are found in any classes, they need to be abstracted and inverted, so the remaining functionality can be unit tested.
  • database access
  • network access
  • disk operation, eg. file read/write operation
  • display operation
  • any long running process
  • inter process communication
  • any other I/O operations
The functionality can be abstracted as interface, and the existing code will go to the implementation of the interface. Here is the example:
public class ComplexClass
{

  public void Process(string path)
  {
     ...
     string text = File.ReadAll(path);
     ...
  }  
  .... 
}
Since, the functionality is a disk operation, it need to be abstracted (e.g. interface) and inverted (e.g. dependency injection via constructor)
public interface IReader
{
  string Read(path);
}

public FileReader : IReader
{
  public string Read(string path)
  {
     return File.ReadAll(path);      
  }
}

public class ComplexClass
{
  private IReader _reader;   
  public CompleClass(IReader reader)
  {
    _reader = reader;
  }  

  public void Process(string path)
  {
     ...
     string text = reader.Read(path);
     ...
  }  
  ....
}
In above example, the functionality is abstracted using IReader interface, and the dependency, FileReader, is injected via ComplexClass constructor.

In the scenario where only function or method needs abstraction, you can use a generic delegate, Func<T> or Action instead of using interface.

Back to the code, Console class provides a display functionality, and this is abstracted using IConsole and implemented by a ConsoleTerminal class. The ConsoleTerminal then need to be injected via WebAnalytic constructor.
 internal interface IConsole
    {
        void Write(string text);
        void WaitKey();
    }

    internal class ConsoleTerminal : IConsole
    {
        void Write(string text)
        {
            Console.Write(text);
        }

        void WaitKey()
        {
            Console.ReadKey();
        }
    }
Also, HttpClient provides a network access, and this need to be abstracted and inverted. This will be abstracted using IWebText and implemented by WebText and injected via WebAnalytic constructor. The web address is passed to WebText as a constructor parameter.
    internal interface IWebText
    {
        string WebAddress { get; }
        string GetText();
    }

    internal class WebText : IWebText
    {
        public string WebAddress { get; private set; }
        public WebText(string webAddress)
        {
            WebAddress = webAddress;
        }

        public string GetText()
        {
            using (var client = new HttpClient())
            {
                return client.GetStringAsync(WebAddress).Result;
            }
        }
    }
Finally, there is also a file write operation on FileLogger, which needs an abstraction and inversion of control. This will be abstracted using IWriterStream and implemented by FileWriterStream, and injected via FileLogger constructor.
 internal interface IWriterStream
    {
        StreamWriter OpenStream();
    }

    internal class FileWriterStream : IWriterStream
    {
        private string _filePath;
        public FileWriterStream(string filePath)
        {
            _filePath = filePath;
        }

        StreamWriter OpenStream()
        {
            return File.AppendText(_filePath);
        }
    }
All above new classes are not unit-testable. They are just introduced to isolate external dependencies on the main classes. However, now the main classes, WebAnalytic and FileLogger are unit-testable. Those main classes constructor need a revision to accept the new dependencies. Here is the complete code after refactoring everything above.
internal class Program
    {
        static void Main(string[] args)
        {
            var webAnalytic = new WebAnalytic();
            webAnalytic.Run();
        }
    }

    internal class WebAnalytic
    {
        private FileLogger _fileLogger;
        private TextScanner _textScanner;
        private IConsole _console;
        private IWebText _webText;

        public WebAnalytic()
            : this(new ConsoleTerminal(), new WebText("http://www.google.com"), new FileLogger(), new TextScanner())
        {
        }

        public WebAnalytic(IConsole console, IWebText webText, FileLogger fileLogger, TextScanner textScanner)
        {
            _console = console;
            _webText = webText;
            _fileLogger = fileLogger;            
            _textScanner = textScanner;
        }

        public void Run()
        {
            _fileLogger.WriteLine(string.Format("Web text from {0}", _webText.WebAddress));
            string webText = _webText.GetText();
            var reportText = _textScanner.Scan(webText);

            _console.Write(string.Format("Result of scanning web text from {0}\n{1}", _webText, reportText));
            _console.WaitKey();
        }
    }

    internal interface IWebText
    {
        string WebAddress { get; }
        string GetText();
    }

    internal class WebText : IWebText
    {
        public string WebAddress { get; private set; }
        public WebText(string webAddress)
        {
            WebAddress = webAddress;
        }

        public string GetText()
        {
            using (var client = new HttpClient())
            {
                return client.GetStringAsync(WebAddress).Result;
            }
        }
    }

    internal interface IConsole
    {
        void Write(string text);
        void WaitKey();
    }

    internal class ConsoleTerminal : IConsole
    {
        void Write(string text)
        {
            Console.Write(text);
        }

        void WaitKey()
        {
            Console.ReadKey();
        }
    }

    internal interface IWriterStream
    {
        StreamWriter OpenStream();
    }

    internal class FileWriterStream : IWriterStream
    {
        private string _filePath;
        public FileWriterStream(string filePath)
        {
            _filePath = filePath;
        }

        StreamWriter OpenStream()
        {
            return File.AppendText(_filePath);
        }
    }

    internal class FileLogger
    {
        private IWriterStream _writerStream;

        public FileLogger()
            : this(new FileWriterStream(@"D:\log.txt"))
        {
        }

        public FileLogger(IWriterStream writerStream)
        {
            _writerStream = writerStream;
        }

        public void WriteLine(string text)
        {
            using (var stream = _writerStream.OpenStream())
            {
                stream.Write(text + Environment.NewLine);
            }
        }
    }

    internal class TextScanner
    {
        private FileLogger _fileLogger;

        public TextScanner()
            : this(new FileLogger())
        {
        }

        public TextScanner(FileLogger fileLogger)
        {
            _fileLogger = fileLogger;
        }

        public string Scan(string text)
        {
            var reportText = string.Format("Number of characters scanned: {0}", text.Length);
            //can be extended with more analysis
            _fileLogger.WriteLine(reportText);
            return reportText;
        }
    }
One thing to note in the code, every classes have two constructors, one with dependencies, and another one is a parameterless constructor. The later one is used to inject dependencies into a class. This is not an ideal solution, as the class will depend on the implementation, and not just depends on the abstraction. Some developers will felt suffice at this stage, as you can write unit tests for the classes, by using the constructor with dependencies, so the code is fully testable. However, in this discussion this pattern will be used as prelude to class composition introduced shortly.

2. Abstracting Remaining Dependencies

All dependencies are now inverted, but not all of them are abstracted. If they are not abstracted, the code will be glued to the implementation. In the code, WebAnalytic still has dependencies on FileLogger and WebScanner. ILogger and IScanner will be introduced to abstract the classes respectively.
 internal class Program
    {
        static void Main(string[] args)
        {
            var webAnalytic = new WebAnalytic();
            webAnalytic.Run();
        }
    }

    internal class WebAnalytic
    {
        private ILogger _logger;
        private IScanner _scanner;
        private IConsole _console;
        private IWebText _webText;

        public WebAnalytic()
            : this(new ConsoleTerminal(), new WebText("http://www.google.com"), new FileLogger(), new TextScanner())
        {
        }

        public WebAnalytic(IConsole console, IWebText webText, ILogger logger, IScanner scanner)
        {
            _console = console;
            _logger = logger;
            _webText = webText;
            _scanner = scanner;
        }

        public void Run()
        {
            _logger.WriteLine(string.Format("Web text from {0}", _webText.WebAddress));
            string webText = _webText.GetText();
            var reportText = _scanner.Scan(webText);

            _console.Write(string.Format("Result of scanning web text from {0}\n{1}", _webText, reportText));
            _console.WaitKey();
        }
    }

    internal interface ILogger
    {
        string WriteLine(string text);
    }

    internal interface IScanner
    {
        string Scan(string text);
    }

    internal interface IWebText
    {
        string WebAddress { get; }
        string GetText();
    }

    internal class WebText : IWebText
    {
        public string WebAddress { get; private set; }
        public WebText(string webAddress)
        {
            WebAddress = webAddress;
        }

        public string GetText()
        {
            using (var client = new HttpClient())
            {
                return client.GetStringAsync(WebAddress).Result;
            }
        }
    }

    internal interface IConsole
    {
        void Write(string text);
        void WaitKey();
    }

    internal class ConsoleTerminal : IConsole
    {
        void Write(string text)
        {
            Console.Write(text);
        }

        void WaitKey()
        {
            Console.ReadKey();
        }
    }

    internal interface IWriterStream
    {
        StreamWriter OpenStream();
    }

    internal class FileWriterStream : IWriterStream
    {
        private string _filePath;
        public FileWriterStream(string filePath)
        {
            _filePath = filePath;
        }

        StreamWriter OpenStream()
        {
            return File.AppendText(_filePath);
        }
    }

    internal class FileLogger: ILogger
    {
        private IWriterStream _writerStream;

        public FileLogger(): this(new FileWriterStream(@"D:\log.txt"))
        {
        }

        public FileLogger(IWriterStream writerStream)
        {
            _writerStream = writerStream;
        }

        public void WriteLine(string text)
        {
            using (var stream = _writerStream.OpenStream())
            {
                stream.Write(text + Environment.NewLine);
            }
        }
    }

    internal class TextScanner: IScanner
    {
        private ILogger _logger;

        public TextScanner()
            : this(new FileLogger())
        {
        }

        public TextScanner(ILogger logger)
        {
            _logger = logger;
        }

        public string Scan(string text)
        {
            var reportText = string.Format("Number of characters scanned: {0}", text.Length);
            //can be extended with more analysis
            _logger.WriteLine(reportText);
            return reportText;
        }
    }
In some other code base you may want to split some functionalities into their own classes, aligned with Single Responsibility principles. You can abstract the functionalities into an interface, and move the codes into an implementation class, and inject the dependency via main class's constructor. This makes the code more manageable and the new classes can be unit-tested separately from the main class.

3. Configure Class Composition

In the code, most classes have two constructors. The parameterless contructors are used to specify the implementation of the dependencies, and this basically makes the classes still glue to the implementation and not the abstraction. In order to fully comply with dependency inversion principles, the next refactoring step is removing the parameterless constructors, and configuring class composition instead.

Below is the structure of the class composition.
WebText and FileStreamWriter have dependencies on webAddress and filePath, which are of string type. FileLogger depends on IWriterStream, which is implemented by FileStreamWriter. TextScanner depends on ILogger, which is implemented by FileLogger. Finally, the composition root, WebAnalytic, depends on ILogger, IScanner, IWebText and IConsole and are implemented by FileLogger, TextScanner, WebText, and ConsoleTerminal respectively.

In the root application , I create a private method that return the composition root class, and the whole class composition can be performed inside this method. The method is then called from a main program. Alternatively, you can do all above just in a main program. Here is the complete code after refactoring:
 internal class Program
    {
        static void Main(string[] args)
        {
            WebAnalytic webAnalytic = CreateCompositionRoot();
            webAnalytic.Run();
        }

        internal static WebAnalytic CreateCompositionRoot()
        {
            string logFilePath = @"D:\log.txt";
            string webAddress = @"http://www.google.com";

            IWriterStream writerStream = new FileWriterStream(logFilePath);
            IWebText webText = new WebText(webAddress);
            IConsole console = new ConsoleTerminal();
            ILogger logger = new FileLogger(writerStream);                        
            IScanner scanner = new TextScanner(logger);

            return new WebAnalytic(console, webText, logger, scanner);
        }
    }

    internal class WebAnalytic
    {
        private ILogger _logger;
        private IScanner _scanner;
        private IConsole _console;
        private IWebText _webText;

        public WebAnalytic(IConsole console, IWebText webText, ILogger logger, IScanner scanner)
        {
            _console = console;
            _logger = logger;
            _webText = webText;
            _scanner = scanner;
        }

        public void Run()
        {
            _logger.WriteLine(string.Format("Web text from {0}", _webText.WebAddress));
            string webText = _webText.GetText();
            var reportText = _scanner.Scan(webText);

            _console.Write(string.Format("Result of scanning web text from {0}\n{1}", _webText, reportText));
            _console.WaitKey();
        }
    }

    internal interface ILogger
    {
        string WriteLine(string text);
    }

    internal interface IScanner
    {
        string Scan(string text);
    }

    internal interface IWebText
    {
        string WebAddress { get; }
        string GetText();
    }

    internal class WebText : IWebText
    {
        public string WebAddress { get; private set; }
        public WebText(string webAddress)
        {
            WebAddress = webAddress;
        }

        public string GetText()
        {
            using (var client = new HttpClient())
            {
                return client.GetStringAsync(WebAddress).Result;
            }
        }
    }

    internal interface IConsole
    {
        void Write(string text);
        void WaitKey();
    }

    internal class ConsoleTerminal : IConsole
    {
        void Write(string text)
        {
            Console.Write(text);
        }

        void WaitKey()
        {
            Console.ReadKey();
        }
    }

    internal interface IWriterStream
    {
        StreamWriter OpenStream();
    }

    internal class FileWriterStream : IWriterStream
    {
        private string _filePath;
        public FileWriterStream(string filePath)
        {
            _filePath = filePath;
        }

        StreamWriter OpenStream()
        {
            return File.AppendText(_filePath);
        }
    }

    internal class FileLogger : ILogger
    {
        private IWriterStream _writerStream;

        public FileLogger(IWriterStream writerStream)
        {
            _writerStream = writerStream;
        }

        public void WriteLine(string text)
        {
            using (var stream = _writerStream.OpenStream())
            {
                stream.Write(text + Environment.NewLine);
            }
        }
    }

    internal class TextScanner : IScanner
    {
        private ILogger _logger;

        public TextScanner(ILogger logger)
        {
            _logger = logger;
        }

        public string Scan(string text)
        {
            var reportText = string.Format("Number of characters scanned: {0}", text.Length);
            //can be extended with more analysis
            _logger.WriteLine(reportText);
            return reportText;
        }
    }
Most application will only need one class composition, however it is quite possible in some applications you may want to create multiple class compositions.

All code from this section, and also next section, can be downloaded from Github. I almost finish here. However, the discussion is not complete without explaining the role of DIP/IoC container in the refactoring process. I will discuss this topic in the next section.

No comments:

Post a Comment