The second problem plagues me. I know it plagues us all. How many times have you heard yourself think - or even say out loud - how was this missed? Why am I retrofitting my code now for this? How in the heck did someone forget this?
This problem is so resident in so many places, projects, and situations. My current project, for instance, had requirement changes days - hours - before deployment. Why? Changes in process, restructuing, that sort of thing. Business changes. The processes which drive it evolve, just like the real world around us. The applications we write should adapt as the world does.
This is the workflow part of the essay. During this section, I'll expose you to the workflow-specific areas within MT, and point out how this idea can limit rework later on. I'll make use of some typical OO concept to demonstrate this stuff, and remind you - the more work you put up front, the less you do in the end.
So let's get started - the first part of the workflow portion of MT is the IProcessStep interface. Here's the code for it:
using System;
namespace MetaTechture.Workflow
{
/// <summary>
/// Represents the fundamental building block of the entire workflow model.
/// The IProcessStep interface is the basic component of a workflow process, as
/// it represents a single step in any process. This interface depicts
/// the basic structure of any step in a larger process that an
/// application must execute.
/// </summary>
public interface IProcessStep
{
/// <summary>
/// Any process step must contain independent functionality
/// that it shall execute. Within this method, the functionality
/// should reside.
/// </summary>
/// <remarks>
/// The point of this class is simple. As an object moves through
/// any process, the object's state may alter or be altered as
/// a result of the actions within the step. Any class which
/// executes multiple steps should not only account for the step
/// itself, but the change to the object's state as the
/// step occurs. To persist state, it is good practice for the
/// object to return itself from the execution. In essence,
/// the next step can adapt to the new state, and the process
/// can continue.
/// </remarks>
/// <returns></returns>
void Execute(ProcessEngine engineContext);
/// <summary>
/// Within any process, a step can have adverse affects on the overall
/// execution of the process. As each step occurs, there is the
/// potential for error, potential for the process to complete,
/// and so on. As each step occurs, this property should be
/// set so that the engine knows whether to continue in the
/// process or to terminate it's execution after this step.
/// </summary>
bool HaltsProcess { get; }
}
}
This interface's sole purpose is to mark a class as a “step” in a longer “process.” I'll make use of this idea later in a relatively simplistic process to exemplify the idea. For now, think of it from two points of view.
Processes are Recipes with Little Ingredients (but room is left for variance)
Everything has to be done with steps. You get from your house to the car, you take steps, then open the car door, then shut it, and on and on. You make a PB&J, you have to do things in your own certain order (some of us like the PB before the J, others vice versa, for instance). With this notion in mind, I'll move forward and point out the next part of the workflow idea - the process engine. To begin with, the process engine contains an internal Hashtable that I'll use in much the same way that the HttpCache is used. As a process executes other objects within a subsystem will be affected in appropriate measures. For this, we'll use the Hashtable collection:
using System;
using System.Collections;
namespace MetaTechture.Workflow
{
/// <summary>
/// Runs the collection of steps for a given process against a particular
/// object instance.
/// </summary>
public class ProcessEngine
{
Hashtable ctx;
When the ProcessEngine class is constructed I'll get the Hashtable ready for usage by declaring it as a new object. To boot, I'll add a Facade around the internal collection by adding an AddToApplicationContext method and an indexer.
/// we won't allow construction, as the static Run method will be enough.
public ProcessEngine()
{
ctx = new Hashtable();
}
public int AddToApplicationContext(object key, object value)
{
ctx.Add(key,value);
return ctx.Count;
}
public object this[object key]
{
get { return ctx[key]; }
set { ctx[key] = value; }
}
This way, Process Step classes can re-use objects that have been stashed in the Process Engine's execution layer (as you'll see later on, this allows the steps interact with one another in much the same way we work with other caching methodologies).
Processes Should be Able to Stop Themselves
In the final stages of the ProcessEngine class, I'll add logic that will once again use the configuration capabilities in .Net to load in the steps for a process. This stage of the code creates instances of classes that inherit the IProcessStep interface, and will add these items to the overall process in the order they appear in the configuration file....
Which raises an interesting point! Suppose, from our earlier example of making a PB&J, we have a requirement stating the order things should happen -
- Step 1: Get the Bread
- Step 2: Apply Peanut Butter
- Step 3: Apply Jelly
And you write code implementing this functionality - or rather, you hard-code a method executing this functionality. Then, one day, the world turns upside-down and you learn that in these new days of jellyloving sandwich-eaters, people like their jelly applied before the peanut butter. Guess what? Your method(s) now need to be rewritten to accomodate the new functionality. Sure, each method (GetBread(), ApplyPb(), and ApplyJelly(), if you've been a good refactorer up to this point) may be wonderfully mutually exclusive, but since you're executing these according to the original specification, you now need to edit, recompile, and re-deploy your existing code. With the workflow resident in MT (via .Net's configuration wonders), this problem will vanish!
So back to the process itself! At this point, we've covered everything except the Run() method, which basically uses the configuration classes shown below to load and Execute() each step. Take notice esepcially here of how the method performs a check to see if each step of the process is supposed to Halt the process. In this implementation, you can see the beginnings of a control - should one step cause problems in the process - or better yet, complete it earlier than expected - cancel everything from there on out. This leaves the decision-making logic up to each step in the process. Here's the code:
/// <summary>
/// Executes the collection of providers' functions.
/// </summary>
public void Run(string process)
{
// get the name of the type of provider we're supposed to use
string typeName = String.Empty;
ProcessEngineManagerSettings settings =
ProcessEngineManagerSettings.GetSettings(process);
for(int i=0; i<settings.TypesInProcess.Length; i++)
{
typeName = settings.TypesInProcess[i];
// create an instance of that type
Type t = Type.GetType(typeName);
// now create an instance of that type
object provider = null;
provider = Activator.CreateInstance(t);
// make sure it's an implementor of the proivider interface
IProcessStep step = provider as IProcessStep;
if(step != null)
{
// perform it's functionality
step.Execute(this);
if(step.HaltsProcess)
break;
}
}
}
}
}
Not too bad - just create an instance of each class listed in the custom configuration section, in the order we find them all. To finish up this discussion, take a look at the configuration classes, which are listed below:
using System;
using cs = System.Configuration.ConfigurationSettings;
using System.Xml;
namespace MetaTechture.Workflow
{
/// <summary>
/// The configuration reader
/// </summary>
public class ProcessEngineManagerConfigurationReader : System.Configuration.IConfigurationSectionHandler
{
public object Create(object parent,
object context,
XmlNode section)
{
XmlNodeList lstActions = section.SelectNodes("//ExecutionProvider");
return new ProcessEngineManagerSettings(lstActions);
}
}
/// <summary>
/// The settings class, which contains the type name to be loaded.
/// </summary>
public class ProcessEngineManagerSettings
{
public readonly string[] TypesInProcess;
static string activeProcess = String.Empty;
internal ProcessEngineManagerSettings(XmlNodeList lst)
{
string[] types = new string[0];
for(int i=0; i<lst.Count; i++)
{
if(lst[i].Attributes["process"].Value.ToLower() == activeProcess.ToLower())
{
string[] tTmp = new string[types.Length+1];
Array.Copy(types,0,tTmp,0,types.Length);
tTmp[types.Length] = lst[i].Attributes["type"].Value;
types = tTmp;
}
}
TypesInProcess = types;
}
/// <summary>
/// Build the name of the path to the nodes we'll run.
/// </summary>
/// <returns></returns>
public static ProcessEngineManagerSettings GetSettings(string process)
{
string section = "ProcessProviders";
activeProcess = process;
ProcessEngineManagerSettings b =
(ProcessEngineManagerSettings)cs.GetConfig(section);
return b;
}
}
}
Once again making use of the Provider concept, we've assumed that, within the configuration file, a series of steps have been listed. These steps, when executed in appropriate order, complete a total process' execution. The XML below is a good example of how a process could be listed and executed within an application making use of the MT workflow concept.
<?
xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="ProcessProviders" type="MetaTechture.Workflow.ProcessEngineManagerConfigurationReader, MetaTechture.Workflow" />
</configSections>
<!-- process-specific application settings -->
<ProcessProviders>
<!-- steps in various processes -->
<ExecutionProvider type="MetaTechture.Workflow.BreadStep, MetaTechture.ConsoleClients" process="MakeSandwich" />
<ExecutionProvider type="MetaTechture.Workflow.JellyStep, MetaTechture.ConsoleClients" process="MakeSandwich" />
<ExecutionProvider type="MetaTechture.Workflow.PeanutButterStep, MetaTechture.ConsoleClients" process="MakeSandwich" />
</ProcessProviders>
</configuration>
At this point, we've listed a few steps in the process section for a small application. This small little app will make a nice demonstration of using the following three classes - each of which representing or objectifying a small part of a larger process.
using System;
namespace MetaTechture.Workflow
{
/// <summary>
/// The first step in the process.
/// </summary>
public class BreadStep : IProcessStep
{
public void Execute(ProcessEngine engine)
{
Console.WriteLine("Get the Bread");
}
public bool HaltsProcess
{
get
{
return false;
}
}
}
/// <summary>
/// The second step in the process.
/// </summary>
public class PeanutButterStep : IProcessStep
{
public void Execute(ProcessEngine engine)
{
Console.WriteLine("Put PB on the bread");
}
public bool HaltsProcess
{
get
{
return false;
}
}
}
/// <summary>
/// The second step in the process.
/// </summary>
public class JellyStep : IProcessStep
{
public void Execute(ProcessEngine engine)
{
Console.WriteLine("Put jelly on the bread");
}
public bool HaltsProcess
{
get
{
return false;
}
}
}
}
For now, I'll save the more complex idea of working with the context for a later, more comprehensive example. The thing here is to notice - that each step of the process completes one small aspect of the larger, wholer process. In a sense, we've refactored the entire process of making a sandwich into seperate classes, thereby giving structure and shape to activity. In this way, even the process itself has been objectified and therefore, is extensible in and of itself. Should someone decide that we need to change the PB&J order, the re-coding is minimal - just swap two lines in the config file, and the whole application is altered on the fly. A process could be added to or removed in the same method. We'll make use of this heavily in the study at the end of the post series. For now, let's wrap it all up by taking a look at the console application which kicks off the whole MakeSandwich process.
using System;
using MetaTechture.Workflow;
namespace MetaTechture.ConsoleClients
{
/// <summary>
/// Summary description for Class1.
/// </summary>
class Class1
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main(string[] args)
{
RunSandwichStepDemo();
Console.ReadLine();
}
static void RunSandwichStepDemo()
{
ProcessEngine engine = new ProcessEngine();
engine.Run("MakeSandwich");
}
}
}
Happy coding! Next up - a plugin model!