Introduction
It's always fascinating to find a proper name for a thing, event, or some abstraction. Sometimes, words come first as formulas of ideas, and we then start looking for a way to materialize them. Design patterns metaphor is the way you organize the world around you (analysis), and simultaneously - the way you create new worlds (synthesis), words create worlds.
One of the most misleading and hidden things about class factories is how to articulate the idea behind creating class instances by means of some other class objects. The only reason for that should be a better control over these new instances through all of their life cycles (how many instances may be created, security restrictions, objects' ownership, etc.), and a better decoupling of the components implementation from the components consumer.
Another important consideration about the component model is to provide the possibility to get the current status information back from the component instance, running one of its methods asynchronously. Paraphrasing Alice: what is the use of a component without asynchronous callbacks?
In this article, we'll rather talk about constructs, we'll call them component factories, the name better explaining the essence of decoupling of the application architecture levels.
Decomposition and Design
We are going to present the 3-tier model:
- master application or consumer;
- middle tier implementing our components' creational strategy;
- business logic tier containing different component (plug-ins) declarations.
All of the implementation tiers will reside in different assemblies.
The first thing we'll do is to describe a plug-in interface. We choose the IPlugin
name just to indicate our component-oriented approach.
At this point, we'll provide a means to make both synchronous and asynchronous plug-in method calls. There are only two methods on the interface which will start the business logic routines: Do
and DoAsync
, performing synchronous and asynchronous calls, respectively. They both have one single argument - a string, containing the call parameters in any representation, e.g., in XML. This argument string is being parsed then, business classes are being instantiated, and the corresponding methods are being called.
In other words, the plug-in serves as a redirector, parsing requests and streaming data to the corresponding business modules. The plug-in itself, together with the business modules, will make up the business logic tier.
To introduce asynchronous calls, we need to invent a way to inform the application about the status of business logic calculations. We'll start asynchronous operations on a new thread. Whenever we need to inform a consumer about anything important, we'll fire an event from the plug-in, which should be caught in the consumer application. We'll apply event wiring based on the plug-in metadata, discovered through the Reflection API.
Now, we are ready to design a component factory with some creational strategy in mind. We will make this class factory an integral part of the interface library. This will decouple well, the business logic and the abstract middle tier, responsible for business logic creation and control, this way shaping out our own full-scale component model. We'll define the CreateObjectDirect
method responsible for loading of a plug-in assembly and invoking of the static Create
method of the test plug-in class, which will actually instantiate a new instance of the plug-in (extremely late binding). The CreateObjectDirect
method will also wire up events originating in the plug-in to the callback method in the consumer application.
Proof of Concept
Our solution will consist of three projects containing the program modules listed below: ComponentFactoryTest
(ComponentFactoryTest.cs) - contains consumer source code; PluginInterface
(PluginInterface.cs) - interface, event class, event delegate; ComponentFactory.cs - component factory logic; TestPluginImplementation
(TestPlugin.cs) - plugin redirector; TestBusinessLogic.cs - sample business module without user interface; TestBusinessForm.cs - sample Windows Forms based business module.
The IPlugin
interface is fairly simple:
public interface IPlugin
{
int Mode{ get; set; }
void Setup(string p_init);
string Do( string p_XMLRequest );
string DoAsync( string p_XMLRequest );
bool IsEventInitialized();
string GetProductDetails();
void FireEvent(CEventArgs p_event);
event ProviderEventDelegate FireProviderEvent;
}
In the class factory implementation, the CreateObjectDirect
method serves actually as a liaison between the consumer and the plug-in. It really makes sense to display the code:
public Object CreateObjectDirect(string p_Assembly,
string p_ClassName, string p_InitData)
{
IPlugin plugin = null;
try
{
Assembly assembly = Assembly.LoadFrom(p_Assembly);
Type tPlugin = assembly.GetType(p_ClassName);
if(null == tPlugin)
{
string info = "GetType error for " + p_ClassName;
Console.WriteLine(info);
return null;
}
object[] arguments = new object [] {p_InitData};
plugin = (IPlugin)tPlugin.InvokeMember ("Create",
BindingFlags.Default | BindingFlags.InvokeMethod,
null,
null,
arguments);
if(plugin.IsEventInitialized())
{
return plugin;
}
Delegate eventDelegate =
Delegate.CreateDelegate(typeof(ProviderEventDelegate),
m_that,
"On_ProviderEvent");
plugin.FireProviderEvent += (ProviderEventDelegate)eventDelegate;
}
catch(Exception e)
{
string info = ">> Exception in CreateObjectDirect : " + e.Message;
Console.WriteLine(info);
}
return plugin;
}
From a consumer perspective, the calling sequence includes the steps below:
- Define a callback method to capture the events from the plug-in;
- Create component factory;
- Get an interface to a new instance of the plug-in with the
CreateObjectDirect
call, passing arguments referencing the plug-in assembly dynamic library;
- Call
Do
or DoAsync
on this interface providing parameters necessary to invoke business logic methods;
- Receive status information from asynchronous calls via the callback method defined.
This is a sequence diagram, sketching the consumer, component factory, and the plug-in interaction.
Please note that we run the Execute
method on a business module instance on a dedicated thread.
The test application instantiates business logic plug-ins of two types - with and without a user interface. Both plug-ins simulate some business logic behavior by generating events. To fire events from the user interface-based plug-in, just press a button. These events are being caught in the consumer application, which prints out events' details. That's it!
Implementation Details and Considerations
To make the IPLugin
interface accessible for both consumers and business logic modules, we shall declare it in a separate library, which we shall reference in all of our projects.
We will not sign the plug-in assembly with a strong name. We'll rather put the assembly into the same folder with the test application executable. Don't forget to do that if you want to change the implementation code.
In our approach, we will not pay much attention to the actual call parameter manipulations. We'll make things as clear as possible, implementing string transfers between the client and the component code. We will not even bother if it is an XML string. Any semantics of parameter string formatting may be reasonable and convenient. This approach has its own pros and contras, and might really be effective in a number of application scenarios.
Conclusion
We discussed a lightweight .NET component based framework, which will help us to develop n-tier, conveniently decoupled applications. This design provides enough flexibility for future enhancements and advancements. From the evolution perspective, we may start thinking about a container, holding newly created plug-in instances, adding reference counting to the IPlugin
interface, providing remote access to the container, serializing the input/output data, etc.
Sounds familiar, doesn't it?
History
- 05/01/2006 - Thanks for the comments submitted. The
IPlugin
interface now includes a FireProviderEvent
definition, which improved the CreateObjectDirect
code. Added the CreateObjectWithActivator
method, just to illustrate other possibilities. Note that this function call requires the plug-in to have a public constructor, which may change the creational logic design.