You
could continue to put all processing logic in the Main routine of your
console application, but eventually you would get tired of always typing
the same basic code. All of your programs would start to look pretty
similar to Passthrough and Replace. It would be nice if you could
inherit some functionality and simply change the parts that you need to
change. You can achieve such an effect with a design pattern known as
the Template Method. In it, you create a skeleton algorithm in a base
class and then allow child classes to fill in the details. Expressed in
pseudocode, the skeleton algorithm might look like the following
pseudocode snippet:
If the arguments to the program are valid then
Do necessary pre-processing
For every line in the input
Do necessary input processing
Do necessary post-processing
Otherwise
Show the user a friendly usage message
An example of subclass responsibility is to define what pre-processing,
post-processing, and input processing actually mean to that particular
subclass. You could put logic like this in the static Main method,
effectively making it the skeleton algorithm, but this presents two
problems. First, console apps compile to executables. .NET executables
can be used as class libraries, but Visual Studio .NET makes that
difficult to do. Thus, it's hard to allow subclasses to fill in the
details because it's difficult to create subclasses.
The
second problem is that Main is a static method. Static methods don't
belong to instances of a class; they belong to the class itself. This
means that all the steps of the algorithm (pre-process, post-process,
and so on) would have to be implemented as static methods as well,
preventing them from being overridden. Only instance methods can be
overridden and, as such, the methods representing the steps of the
skeleton algorithm need to be instance methods of a class.
The
best way to get around these limitations is to separate each console
application into two classes. The first class is just the normal console
application class with the static Main entry point (call it the
"chassis"). Instead of containing processing logic, however, the Main
routine creates an instance of the second class (call it the "engine")
and delegates processing to it. For the Passthrough example, the chassis
would look like the code in Figure 3. Notice how the
engine class has a similar entry point, Main, but that it is an instance
method rather than a static method. The Main method of the engine class
will become your skeleton algorithm, allowing you to employ the
Template Method design pattern. ConsoleEngineBase is the name of the
class that will hold the implementation of the skeleton algorithm and
all engines will inherit from it, supplying their own versions of the
algorithm's steps along the way. The complete code for ConsoleEngineBase
is given in Figure 4.
using System;
namespace Engines
{
public abstract class ConsoleEngineBase
{
private string[] m_args;
protected bool ReadInput = true;
protected System.IO.TextReader In = null;
protected System.IO.TextWriter Out = null;
protected System.IO.TextWriter Error = null;
public ConsoleEngineBase()
{
//by default, read from/write to standard streams
this.In = System.Console.In;
this.Out = System.Console.Out;
this.Error = System.Console.Error;
}
public void Main(string[] args)
{
this.m_args = args;
if(this.ValidateArguments())
{
this.PreProcess();
if(this.ReadInput)
{
string currentLine = this.In.ReadLine();
while(currentLine != null)
{
this.ProcessLine(currentLine);
currentLine = this.In.ReadLine();
}
}
this.PostProcess();
}
else
this.Error.Write("Usage: " + this.Usage());
}
public void Main(string[] args,
System.IO.TextReader In,
System.IO.TextWriter Out,
System.IO.TextWriter Error)
{
//this version of Main allows alternate streams
this.In = In;
this.Out = Out;
this.Error = Error;
this.Main(args);
}
protected virtual bool ValidateArguments()
{
//override this to add custom argument checking
return true;
}
protected virtual string Usage()
{
//override this to add custom usage statement
return "";
}
protected virtual void PreProcess()
{
//override this to add custom logic that
//executes just before standard in is processed
return;
}
protected virtual void PostProcess()
{
//override this to add custom logic that
//executes just after standard in is processed
return;
}
protected virtual void ProcessLine(string line)
{
//override this to add custom processing
//on each line of input
return;
}
protected string[] Arguments
{
get {return this.m_args;}
}
}
}
using System;
using Engines;
namespace ConsoleApps
{
class PassthroughChassis
{
[STAThread]
static void Main(string[] args)
{
Engines.ConsoleEngineBase engine = new PassthroughEngine();
engine.Main(args);
}
}
}
Since
PassthroughEngine inherits from ConsoleEngineBase, it can be
implemented very simply. The complete PassthroughEngine class is shown
here:
Passthrough doesn't need to bother with any special pre- or
post-processing or checking of arguments, so it doesn't override these
steps of the algorithm. It needs to do one thing—echo every line of
input to output—and it does this by providing its own implementation of
ProcessLine. Figure 5 summarizes the interactions between the classes in this model.
public class PassthroughEngine : ConsoleEngineBase
{
public override void ProcessLine(string line)
{
this.Out.WriteLine(line);
}
}
Figure 5 Class Relationships
The
benefit of employing the Template Method design pattern is obvious:
inherited classes only need to change the parts of the algorithm that
they require, resulting in less overall coding. But what are the
consequences of breaking every console application into chassis and
engine classes? There are at least two negative consequences: first, you
end up with twice as many classes as before, and second, you end up
with a bunch of really simple, similar chassis classes. I think these
drawbacks, however, are more than outweighed by the benefits.
The
first, most important benefit is that engine classes are now free to
inherit from one another. PassthroughEngine can be implemented with one
line of code. A second, more subtle benefit is that the engine can be
chosen at run time—they are pluggable. Since all engine classes derive
from ConsoleEngineBase, the chassis doesn't really care which one is
used. In fact, you could generalize PassthroughChassis to be
configurable at run time. Figure 6 shows the code for a
chassis class that requires an assembly name and class name as
command-line arguments. It uses this information to create an instance
of a particular engine class. It then calls its Main method, effectively
turning over processing to it. For Passthrough, the command would look
like this:
In this case, the PassthroughEngine class is a member of the Engines
namespace and resides in an assembly called Engines.dll (note that the
DLL extension must be omitted from the argument in this example).
C:\> GenericChassis "Engines" "Engines.PassthroughEngine"
using System;
using Engines;
namespace ConsoleApps
{
class GenericChassis
{
[STAThread]
static void Main(string[] args)
{
if(args.Length < 2)
{
Console.Error.WriteLine("Two arguments required:");
Console.Error.WriteLine("assembly and class name.");
return;
}
Engines.ConsoleEngineBase engine = null;
string assemName = args[0];
string className = args[1];
string[] newargs = new string[args.Length - 2];
for(int i = 2; i < args.Length; i++)
{
newargs[i - 2] = args[i];
}
try
{
Runtime.Remoting.ObjectHandle engineHandle =
Activator.CreateInstance(assemName, className);
engine = (ConsoleEngineBase)engineHandle.Unwrap();
}
catch(Exception e)
{
Console.Error.WriteLine(e.Message);
return;
}
engine.Main(newargs);
}
}
}
This
approach—separating the chassis from the engine—is usually called the
Strategy design pattern (though I prefer the car metaphor). It allows an
algorithm to change without having to create an entire subclass of its
associated class. What's so bad about subclassing just to change one
method? Having lots of classes that differ only in their implementation
of a single algorithm can be confusing and hard to maintain. Families of
algorithms are generally thought to be more intuitive than families of
classes. In Figure 5, ConcreteEngine1 and
ConcreteEngine2 are members of this family. Physically, they are
implemented as classes; logically, they exist only to help the chassis
implement its Main method. (This might have been more intuitively
accomplished with delegates, though it would have complicated the use of
the Template Method pattern.)
You
may have noticed that there isn't anything console-specific about these
engine classes. They simply know how to read and write
System.IO.Streams. This is another benefit of this approach—the same
logic that you use from the console can be used from (for example) a
network server class. Just as you can swap out the engine, you can swap
out the chassis, albeit with a little more work.
Even
engines that are more complex than Passthrough are similarly
straightforward. Think back to the Replace example. Replace is similar
to Passthrough except that it performs a regular expressions-based
replacement on each line of input. The complete listing for
ReplaceEngine is shown in Figure 7. Notice how the
PreProcess method sets up a single instance of the
System.Text.RegularExpressions.Regex class and the ProcessLine method
uses it on each line of input. I'll build further on these later in the
article. For now, however, let's look at the other side of console apps:
how to control them from within a .NET Framework-based app.
using System;
using System.Text.RegularExpressions;
namespace Engines
{
public class ReplaceEngine : ConsoleEngineBase
{
private Regex replacer = null;
protected override void PreProcess()
{
this.replacer = new Regex(this.Arguments[0]);
}
protected override void ProcessLine(string line)
{
this.Out.WriteLine(
this.replacer.Replace(line, this.Arguments[1]));
}
protected override string Usage()
{
return "Replace \"findexp\" \"replaceexp\"";
}
protected override bool ValidateArguments()
{
if(this.Arguments.Length == 2)
return true;
return false;
}
}
}
No comments:
Post a Comment