|
|
Use Activator.CreateInstance to Create Objects from a String Name Decoupling Objects | | How can I create an object of a class by using its string name instead of creating it directly? By using Activator.CreateInstance as demonstrated in this article. This will be a fairly lengthy article, but it describes some important aspects of Object Oriented Programming that sometimes programmers coming from a procedural programming mindset will completely miss in moving to an OOP language.
One of the hot topics in Object Oriented Programming (OOP) is decoupling or keeping objects loosely coupled. The less tightly coupled an application is, the easier it will be to maintain. When you do have to make changes to a loosely coupled application, the changes will normally be confined to the area you are changing. Conversely, when you change a tightly coupled application the risk of breaking something that you never intended to touch is alwasy greater.
I wrote an application recently where I used an Abstract Factory Design Pattern to create objects. If I had only a half dozen or so different objects of the same type to create, I might normally have used an if else or switch construct in C# (Select Case in VB.NET) to determine which concrete object to create based on input data. This application had approximately 40 objects of the same type (all implemented an Interface) and I did not want to convolute the code with such a construct. A 40 case switch would be a little much to say the least. So, knowing all of the input types that I would encounter and their associated processing class, I used the approach described in this article.
In a previous article on generating numerous objects of like type using a Macro, I discussed the creation of multiple classes from tables in a Word document. In this article, I will describe how I created concrete objects from those classes utilizing a dictionary without the application ever knowing which object was being created. The objects were subsequently stored in a Generic List for later processing. Since the objects were to be created and and saved in a dictionary and processed as objects of the same base type, I had to add the implementation of an interface to the classes shown in the previous article. In the article, I left that part out of the Macro and its associated discussion because that article was about code generation and I did not address the use of the resultant classes.
In this particular application, a simple description of the process was to read the input data lines, use the data from the various types of input lines to populate data objects, call the AddXMLValues method of each object, pass the data object through an XML building component, and finally use the resultant XML to call a Web Service. First, I will start with the Interface that was implemented by all of the objects which allows me to create all objects as the same type. Figure 1 shows the simple interface that all data objects implement.
Figure 1 - IProcess Interface
namespace TestCreateInstance
{
interface IProcess
{
void Parse;
void AddXMLValues(XMLDataObject xdo);
}
}
|
Next, I set up a dictionary object that would normally contain all of the transaction codes and the associated class name that will create and populate the respecttive concrete objects. Figure 2 shows the code for the Abstract Factory Class that is responsible for creating the various objects. This class has two methods in it. The first method, SetupDictionary, is called at the beginning of the application to set up a dictionary of all classes that can be created and the associated transaction code that triggers its creation. The second, or factory method, will be called each time a new input data line is read and the factory method will determine the class to instantiate based on a lookup of the transaction code in the dictionary.
The use of the dictionary does two things. First, it removes the old-time procedural logic of using an if..else or switch construct that would have to be changed anytime a new transaction type is interjected into the application. Secondly, the factory method uses Activator.CreateInstance to create the object by the use of a fully qualified string class name. This makes the method completly agnostic as to the class that is being created. In other words, the code will never make a direct reference to the class name.
Figure 2 - ProcessObjectFactory
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
namespace TestCreateInstance
{
public class ProcessObjectFactory
{
private static Dictionary<string, string> txDict;
/// <summary>
/// This method is called once to setup the data objects
/// and the associated transaction code that triggers their
/// creation. The dictionary will contain a name value pair
/// which consists of the transaction id and the associated
/// string name of the class that knows how to process the
/// transaction type.
/// This method will only add a few transactions and classes
/// to the dictionary; just enough to illustrate the process.
///
public static void SetupDictionary()
{
txDict = new Dictionary<string, string>();
txDict.Add("Name", "NameLine");
txDict.Add("Addr1", "Address1Line2");
txDict.Add("Addr2", "AddressLine2");
txDict.Add("1234", "ProductType1234");
} // method: SetupDictionary
/// <summary>
/// This is the factory method. The parameter txType
/// is one of the four shown above plus any others that
/// would be added in the real application.
///
///
/// <returns>
public static void CreateObject(string inputLine)
{
string txType = inputLine.Substring(3, 6).Trim();
// test for the existence of the transaction type
// as a key in the dictionary before attempting to
// reference it to prevent throwing an unexpected exception
if (txDict.ContainsKey(txType))
{
// the string name must be fully qualified for GetType to work
string objName = "TestCreateInstance." + txDict[txType];
IProcess txObject =
(IProcess)Activator.CreateInstance(Type.GetType(objName));
txObject.Parse(inputLine);
if (DataObjects.objList == null)
DataObjects.objList = new List<IProcess>();
DataObjects.objList.Add(txObject);
return;
} // if
throw new NullReferenceException();
} // method: CreateObject
} // class
} |
Figure 3 will show an example of the class used to process the person's name data line. This class was initially taken from the article previously mentioned, but it was enhanced to implement an IProcess Interface so that the factory can create all objects as the same type, and storing them in a processing list which can be referenced later in a processing loop. That processing loop is again agnostic with respect to the object being referenced and to the process being performed. Again, loose coupling comes into play with each object being autonomous and responsible to do only that which it knows how to do, and no other object need be concerned with what these objects are doing. Note also, because of the Interface, they all do different things with different data, yet they all are of the same base type and can be called without any knowledge on the part of the caller.
Figure 3 - NameLine Class
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace TestCreateInstance
{
public class NameLine : IProcess
{
#region Class Constructor
// calling the constructor will populate the object
public NameLine()
{
}
#endregion // constructor
#region private methods
private string GetField(string dataLine, int stChar, int len)
{
if (dataLine.Length < 74) dataLine += Space(74 - dataLine.Length);
return dataLine.Substring(stChar - 1, len).Trim();
}
// Replacement for VB.NET Space Function
public string Space(int spaceCount)
{
return String.Empty.PadLeft(spaceCount);
} // method: Space
#endregion // private methods
#region public properties
private string RecordId { get; set; }
private string RecordType { get; set; }
private string LastName { get; set; }
private string MiddleInitial { get; set; }
private string FirstName { get; set; }
#endregion // public properties
#region public methods
public void Parse(string line)
{
RecordId = GetField(line, 1, 3);
RecordType = GetField(line, 4, 6);
FirstName = GetField(line, 10, 25);
MiddleInitial = GetField(line, 35, 15);
LastName = GetField(line, 50, 25);
}
/// <summary>
/// This method will be visited so that it can
/// add the values to the XMLData object that
/// it knows about.
///
///
public void AddXMLValues(TestCreateInstance.XMLData xdo)
{
xdo.LastName = this.LastName;
xdo.MiddleName = this.MiddleInitial;
xdo.FirstName = this.FirstName;
} // method: AddXMLValues
#endregion // public methods
}
}
|
Figure 4 will show a simple XMLData class which has very few of the properties that would normally be used in the real world application. Its purpose is to act as a container for data fields that will be used to create XML for a Web Service call. However, to keep the length of the article reasonable, I am only showig one data object and the processing of one input data line.
Figure 4 - XMLData Class
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace TestCreateInstance
{
/// <summary>
/// This is a dummy class
///
public class XMLData
{
public string LastName { get; set; }
public string FirstName { get; set; }
public string MiddleName { get; set; }
public string Address1 { get; set; }
public string Address2 { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zip { get; set; }
} // class
}
|
Figure 5 will show the last helper class. The object of this class is simply to process the data objects that were created by the ProcessObjectFactory class. As you will see in the foreach loop, this method does not even know how many or which objects it is referencing, much less what the method call is accomplishing. This is encapsulation of business logic as it should be. This class is simply letting a set of objects do what they alone know to do. The DataObjects class knows only to do what he does; call other objects. What they do, how they do it, or even who they are is of no concern to it.
Figure 5 - DataObjects
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace TestCreateInstance
{
public class DataObjects
{
public static List<IProcess> objList;
/// <summary>
/// Visit all of the objects in the list. This method
/// is agnostic as to what will be done by the call
/// to the AddXMLValues method and doesn't care. Every
/// object is responsible to do its own thing so it can
/// be changed without chaning any other part of the app.
///
public static void ProcessAllObjects(XMLData xdo)
{
foreach (IProcess obj in objList)
{
obj.AddXMLValues(xdo);
} // foreach
} // method: Process
}
}
|
Finally, Figure 6 shows the main processing loop of the application. This module has been kept very simple. It reads only one line of input data, creates one input data object, and then calls the ProcessAllObjects method of the DataObjects class shown in Figure 5. Actually, you can create a project using all of the classes shown in this article and it will run, assuming that you use VS 2008 because it utilizes automatic properties in the NameLine class.
Figure 6 - Program Class
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
namespace TestCreateInstance
{
class Program
{
static void Main(string[] args)
{
string data = BuildDataString();
// this would normally read an input file, but is just
// going to read one line of data with a string reader
// for the demo
using (StringReader sr = new StringReader(data))
{
ProcessObjectFactory.SetupDictionary();
string inputLine = string.Empty;
while(inputLine != null)
{
inputLine = sr.ReadLine();
if (inputLine == null) break;
// CreateObject will create an object to process
// the associated transaction type and insert into a
// list of objects for future processing
ProcessObjectFactory.CreateObject(inputLine);
} // while
sr.Close();
} // using
XMLData xd = new XMLData();
DataObjects.ProcessAllObjects(xd);
}
/// <summary>
/// Build a test data string.
///
/// <returns>
private static string BuildDataString()
{
return "001Name Joe E Brown";
} // method: BuildDataString
}
}
|
One of the main goals of Object Oriented Programming is encapsulation or "hiding" the data and the business logic of an application in modular, single process components. The very term encapsulation promotes the mind set of loose coupling. In other words, develop code that does not have to be changed just because changes occur in other areas of an application. This not only makes code easier to maintain, and easier to test, but easier to do regression testing when changes do have to be made.
Have you tried our newest product, Visual Class Organizer? You'll be amazed how easy it is to keep the code in your code windows organized. TRY IT FREE FOR 30 DAYS BY CLICKING HERE.
If you are developing in C# and haven't tried CSharpCompleter, you are wasting valuable time typing hundreds of braces {} daily needlessly. Try CSharpCompleter for 30 DAYS FREE.
| Ask a Question, or give your feedback on my articles or products by going to the KnowDotNet Forum or by clicking on My Blog. |  |
You can also email me directly at les@KnowDotNet.com.
|
|