“One program, Four ways” – A beginner’s guide to abstraction and interfaces in C# (Part four)

Apr 30 2012

“One program, Four ways” – A beginner’s guide to abstraction and interfaces in C# (Part four)


This is going to be about interfaces isn’t it?

Heck yes!
Interfaces are awesome.

Lets take a look at an emerging pattern

If you have a look at the string processing methods from part 2 and the string processing classes from part 3, you will notice something. All of the classes are about one method, which takes in a string and it gives back a string, and if you think about it, they kind of do the same job, but with a different output. I.E. THEY ALL PROCESS STRINGS! and from the outside, don’t really appear to be doing anything different. So why should we treat them differently? Answer: We shouldn’t!

Let us unite all of our string processors under an interface

Have a look at the following interface:

//IStringProcessor.cs

namespace Way_04
{
    public interface IStringProcessor
    {
        string ProcessString(string input);
    }
}

So we have this interface, which is called IStringProcessor. It is time to bring all of our different string processing classes together, and give them one signature that works for all of them:

stringProcessString(string input)

Implementing the interface

Implementing this interface in each of our 3 string handling classes will require a tiny bit of modification. Have a look at what I have done:

//StringToUpperMaker.cs

namespace Way_04
{
    public class StringToUpperMaker : IStringProcessor
    {
        public string ProcessString(string input)
        {
            return input.ToUpper();
        }
    }
}
//StringReverser.cs

namespace Way_04
{
    public class StringReverser : IStringProcessor
    {
        public string ProcessString(string input)
        {
            var output = "";

            for (var index = input.Length - 1; index >= 0; index--)
            {
                output += input[index];
            }

            return output;
        }
    }
}
//StringVowelRemover.cs

namespace Way_04
{
    public class StringVowelRemover : IStringProcessor
    {
        public string ProcessString(string input)
        {
            var output = "";

            var vowels = new char[] { 'A', 'E', 'I', 'O', 'U', 'a', 'e', 'i', 'o', 'u' };
            foreach (var character in input)
            {
                var vowelFound = false;
                foreach (var vowel in vowels)
                {
                    if (character == vowel)
                    {
                        vowelFound = true;
                        break;
                    }
                }
                if (!vowelFound)
                {
                    output += character;
                }
            }

            return output;
        }
    }
}

And there we have it. All I have done is changed the signature of the method, and derived from the interface.

That looks great, but what good does it do?

Good question. By implementing the interface on each of our classes, we are giving the compiler a guarantee, and that guarantee goes like this “All of our classes that implement this interface will have a string processString(string input) method on them”

This is huge, because we can now store objects using variables that are typed to IStringProcessor. This means that we don’t need to make our code dependant on any of our concrete classes because we can just make it dependant on the interface instead. Take a look at this new class I have added:

//StringHandler.cs

using System;
using System.Collections.Generic;

namespace Way_04
{
    public class StringHandler
    {
        private readonly List<IStringProcessor> _processors;
        private readonly List<ConsoleColor> _colors;
        private readonly StringDisplayer _displayer;

        public StringHandler()
        {
            _processors = new List<IStringProcessor>();
            _colors = new List<ConsoleColor>();
            _displayer = new StringDisplayer();

        }

        public void AddProcessor(IStringProcessor processor, ConsoleColor color = ConsoleColor.Gray)
        {
            _processors.Add(processor);
            _colors.Add(color);
        }

        public void HandleString(string input)
        {
            for(var index = 0; index < _processors.Count; index++)
            {
                var processor = _processors[index];
                var color = _colors[index];
                _displayer.DisplayString(processor.ProcessString(input),color);
            }
        }

    }
}

Basically all this class does is store a list of stringProcessors and colors. And we can invoke the HandleString method and give it an input and when we do, it will go over each processor and display the result from each one.
Notice how there is no reference to any of our string processing classes in this code. The only thing that this class cares about is that we are using IStringProcessors which is a type that all of our processors can be thought of as. This class gives us the ability to add a processor, using the AddProcessor method, but it calls for an IStringProcessor.

How can we give it an IStringProcessor? You can’t instantiate interfaces…duh

Indeed not. What it wants us to do is give it an object that fulfils IStringProcessor and we have 3 such classes. Check out the new program class:

