Freigeben über


Applications As Composition Of Services

After watching a Tech.Ed session by Juval Lowy and working with a variety of workflow-based applications (MSBuild build processes, Workflow Foundation build processes, etc.) I’ve come to realize that there is significant value to be gained by thinking of all applications as a series of services that are orchestrated together. Whenever I discuss this concept with people their first thought is web services, SOA, and ESBs but I’m talking about services at a much more fundamental concept of any class that provides a service. To demonstrate this the example I’ll walkthrough is comprised entirely of local services without a web service in sight.

Lesson #1: Services != Web Services != SOA != ESB

One of the fundamental concepts to building applications as a composition of services is that of interface-oriented programming and one of the most talked about advantages of this is increasing testability by enabling inversion of control (IoC) and mocking. However, this style of programming also encourages building classes that follow the single responsibility principle (SRP) and do one thing and do it well.

Lesson #2: Interface-Oriented Programming isn’t just about testability, it encourages well-architected applications that follow the single responsibility principle

So let’s take a look at an example, an implementation of the utility “which”. Which searches your path for each of the executables you pass to it and outputs the location they’re found in. For example:

C:\>which ipconfig.exe powershell.exe
C:\Windows\system32\ipconfig.exe
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe

Our implementation of this utility is pretty trivial as you can see here:

using System;

using System.IO;

namespace which

{

    class Program

    {

        static void Main(string[] args)

        {

            string[] paths = Environment.GetEnvironmentVariable("PATH").Split(';');

            foreach (string executable in args)

            {

                foreach (string path in paths)

                {

                    string candidatePath = Path.Combine(path, executable);

                    if (File.Exists(candidatePath))

                    {

                        Console.WriteLine(candidatePath);

                        break;

                    }

                }

            }

        }

    }

}

I want you to look at the implementation of this utility for a couple of minutes and think about how you would test it. I’ll wait…

Lesson #3: Just because an application is small doesn’t mean it’s easy to test

I chose this particular example because it demonstrates that it’s not just large applications that are difficult to test. To test this utility you would need to be able to control the environment that it runs in (so you could provide controlled values for the PATH environment variables) as well as be able to control the return value from the File.Exists API call. To make things worse the output from the utility is to the console so you’d need to be able to redirect this to be able to check that the utility is returning the correct result.

So what exactly is it that makes something hard to test? In this example it is the number of dependencies that either cannot be controlled or don’t have deterministic values. In our case it is the Environment.GetEnvironmentVariable, File.Exists, and Console.WriteLine APIs that make this utility hard to test. These aren’t the only API calls, there is another, Path.Combine, but it doesn’t make the utility harder to test because it is deterministic, given two inputs we can predict the output and it isn’t influenced by outside factors that we can’t control.

Lesson #4: The testability of an application decreases as the number of non-deterministic or environment-dependent dependencies increases

There’s one other interesting thing about this example and that is that these dependencies appear innocuous because they’re part of the .NET Framework itself. Some dependencies are obvious (such as a database library, an email library, or a logging library) but some are less obvious and these can have just as big an impact on the testability of your application.

Lesson #5: Beware of hidden dependencies

So how do we solve this problem? The approach that I find myself using more and more often is to treat everything outside of the application that is non-deterministic or environment-dependent as a service and wrap it in an interface and a class that implements this interface. This approach allows me to easily mock out those services for testing and it also makes maintenance easier because the dependencies are isolated into classes separate from the application’s logic. If you’re application is large enough or quite modular you might extend this technique to formalize communication between different parts of your application.

In the amended example I’ve taken each of the dependencies and turned it into a service by creating my own interface and class that wraps the dependency. This gives me control over the dependency and allows me to mock it out for testing, easily swap it for a different implementation, or do pre/post processing of calls to that dependency.

Over time you’ll find that you regularly use the same services over and over again, in which case you can extract these services into a separate assembly that can be reused. This minimizes the size increase imposed on each individual application that uses the service.

using System;

using System.IO;

namespace which

{

    class Program

    {

        static IEnvironment m_Environment = new EnvironmentService();

        static IFile m_File = new FileService();

        static IDisplay m_Display = new ConsoleDisplay();

        static void Main(string[] args)

        {

            string[] paths = m_Environment.GetEnvironmentVariable("PATH").Split(';');

            foreach (string executable in args)

            {

                foreach (string path in paths)

                {

                    string candidatePath = Path.Combine(path, executable);

                    if (m_File.Exists(candidatePath))

                   {

                        m_Display.WriteLine(candidatePath);

                        break;

                    }

                }

            }

        }

    }

    interface IEnvironment

    {

        string GetEnvironmentVariable(string variable);

    }

    class EnvironmentService : IEnvironment

    {

        public string GetEnvironmentVariable(string variable)

        {

            return Environment.GetEnvironmentVariable(variable);

        }

    }

    interface IFile

    {

        bool Exists(string path);

    }

    class FileService : IFile

    {

        public bool Exists(string path)

        {

            return File.Exists(path);

        }

    }

    interface IDisplay

    {

        void WriteLine(string message);

    }

    class ConsoleDisplay : IDisplay

    {

        public void WriteLine(string message)

        {

            Console.WriteLine(message);

        }

    }

}