I'm Tom Jaeschke and you may contact me at tomjaeschke@tomjaeschke.com or (512) 419-8788.

Thursday, February 26, 2009

Idiot-Proof Inversion of Control Example

What follows is what I hope to be a dumbed-down example of how to enact IoC (Inversion of Control) that anyone may easily follow. My example uses StructureMap. There are a great many tools that one may utilize for IoC. I'm using StructureMap as Headspring has standardized around StructureMap.



Alright, we will use an example Visual Studio 2008 C# web application with one web form (Default.aspx) and three classes. The function of the application is to firstly display a TextBox, a Button, and a Label, and secondly to put the number of characters in the TextBox into the Label whenever the Button is pressed. Thus, if I enter "123 Happy Street" into the TextBox and press the Button, I should see "11" appear in the Label.





So how does the web form translate a random string into a numeric count of particular characters? The code behind (Default.aspx.cs) looks like this:



using System;

namespace inversiontest
{
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{

}

protected void Button1_Click(object sender, EventArgs e)
{
string myCopy = TextBox1.Text;
NonLetterPurge myCutter = new NonLetterPurge();
CharacterCount myCounter = new CharacterCount();
StringHandler myHandler = new StringHandler();
int letterCount = myHandler.runProcess(myCutter, myCounter, myCopy);
Label1.Text = letterCount.ToString();
}
}
}



The three classes being used look like this:



NonLetterPurge.cs:
namespace inversiontest
{
public class NonLetterPurge
{
public string dropUndesirableCharacters(string blobOfCopy)
{
string cleanBlob = "";
foreach(char blobCharacter in blobOfCopy)
{
if (char.IsLetter(blobCharacter))
{
cleanBlob = cleanBlob + blobCharacter;
}
}
return cleanBlob;
}
}
}




CharacterCount.cs:
namespace inversiontest
{
public class CharacterCount
{
public int convertStringToNumber(string blobOfCopy)
{
return blobOfCopy.Length;
}
}
}




StringHandler.cs:
namespace inversiontest
{
public class StringHandler
{
public int runProcess(NonLetterPurge cutter, CharacterCount counter, string copy)
{
string revisedCopy = cutter.dropUndesirableCharacters(copy);
return counter.convertStringToNumber(revisedCopy);
}
}
}




With the four pieces of C# above (one code behind and three classes), my application performs as it should. However, all of my classes are tightly coupled and I have to new up three classes just to use one.



The following details the introduction of Inversion of Control methodologies to the application described above.



First, I download the latest version of StructureMap (version 2.5.3) at http://sourceforge.net/project/showfiles.php?group_id=104740 and once I had the StructureMap.dll I placed it in my bin directory and then created a reference to it in my project within Visual Studio.



Secondly, I significantly rewrote my application. For each of the three classes I created an interface, and the classes themselves were altered as well (as was the way they were enacted from the web form's code behind). In the end there were seven files containing C# which were: CharacterCount.cs, Default.aspx.cs, ICharacterCount.cs (an interface for CharacterCount), INonLetterPurge.cs (an interface for NonLetterPurge), IStringHandler.cs (an interface for StringHandler), NonLetterPurge.cs, and StringHandler.cs. These items contained the following code:



CharacterCount.cs:
namespace inversiontest
{
public class CharacterCount : ICharacterCount
{
public int convertStringToNumber(string blobOfCopy)
{
return blobOfCopy.Length;
}
}
}




Default.aspx.cs:
using System;
using StructureMap;

namespace inversiontest
{
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
ObjectFactory.Initialize(x =>
{
x.ForRequestedType<ICharacterCount>().TheDefaultIsConcreteType<CharacterCount>();
x.ForRequestedType<IStringHandler>().TheDefaultIsConcreteType<StringHandler>();
x.ForRequestedType<INonLetterPurge>().TheDefaultIsConcreteType<NonLetterPurge>();
});
}

protected void Button1_Click(object sender, EventArgs e)
{
string myCopy = TextBox1.Text;
var myHandler = ObjectFactory.GetInstance();
int myNumber = myHandler.runProcess(myCopy);
Label1.Text = myNumber.ToString();
}
}
}




