Borland Delphi Informant Magazine Home 1stClass Components


Member Services
FREE Trial Issue
New Subscription
Renew Subscription
Delphi CD-ROM
Report Problems
Change of Address


Delphi Informant
Features
Case Studies
News
New Products
Book Reviews
Product Reviews
Opinion
Back Issues

Downloads
Article files
Third-party files
Upload a File

Informant
ICG News
Contact Us
Advertise with Us
Write for Us
The power of SQL combined with navigational speed and control
Delphi Informant Complete Works
 
The power of SQL combined with navigational speed and control

Readers Choice
Awards 2000


 •

The Builder Pattern : Extending Frameworks - Building Add-in Packages


 •

The Template Method Pattern : Building a UI Application Framework


 •

The Singleton Pattern : Implementing a Reusable Object for Saving Persistent Data



 •

Run-time ActiveX


 •

Scriptable Plug-Ins


 •

Add Scripting to Your Apps


 •

Request Threads


 •

A Multimedia Assembly Line: Part II





Tell a friend
about this article!




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):

  • Mediator defines an abstract class or interface for communication between the Colleague classes.
  • A ConcreteMediator implements the abstract mediator. Each Concrete mediator coordinates the Colleague communication.
  • The Colleague is an abstract class or interface that defines the class to be managed by the Mediator class.
  • ConcreteColleague classes are self-contained classes that communicate with a Mediator class. Knowledge of the mediator class is typical but not necessary.


Figure 1: The Mediator pattern (from Design Patterns, Gamma, et al.).

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.


Figure 2: Mediator pattern implementation.

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).


Figure 3: Design without Mediator is inflexible.

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.


Figure 4: Design with Mediator is very flexible.

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:

  • Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, et al. [Addison-Wesley, 1995].
  • The Design Patterns Smalltalk Companion by Sherman R. Alpert, et al. [Addison-Wesley, 1998].
  • Patterns in Java, Volume 1 by Mark Grand [John Wiley & Sons, 1998].

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/.

Microsoft Internet Explorer

Top of page
 
1stClass Components

Informant Communications Group

Informant Communications Group, Inc.
10519 E. Stockton Blvd., Suite 100
Elk Grove, CA 95624-9703
Phone: (916) 686-6610 • Fax: (916) 686-8497

Copyright © 1999 Informant Communications Group. All Rights Reserved. • Site Use Agreement • Send feedback to the Webmaster • Important information about privacy