Click here to Skip to main content
Click here to Skip to main content

Refactor to Dependency Injection

, 14 May 2010 CPOL
Rate this:
Please Sign up or sign in to vote.
Further refine our Template Method example to use Dependency Injection.

Introduction

This is the second of two articles on Refactoring to Design Patterns.

In part one of this series of articles, we took some simple procedural code and used the Template Method Design Pattern to allow us to override specific parts of the algorithm. We saw that we can create derived classes that override just one method, or that overrides multiple methods.

This raises an interesting question. Let's assume that we have a base class and we want to deploy it to a number of different countries; the required behaviour for each country is as follows:

Country Encryption Insert User Details Method
UK Reverse Default
Australia Reverse and Upper Case Default
France No Encryption Default
Germany Reverse DB Design 2
Japan Reverse and Upper Case DB Design 2
United States Reverse and Upper Case DB Design 3

No two countries use the same combination of Encryption and Inserting User Details. We can use the default SecurityManager class for UK, but after that, it looks like we need 5 different sub classes, one for each of the remaining countries.

The really annoying thing is that Australia, Japan, and the United States all use the same encryption, but differ in how they insert user details. So, we need three sub classes and we'll be duplicating the same encryption method on those three classes.

Similarly UK, Australia, and France share the same method of inserting user data, but differ in how they encrypt the password. This also applies to Germany and Japan. This isn't acceptable. We started out trying to keep all of our special case logic in one place, and here we are duplicating it into multiple classes.

Remember, we're dealing here with a very small, simplified problem which has only two variables. If you scale this up to the kinds of issues you face in real production code, you can see that a separate subclass for each unique situation isn't going to work.

The initial code

We pick up where we left off at the end of the first article. We have a working solution which encrypts passwords and inserts user data into a variety of database designs.