//Program.cs

using System;
using System.Collections.Generic;

namespace Way_04
{
    class Program
    {
        static void Main()
        {
            Console.Write("Please type some text: ");
            var input = Console.ReadLine();

            var handler = new StringHandler();

            handler.AddProcessor(new StringToUpperMaker());
            handler.AddProcessor(new StringReverser());
            handler.AddProcessor(new StringVowelRemover());

            handler.AddProcessor(new StringToUpperMaker(),ConsoleColor.Green);

            handler.HandleString(input);

            Console.ReadKey(true);

        }
    }
}

Now the fruits of our labour are beginning to ripen! Look at how much easier main is now. Sure it is still a bit of a middle-man, but the code is much clearer. Notice how on lines 17,18 and 19 the AddProcessor method happily accepts our concrete implementations of the IStringProcessor interface. The big advantage about that is we could have as many classes as we want that fulfil the IStringProcessor interface and the StringHandler class will automatically be compatible with them, with no modification. This means that we can continue expanding in a clean, none destructive way.

A task for the reader

I advise you to copy out this code and try this for yourself. Why not have a go at making your own string processor class. Here is how to do it:

  1. Make a new class, and implement the IStringProcessor interface
  2. Write the ProcessString method in your new class, so it fulfils the interface
  3. use the AddProcessor method of the handler object to add your processor to it, just like on lines 17, 18 and 19

Hang on a second buddy…You can’t combine processors in this version!

Okay, you got me. You can’t combine processors with this version, but if you are smart about your design, you will see that there is a way to combine the processors that is way more elegant than what we were doing before. We simply write a new type of string processor, I call it the StringProcessorChain. Behold! :

//StringProcessorChain.cs

using System.Collections.Generic;

namespace Way_04
{
    public class StringProcessorChain : IStringProcessor
    {
        private readonly List<IStringProcessor> _chain;

        public StringProcessorChain()
        {
            _chain = new List<IStringProcessor>();
        }

        public void AddProcessor(IStringProcessor processor)
        {
            _chain.Add(processor);
        }
        public string ProcessString(string input)
        {
            foreach(var processor in _chain)
            {
                input = processor.ProcessString(input);
            }

            return input;
        }
    }
}

Using this type of string processor, we can chain multiple processors together and when we call ProcessString, it will go through each processor, and update the input with each one, and then return back the final result. Look at the new main method which makes use of this:

//Program.cs

using System;
using System.Collections.Generic;

namespace Way_04
{
    class Program
    {
        static void Main()
        {
            Console.Write("Please type some text: ");
            var input = Console.ReadLine();

            var handler = new StringHandler();

            var chain = new StringProcessorChain();

            chain.AddProcessor(new StringReverser());
            chain.AddProcessor(new StringToUpperMaker());
            chain.AddProcessor(new StringVowelRemover());

            handler.AddProcessor(chain,ConsoleColor.Yellow);
            handler.HandleString(input);

            Console.ReadKey(true);

        }
    }
}

The beauty of the interface in this case, is that even our StringProcessChain itself is an IStringProcessor and can thus be used with the StringHandler! Even though it does a more advanced form of string processing, combining a whole bunch of processors, it still fulfils the interface and therefore it can be treated just like a StringReverser or a StringVowelRemover.

Summary

Thank you for reading through this tutorial. I do hope you found time to follow my examples and have a bit of a play with them to see what results you can get. If I was you, I would try and do something cool. Imagine if you had a type of string processor that encrypted the string (:OOO HAX!). I think that would be pretty cool.

Knowing how and when to refactor code and when to use interfaces is a huge part of object oriented design. I must stress though that it is not something that you will learn overnight and that it will take practice. Try and think of scenarios where the design would be bad, and then try and work out what you can do to make it better. Don’t be afraid to throw big chunks of code away if you think of a better way of doing it, it takes time to develop the intuition to solve and eventually avoid these problems.

If you have any feedback on my tutorial, please do not hesitate to contact me!

Kind regards,

Andrew Thompson 2012

Download the project files

The project files are available for download. They were produced in Microsoft Visual Studio 2010 Professional.

Download Sourcecode523 downloads

Leave a Reply