![]() |
![]() |
![]()
| ![]() |
Patterns in Practice Design Patterns / Mediator / Delphi 4, 5
Mediator Pattern: Part I Introduction to Process Control Frameworks
In March of 1999, I began a series of articles on design patterns and how to use such patterns within the Delphi VCL framework. In those articles, I discussed the Singleton, Template Method, and Builder patterns.
In this article, we'll use the Mediator pattern to illustrate how to solve the problem of process flow control in a batch processing system. I initially planned for this entire example to appear in one article - until I realized that there was too much information. Therefore, the next three articles will focus on this pattern and how to use it in a distributed process control system.
Let's begin by defining the pattern in detail. As stated in the classic, Design Patterns [Addison-Wesley, 1995], by Erich Gamma, et al., the Mediator pattern is used to "Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently."
The Mediator pattern is used when objects must communicate with one another in well defined ways, but the communication is complex. The Mediator pattern can simplify the communication between objects. Additionally, the Mediator pattern can also unbind the dependencies between objects. This facilitates object reuse and customizations. There are four participants to the Mediator pattern (see Figure 1):
Figure 2 shows how the Mediator pattern works when implemented. The ConcreteMediator serves as "traffic cop," controlling the execution of, and communication between, the ConcreteColleague objects. In this scenario, ConcreteColleague objects don't communicate directly with each other; rather they rely on the Mediator to enforce inter-object communication. This requires a standard form of communication as defined by the abstract class definitions or interfaces.
Uses and Motivation The Mediator pattern removes the complexities and dependencies of inter-object communication. This objective is especially applicable to process control. Processes by definition are subject to change due to constant improvements in the way we design and handle information. Planning and designing for changes in a process reduce the pain of making modifications.
Any repetitive process we see in business today can benefit from a system design that incorporates the Mediator pattern. Before we delve into the technicalities of an example implementation, however, let's address the motivation of including the Mediator pattern in the design.
Applications focused on managing processes involve the systematic execution of tasks and reporting of task status. If each task were responsible for knowing subsequent tasks and passing behavioral statistics to those tasks, it's easy to see how the resulting application would be constrained to the initial definition of the process, i.e. the sequence of tasks necessary to complete the process. Changes in the sequence of tasks and/or the passing of behavioral parameters between tasks, in this instance, would require updates to all objects involved - even if the individual task behavior had not changed (see Figure 3).
In Figure 3, you see that if we were to modify the input and/or output parameters of any given task, the changes to the system would impact more than just the changed task. Additionally, this design restricts the execution of tasks to the specified sequence. It becomes difficult for the results of one task to determine the next task to be implemented. A task would have to know about all tasks that it might invoke. This violates the intent to loosely couple each task. The Mediator pattern addresses these issues.
Adding a Mediator between the task objects removes the task-to-task dependencies. The application is now free to alter the sequence of tasks without modifying any of the individual tasks. Additionally, this design can allow generic parameters (process modifiers, performance statistics, error conditions, etc.) of one task to be passed to tasks that aren't necessarily next in sequence. Figure 4 illustrates how this might look; notice the similarities with Figure 2.
Task Control Example This article's example illustrates a simplified architecture by which you can control a series of tasks/processes. To reduce any possible confusion, "task" refers to an encapsulated unit of work, and "process" refers to a series of tasks coordinated to achieve a defined purpose
The example we'll present is generic for now; we'll expand on its capabilities in later articles. The intent is simply to illustrate how the Mediator controls the invocation of tasks, and how tasks can be invoked non-sequentially.
Defining the ITask Interface ITask defines an interface with a single function, ExecuteTask (see Figure 5).
unit IntfTask;
interface
type ITask = interface ['{712185C1-810F-11D3-8117-00008638E5EA}'] function ExecuteTask(AInParams: OleVariant): OleVariant; end;
implementation
end. Figure 5: The ITask interface.
This function takes an OleVariant as a parameter and returns an OleVariant. The reason is due largely to how the interface will be used in a distributed environment. In my initial design of the process control system, I had hard-coded parameters to exactly those needed by the specific tasks. This led to problems when a task's parameters required modification, especially when that task has already been deployed on other machines. I needed a way to re-implement a task and to re-deploy it without having to unregister/register the task on a given machine. Also, I did not want to have to re-compile the calling module that also passed in hard-coded parameters. In a later article, we'll see how to use a TClientDataset to implement parameters for each task.
Defining the Mediator Interface IProcess defines two methods, ExecuteProcess and MessageToProcess (see Figure 6). Implementations of IProcess will serve as the Mediator objects.
unit IntfProcess;
interface
type IProcess = interface ['{ 712185C3-810F-11D3-8117-00008638E5EA }'] procedure MessageToProcess(AMessage: string); function ExecuteProcess(AInParams: OleVariant): OleVariant; end;
implementation
end. Figure 6: The IProcess interface.
MessageToProcess is a method that will be used by each implementation of ITask to allow a message to be passed back to the Mediator class of a given ITask implementation. ExecuteProcess is similar to ExecuteTask in that it's invoked by the client of the process.
Implementing ITask Figure 7 illustrates the implementation of the ITask interface.
unit TaskClass;
interface
uses IntfProcess, IntfTask;
type TTask = class(TInterfacedObject, ITask) protected FMediator: IProcess; public function ExecuteTask(AInParams: OleVariant): OleVariant; virtual; abstract; constructor Create(AMediator: IProcess); end;
implementation
constructor TTask.Create(AMediator: IProcess); begin inherited Create; FMediator := AMediator; end;
end. Figure 7: ITask implementation TTask.
As you can see, TTask is implemented as an abstract class. This has two primary purposes. First, we want to propagate the requirement for descendant classes to implement the ExecuteTask method. Second, we want to provide a mechanism by which the TTask descendants would know about, or have a reference to, their Mediator object. This is done through the TTask's constructor.
You'll also notice that we've made TTask a descendant of TInterfacedObject so the IUnknown methods are implemented. IUnknown is the root definition from which all interfaces descend. TInterfacedObject is a class that implements IUnknown's reference counting methods, so your classes don't have to.
The IProcess Class Figure 8 illustrates the implementation of IProcess as an abstract class.
unit ProcessClass;
interface
uses IntfProcess;
type TProcess = class(TInterfacedObject, IProcess) public function ExecuteProcess(AInParams: OleVariant): OleVariant; virtual; abstract; procedure MessageToProcess(AMessage: string); virtual; abstract; end;
implementation
end. Figure 8: Implementing IProcess with TProcess.
TProcess descends from TInterfacedObject for the same reasons mentioned for TTask. TProcess implements the IProcess interface, and defines TProcess as an abstract class. Again, we want to force implementations of ExecuteProcess and MessageToProcess.
The Concrete TProcess Implementation Figure 9 illustrates a concrete implementation of the TProcess abstract class.
unit DemoProcess;
interface
uses Classes, ProcessClass;
type TDemoProcess = class(TProcess) private FMessageStrings: TStrings; public function ExecuteProcess(AInParams: OleVariant): OleVariant; override; procedure MessageToProcess(AMessage: string); override; constructor Create(AMessageStrings: TStrings); end;
implementation
uses IntfTask, Task1, Task2, Task3, Task4;
constructor TDemoProcess.Create(AMessageStrings: TStrings); begin FMessageStrings := AMessageStrings; end;
function TDemoProcess.ExecuteProcess( AInParams: OleVariant): OleVariant; var Task: ITask; i: Integer; InParam: Integer; begin Randomize; InParam := AInParams; for i := 1 to 10 do begin case InParam of 1: Task := TTask1.Create(Self); 2: Task := TTask2.Create(Self); 3: Task := TTask3.Create(Self); 4: Task := TTask4.Create(Self); else Task := TTask3.Create(Self); end; InParam := Task.ExecuteTask(InParam); end; end;
procedure TDemoProcess.MessageToProcess(AMessage: string); begin FMessageStrings.Add(AMessage); end;
end. Figure 9: TDemoProcess, the concrete TProcess implementation.
As mentioned earlier, we want to illustrate how the Mediator pattern can be used to invoke tasks, and how it can do so in a non-sequential fashion. This simulates a scenario where a task can specify the next task to get executed in a sequence. As this series progresses, we'll expand on this design to allow for asynchronous invocation of tasks by the Mediator object.
TDemoProcess is a simple mediator class that contains a reference to a TStrings object and adds strings to those objects in the MessageToProcess method. Therefore, TTask objects can communicate to the TDemoProcess through the MessageToProcess method. The reference to FMessageStrings, the TStrings instance, is set up in the constructor for TDemoProcess.
TDemoProcess.ExecuteProcess creates and executes four different implementations of the TTask class. We'll use a randomly generated return value from each TTask implementation to determine the next TTask implementation to invoke in the sequence. We do this 10 times before leaving the procedure. This effectively illustrates non-sequential invocation of TTask objects.
The Concrete TTask Implementation Figure 10 illustrates one of five implementations of the TTask abstract class.
unit Task1;
interface
uses TaskClass;
type TTask1 = class(TTask) function ExecuteTask(AInParams: OleVariant): OleVariant; override; end;
implementation
{ Process will get executed here. This would consist of reading the parameters from AInParams, using them and then creating the output params which are passed back as Result. } function TTask1.ExecuteTask(AInParams: OleVariant): OleVariant; begin FMediator.MessageToProcess( 'Executing TTask1.ExecuteTask'); Result := Random(5); end;
end. Figure 10: TTask1, an implementation of the TTask abstract class.
The implementation of TTask is simple; it first passes a message back to its Mediator through the MessageToProcess method, then passes back a random number from 0 to 4. As shown in Figure 9, TDemoProcess uses this randomly generated result to determine the next TTask implementation to invoke.
This implementation of the Mediator pattern is simple, yet illustrative as an expandable model for a Mediator pattern. We've created a simple set-up where a process (TDemoProcess) can create and invoke tasks in a non-sequential fashion by using the resulting values from each task to determine the next task to invoke. This is a very simple implementation of a much more complex set-up, where the process determines which task to implement based on status values contained in a database server.
Conclusion The Mediator pattern is an ideal approach to any system that requires some form of process control, and where extensibility and loosely coupled classes are essential. In the next article in this series, we'll show how to use another pattern to enhance the Mediator capabilities shown here. We'll illustrate how to allow a variable number of parameters to be passed to each task and still maintain loose coupling between each task and between tasks and their Mediator object.
Many thanks to John Wilcher for his feedback and help with this article. A Consulting Manager for Inprise Corp., John provided his experience and some of the initial content for this article. He also provides architectural and design consulting services as a Principal Consultant for Inprise PSO. You can write John at mailto:jwilcher@inprise.com, and visit Inprise's PSO Web site at http://www.inprise.com/services.
References I use these books whenever considering applying a design pattern to a given problem. They are a must for any developer serious about learning and using design patterns:
Xavier Pacheco is the president and chief consultant of Xapware Technologies Inc., where he provides consulting services and training. He is also the co-author of Delphi 5 Developer's Guide published by SAMS Publishing. You can write Xavier at mailto:xavier@xapware.com, or visit http://www.xapware.com/.
|
![]() |
|