class SecurityManager
{
    public void CreateUser(string username, string realName, string password)
    {
        string encryptedPassword = GetEncryptedPassword(password);
        InsertUserDetails(username, realName, encryptedPassword);
        AuditTrailCreateUser(username);
    }
    protected virtual string GetEncryptedPassword(string password)
    {
        //Encrypt the Password
        char[] array = password.ToCharArray();
        Array.Reverse(array);
        return new string(array);
    }
    protected virtual void InsertUserDetails
        (string username, string realName, string encryptedPassword)
    {
        // Insert any required database details about the user 
        Console.Write(String.Format("Default Behaviour\n
        Inserting Details for User ({0}, {1}, {2})\n\n", 
        username, realName, encryptedPassword));
    }
    private void AuditTrailCreateUser(string username)
    {
        //Insert audit trail entries about the creation of the user
        Console.Write(String.Format("Default Behaviour\n
        AuditTrail Create User ({0})\n\n", username));
    }
}

Extracting to a helper class

We need to extract our encryption algorithms into a separate object. Like all good design decisions, this one seems like a no-brainer in retrospect. It's quite likely that encryption will be handy in more scenarios than when we actually create a user. For example, if we need to generate a new password and email it to a user, this could be a handy object to have at our disposal.

Before I continue with this, some readers might be screaming that for a simple piece of logic like this, I should be using a Delegate. They're right. However, in this case, the simple one method class is just for illustration. The design concept is intended for situations where we need to inject something more complicated than a single function.

With that disclaimer out of the way, let's create a PaswordEncrpytor class and see how it makes things easier for us. We start by creating a simple class that provides one method called Encrypt. What we've actually done is taken the GetEncryptedPassword function from the base SecurityManager class.

Note that the Encrypt method in our new PasswordEncryptor is virtual, this is very important as we'll see shortly.

class PasswordEncryptor
{
    public virtual string Encrypt(string password)
    {
        char[] array = password.ToCharArray();
        Array.Reverse(array);
        return new string(array);
    }
}

Having created this helper class, we now need to modify the existing SecurityManager class to use it.

Step one is to add a constructor to the base class so that we can send it the PasswordEncryptor that we'd like it to use. We also need a private variable in the SecurityManager class to hold the PasswordEncryptor that has been provided.

With that done, we also need to modify the SecurityManager class to remove the EncryptPassword function that it was using and make it use our PasswordEncryptor object instead. Here are the relevant parts of the new version of SecurityManager:

class SecurityManager
{
    private PasswordEncryptor _passwordEncryptor;
    public SecurityManager(PasswordEncryptor passwordEncryptor)
    {
        _passwordEncryptor = passwordEncryptor;
    }

    public void CreateUser(string username, string realName, string password)
    {
        string encryptedPassword = _passwordEncryptor.Encrypt(password);
        InsertUserDetails(username, realName, encryptedPassword);
        AuditTrailCreateUser(username);
    }
    ...
}

To use this new SecurityManager class, we now instantiate the PasswordEncryptor separately from our SecurityManager, and then pass it to the constructor of SecurityManager.

PasswordEncryptor passwordEncryptor = new PasswordEncryptor();            
SecurityManager securityManager = new SecurityManager(passwordEncryptor);
DoStuffWithSecurityManager(securityManager);

It's a small extra step in getting our SecurityManager up and running, but what a difference it will make to the mess of classes that we discussed at the top of this article.

We have extracted the logic about encrypting passwords into a separate class, we then inject that class into the SecurityManager to enable it to encrypt passwords without it knowing or caring about the mechanics of the encryption.

If you're keeping up, then it might have dawned on you that if we were to inherit from PasswordEncryptor, we could pass a subclass into SecurityManager to handle encryption differently, and SecurityManager would be fine with that.

Let's create a subclass of PasswordEncryptor called DoNothingPasswordEncryptor.

class DoNothingPasswordEncryptor: PasswordEncryptor 
{
    public override string Encrypt(string password)
    {
        return password;
    }
}

Now, let's pass it to SecurityManager and see what happens.

PasswordEncryptor passwordEncryptor = new DoNothingPasswordEncryptor();            
SecurityManager securityManager = new SecurityManager(passwordEncryptor);
DoStuffWithSecurityManager(securityManager);

Here's the console output:

Default Behaviour
Inserting Details for User (daltonr, Richard Dalton, GuessThis)
Default Behaviour
AuditTrail Create User (daltonr)

We can see that our 'GuessThis' password comes out unchanged. We now have PasswordEncryptor objects that handle two of the three possible types of encryption. The remaining type involves reversing the password and converting it to uppercase. Time for another subclass.

class UpperCasePasswordEncryptor: PasswordEncryptor 
{
    public override string Encrypt(string password)
    {
        return base.Encrypt(password).ToUpper();
    }
}

Passing this to SecurityManager gives us a reversed upper case password. Perfect.

PasswordEncryptor passwordEncryptor = new UpperCasePasswordEncryptor();            
SecurityManager securityManager = new SecurityManager(passwordEncryptor);
DoStuffWithSecurityManager(securityManager);

Here's the console output:

Default Behaviour
Inserting Details for User (daltonr, Richard Dalton, SIHTSSEUG)

Default Behaviour
AuditTrail Create User (daltonr)

What we've actually done here is use two Design Patterns: Strategy and Dependency Injection.

The Strategy pattern involves extracting an algorithm into an object so that different algorithms can be swapped with each other at runtime. That's exactly what our PasswordEncryptor object is.

The Dependency Injecting pattern involves providing something to a class that it depends on, rather than leaving it up to the class to get that resource itself. In this case, our SecurityManager depends on having a PasswordEncryptor.

We could have left it up to SecurityManager to instantiate the correct PasswordEncryptor. It would have probably involved some 'If' statements.

This would have meant pushing decisions back into the SecurityManager that we worked to remove in the first article. By injecting a password encryptor into SecurityManager, we keep our decision logic out of these basic classes.

Inheriting from SecurityManager

We have covered all the password encryption possibilities; we now need to ensure we cover the different ways of inserting user details. Our base SecurityManager class will cover the default case for UK, Australia, and France.

We need a SchemaTwoSecurityManager and SchemaThreeSecurityManager for the remaining cases.

SchemaTwoSecurityManager looks a lot like it did in the previous article; however, the PasswordEncryption logic is now gone. The constructor accepts our PasswordEncryptor class and passes it on directly to the base SecurityManager class.

class SchemaTwoSecurityManager: SecurityManager 
{
    public SchemaTwoSecurityManager(PasswordEncryptor passwordEncryptor): 
                                    base(passwordEncryptor) {}
    protected override void InsertUserDetails(string username, 
              string realName, string encryptedPassword)
    {
        // SchemaTwo Inserts the Data in the same way as the Default method
        base.InsertUserDetails(username, realName, encryptedPassword);
        // But adds some New Steps
        InsertDetailsForABCSystem();
        GrantPermissionsForXYZ();
    }
    private void InsertDetailsForABCSystem()
    {
        Console.Write("Schema Two Behaviour\nInserting Details into ABC System\n\n");
    }
    private void GrantPermissionsForXYZ()
    {
        Console.Write("Schema Two Behaviour\nGrant Permissions For XYZ\n\n");
    }
}

SchemaThreeSecurityManager works in the same way.

class SchemaThreeSecurityManager : SecurityManager
{
    public SchemaThreeSecurityManager(PasswordEncryptor passwordEncryptor) : 
                                      base(passwordEncryptor) { }
    protected override void InsertUserDetails(string username, 
              string realName, string encryptedPassword)
    {
        // Insert any required database details about the user 
        Console.Write(String.Format("Schema Three Behaviour\n
           Inserting Details for User ({0}, {1}, {2})\n\n",
           username, realName, encryptedPassword));
    }
}

Putting it all together

We now have all the components we need to implement the different combinations of encryption and database inserts. Instead of having separate classes for every distinct combination, we can instantiate our two components separately and combine them for the desired results.

And here are those desired results again:

Country Encryption Insert User Details Method
UK Reverse Default
Australia Reverse and Upper Case Default
France No Encryption Default
Germany Reverse DB Design 2
Japan Reverse and Upper Case DB Design 2
United States Reverse and Upper Case DB Design 3

And, here's an example of how all that logic has been pulled out of the SecurityManager class and kept together:

static void Main(string[] args)
{
    // Country hard coded for illustration purposes
    string country = "France";

    PasswordEncryptor passwordEncryptor;
    passwordEncryptor = GetPasswordEncryptor(country);
    SecurityManager securityManager;
    securityManager = GetSecurityManager(passwordEncryptor, country);
    DoStuffWithSecurityManager(securityManager);
    Console.ReadKey(true);
}

The GetPasswordEncryptor() and GetSecurityManager() functions are what are known as Factory Methods (another Design Pattern). The algorithm above knows it needs a PasswordEncryptor and a SecurityManager. It doesn't know (or care) which specific subclass of those classes that it gets.

In fact, it's more interesting than that. This algorithm doesn't even know (or care) what subclasses exist for PasswordEncryptor and SecurityManager. It calls two Factory Methods that know about the subclasses and which one to return.

The Factory Methods are every bit as simple as you would imagine.

private static PasswordEncryptor GetPasswordEncryptor(string country)
{
    PasswordEncryptor passwordEncryptor;
    switch (country)
    {
        case "UK":
        case "Germany":
            passwordEncryptor = new PasswordEncryptor();
            break;
        case "France":
            passwordEncryptor = new DoNothingPasswordEncryptor();
            break;
        case "Australia":
        case "Japan":
        case "US":
            passwordEncryptor = new UpperCasePasswordEncryptor();
            break;
    }
    return passwordEncryptor;
} 

private static SecurityManager GetSecurityManager(PasswordEncryptor 
               passwordEncryptor, string country)
{ 
    SecurityManager securityManager;
    switch (country)
    {
        case "UK":
        case "Australia":
        case "France":
            securityManager = new SecurityManager(passwordEncryptor);
            break;
        case "Japan":
        case "Germany":
            securityManager = new SchemaTwoSecurityManager(passwordEncryptor);
            break;
        case "US":
            securityManager = new SchemaThreeSecurityManager(passwordEncryptor);
            break;
    }
    return securityManager;
}

There's one final thing worth mentioning here.

We are not limited to injecting one dependency. In this case, we had two aspects of the SecurityManager that varied, so we kept them separate by injecting one as needed. You could have an algorithm with three or four (or more) parts that you need to modify independently of each other.

Be careful though. If you have an algorithm that has a lot of dependencies which need to be injected, you may be doing too much with the algorithm in the first place.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

No Biography provided

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.141216.1 | Last Updated 14 May 2010
Article Copyright 2010 by Richard A. Dalton
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid