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)
{
char[] array = password.ToCharArray();
Array.Reverse(array);
return new string(array);
}
protected virtual void InsertUserDetails
(string username, string realName, string encryptedPassword)
{
Console.Write(String.Format("Default Behaviour\n
Inserting Details for User ({0}, {1}, {2})\n\n",
username, realName, encryptedPassword));
}
private void AuditTrailCreateUser(string username)
{
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)
{
base.InsertUserDetails(username, realName, encryptedPassword);
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)
{
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)
{
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.