ICharacterCount.cs:
namespace inversiontest
{
public interface ICharacterCount
{
int convertStringToNumber(string blobOfCopy);
}
}




INonLetterPurge.cs:
namespace inversiontest
{
public interface INonLetterPurge
{
string dropUndesirableCharacters(string blobOfCopy);
}
}




IStringHandler.cs:
namespace inversiontest
{
public interface IStringHandler
{
int runProcess(string copy);
}
}




NonLetterPurge.cs:
namespace inversiontest
{
public class NonLetterPurge : INonLetterPurge
{
public string dropUndesirableCharacters(string blobOfCopy)
{
string cleanBlob = "";
foreach(char blobCharacter in blobOfCopy)
{
if (char.IsLetter(blobCharacter))
{
cleanBlob = cleanBlob + blobCharacter;
}
}
return cleanBlob;
}
}
}




StringHandler.cs
namespace inversiontest
{
public class StringHandler : IStringHandler
{
private readonly ICharacterCount _characterCount;
private readonly INonLetterPurge _nonLetterPurge;

public StringHandler(ICharacterCount characterCount, INonLetterPurge nonLetterPurge)
{
_characterCount = characterCount;
_nonLetterPurge = nonLetterPurge;
}

public int runProcess(string copy)
{
string revisedCopy = _nonLetterPurge.dropUndesirableCharacters(copy);
return _characterCount.convertStringToNumber(revisedCopy);
}
}
}




The application now runs and works as it did before, but much has changed behind the scenes. Notice that the code that runs when the button is clicked which once read...



string myCopy = TextBox1.Text;
NonLetterPurge myCutter = new NonLetterPurge();
CharacterCount myCounter = new CharacterCount();
StringHandler myHandler = new StringHandler();
int letterCount = myHandler.runProcess(myCutter, myCounter, myCopy);
Label1.Text = letterCount.ToString();


...has been simplified to:


      
string myCopy = TextBox1.Text;
var myHandler = ObjectFactory.GetInstance();
int myNumber = myHandler.runProcess(myCopy);
Label1.Text = myNumber.ToString();


The copy in the Page Load event wires up the interfaces in preparation of their use. It looks like so:



ObjectFactory.Initialize(x =>
{
x.ForRequestedType<ICharacterCount>().TheDefaultIsConcreteType<CharacterCount>();
x.ForRequestedType<IStringHandler>().TheDefaultIsConcreteType<StringHandler>();
x.ForRequestedType<INonLetterPurge>().TheDefaultIsConcreteType<NonLetterPurge>();
});


Obviously, it would be ideal to not have to repeat this manner of wiring across the Page Load events of many web forms in a more complicated application. Couldn't these sorts of associations just be enacted once globally at the launch of an application? Yes, if I remove the lines of code above, I can enact the same effect by creating an XML configuration file called StructureMap.config. In this file I will have the following:


<StructureMap>

<PluginFamily Type="inversiontest.ICharacterCount" Assembly="inversiontest" DefaultKey="Default">
<Plugin Assembly="inversiontest" Type="inversiontest.CharacterCount" ConcreteKey="Default" />
</PluginFamily>

<PluginFamily Type="inversiontest.INonLetterPurge" Assembly="inversiontest" DefaultKey="Default">
<Plugin Assembly="inversiontest" Type="inversiontest.NonLetterPurge" ConcreteKey="Default" />
</PluginFamily>

<PluginFamily Type="inversiontest.IStringHandler" Assembly="inversiontest" DefaultKey="Default">
<Plugin Assembly="inversiontest" Type="inversiontest.StringHandler" ConcreteKey="Default" />
</PluginFamily>

</StructureMap>

No comments: