|
One thing that's been a stumbling block to me in C++ is the tension between compile time polymorphism (or static polymorphism) and runtime polymorphism (or dynamic polymorphism).
Compile time polymorphism is done via templates; the decision about which type to use is decided when the program is compiled. Runtime polymorphism is done via virtual methods. The decision about which type to use is decided as the program is running.
These two approaches seem to be orthagonal(?) to each other.
I was (re)watching a video[^] on YouTube. It's a presentation by Sean Parent of Adobe. He's "a principal scientist at Adobe Systems and engineering manager of the Adobe Software Technology Lab." The entire video is interesting. He talks about generic programming and also designing software components that are connected together to form a directed acyclic graph.
There's a point in the video that has intrigued me ever since I first saw it. There's a bullet point (which unfortunately Sean doesn't elaborate on) that says: "Extend generic programming to apply to runtime polymorphism."
Ah, this seems to be getting at that tension I mentioned early between compile time and runtime polymorphism.
So I did some googling and found an interesting paper[^] describing a design pattern called External Polymorphism. This seems to provide a structure for merging generic programming with runtime polymorphism.
I won't describe the pattern, but I can describe how I applied it to writing VST plugins. The following assumes a basic knowledge of the VST v2.4 SDK, but hopefully you'll be able to follow along without being familiar with it.
Say you have a bunch of objects that you've created on the heap, e.g. you created them in createEffectInstance and then passed them to other objects that interact with them. The lifetime of these objects is the same as that of the plugin. You'd like a reuseable, generic way to dispose of them. Here's an approach using External Polymorphism:
class DisposableInterface
{
public:
virtual ~DisposableInterface()
{
}
};
template<class T>
class Disposable : public DisposableInterface
{
public:
Disposable(T *target)
{
this->target = target;
}
~Disposable()
{
delete target;
}
private:
T *target;
};
Ok, in our base Plugin class we have a method for passing any object that will be garbage collected when the Plugin itself is destroyed:
class PluginBase : public AudioEffectX
{
public:
virtual ~PluginBase()
{
std::list<DisposableInterface *>::iterator it;
for(it = objects.begin(); it != objects.end(); it++)
{
delete *it;
}
}
template<class T>
void AddToGarbageCollector(T *object)
{
objects.push_back(new Disposable<T>(object));
}
};
AudioEffect *createEffectInstance(audioMasterCallback audioMaster)
{
Plugin *plugin = new MyPlugin();
Lfo *lfo = new Lfo();
AdsrEnvelope *env = new AdsrEnvelope();
Oscillator *osc = new Oscillator(lfo, env);
plugin->AddToGarbageCollector(lfo);
plugin->AddToGarbageCollector(env);
plugin->AddtoGarbageCollector(osc);
return plugin;
}
Now, you may never need this kind of memory management, so don't get tripped up on the example. The main point is that we can treat "concrete types" polymorphically. We don't have to make sure that they implement a particular base class.
I'm not recommending using this approach to replace virtual methods in all classes, but I prefer to write my low-level classes without them. And I occasionally need to iterate over a collection of these concrete objects for one reason or another, e.g. updating the sample rate or whatever. As long as they meet the requirements of the algorithm that's being applied to them, e.g. provide a method or operators with specific signatures, this technique can be applied to fascilitate this.
Like...
class PluginBase : public AudioEffectX
{
public:
template<class T>
void AddTempoTarget(T *object)
{
tempoTargets.push_back(new TempoTarget(object));
}
};
Then when the tempo changes, we could do this:
std::list<TempoTargetInterface *>::iterator it;
for(it = tempoTargets.begin(); it != tempoTargets.end(); it++)
{
(*it)->Tempo(newTempo);
}
As long as the target objects have a method called Tempo that takes a double (or whatever) they can be notified when the tempo changes, they don't have to inherit from a specific class.
Anyway, this was a puzzle that had been in the back of my mind ever since I became interested in generic programming, and especially after seeing that bullet point in Sean's video. Just something else to add to my bag of tricks.
|
|
|
|
|
Generic programming has become a bit of an obsession of mine lately. I've been studying this website:
Generic Programming[^]
And I've been intrigued by applying generic programming techiniques to DSP programming. I've posted about this in the past with a generic example of a delay line as well as a small parameter handling toolkit.
A key concept of generic programming is called Lifting[^]. It's where you examine a concrete algorithm and look for ways to make it generic. For example, here is a concrete copy algorithm:
void Copy(float *first, float *last, float *out)
{
while(first != last)
{
*out = *first;
first++;
out++;
}
}
Fair enough. But what if we wanted a copy algorithm for integers? We'd have to do this:
void Copy(int *first, int *last, int *out)
{
while(first != last)
{
*out = *first;
first++;
out++;
}
}
In examining the two algorithms we see they are exactly the same except for the type. So we can "lift" the algorithm up by making it type independent:
template<typename InputIterator, typename OutputIterator>
void Copy(InputIterator *first,
InputIterator *last,
OutputIterator *out)
{
while(first != last)
{
*out = *first;
first++;
out++;
}
}
Now we have a Copy function that can work on any type provided that the iterators meet the requirement of the algorithm. This is an important point. It's usually impossible to lift an algorithm to the point that it is so generic it can work on anything. There are usually some requirements. The above algorithm requires that the InputIterator support the postfix increment operator as well as the * operator for reading its value. The OutputIterator must also support the postfix increment operator and the * operator for writing to its value.
So how about applying this to DSP? Can we "lift" some of the algorithms used in DSP and VST programming?
Take a basic sine oscillator function:
float SineOscillator(float *first,
float *last,
float phase,
float increment)
{
while(first != last)
{
phase += increment;
while(phase > PI2)
{
phase -= PI2;
}
*first = sin(phase);
first++;
}
return phase;
}
What can we do to "lift" this algorithm?
We can make it type independent for one. That's easy enough. But it would also be cool to make it waveform independent as well. And it would also be nice to pack up the phase and increment values into one object instead of passing them in seperately.
So applying the above, we come up with this:
template<typename OutputIterator,
typename Oscillator,
typename Waveform>
Oscillator Oscillate(OutputIterator first,
OutputIterator last,
Oscillator osc,
Waveform wave)
{
while(first != last)
{
osc.phase += osc.increment;
while(osc.phase >= osc.length)
{
osc.phase -= osc.length;
}
*first = wave(osc.phase);
first++;
}
return osc;
}
The algorithm has several requirements. Oscillator must have a phase, increment, and length values. Waveform must provide a () operator that takes the current phase and returns a result.
One challenge with this kind of approach is maintaining state after each function call. If we call the above function like this:
Oscillate(&buffer[0], &buffer[BUFFER_SIZE], myOsc, sin);
Both the oscillator and waveform objects are passed in by value. We can update our oscillator object by assigning the return value to it:
myOsc = Oscillate(&buffer[0],
&buffer[BUFFER_SIZE],
myOsc,
sin);
That's fine but may not be very efficient if the oscillator is very large.
Instead we can pass the oscillator object in as a reference so whatever state changes that take place within the function persist afterwards:
Oscillate<float *, MyOscillator &, Sine>(&buffer[0],
&buffer[BUFFER_SIZE],
myOsc,
Sine());
If we need to persist changes in state to our waveform, we can use the same technique:
Oscillate<float *, MyOscillator &, Sine &>(&buffer[0],
&buffer[BUFFER_SIZE],
myOsc,
mySine);
This just scratches the surface. But the idea is to abstract algorithms to make them generic so that they can be reused in many different contexts.
modified on Sunday, July 20, 2008 9:14 PM
|
|
|
|
|
Just stopped by to say you a hi.
It is a crappy thing, but it's life -^ Carlo Pallini
|
|
|
|
|
Rajesh R Subramanian wrote: Just stopped by to say you a hi.
Hello!
Thanks for stopping by. 
|
|
|
|
|
Many times a method needs to perform a series of steps. The problem is that one of these steps can fail. Usually, this results in an exception being thrown from the source of the failure. When this happens, it's important that the object whose method is being invoked is left in a valid state.
Here is an example of the problem:
public class MyClass
{
private SomeObject obj;
private int a;
private int b;
public MyClass(SomeObject obj)
{
#region Require
if(obj == null)
{
throw new ArgumentNullException("obj");
}
#endregion
this.obj = obj;
this.a = obj.CalculateSomething();
this.b = obj.CalculateSomethingElse();
}
public void DoSomething()
{
a = obj.CalculateSomething();
b = obj.CalculateSomethingElse();
}
}
Now in the above example, suppose that the call in DoSomething to the CalculateSomething method succeeds but that the call following it to CalculateSomethingElse fails and an exception is thrown. Also suppose that a and b should always be updated together. If one is updated successfully and the other is not, the object is no longer in a valid state. It's not hard to think of "real" examples of this type of situation, e.g. updating values in an object by loading data from a file (suppose a read operation fails?), but here we'll just have to assume this is the case.
A way to circumvent this is to cache the results from each step temporarily and update the object at the end of the method. At this point, all of the steps have been completed successfully and we can update our object while ensuring that its invariants are maintained.
public class MyClass
{
private SomeObject obj;
private int a;
private int b;
public MyClass(SomeObject obj)
{
#region Require
if(obj == null)
{
throw new ArgumentNullException("obj");
}
#endregion
this.obj = obj;
this.a = obj.CalculateSomething();
this.b = obj.CalculateSomethingElse();
}
public void DoSomething()
{
int tempA = obj.CalculateSomething();
int tempB = obj.CalculateSomethingElse();
a = tempA;
b = tempB;
}
}
In the second example, the calls to CalculateSomething or CalcualteSomethingElse can fail without our object being left in an invalid state.
When it's necessary to perform many steps in a method, caching the results can get complicated. My current approach is view this as an opportunity to refactor. What I do is take the values that need updating and put them into a seperate class. The previous example then looks like this:
public class RefactoredValues
{
private int a;
private int b;
public RefactoredValues(SomeObject obj)
{
#region Require
if(obj == null)
{
throw new ArgumentNullException("obj");
}
#endregion
this.a = obj.CalculateSomething();
this.b = obj.CalculateSomethingElse();
}
public int A
{
get
{
return a;
}
}
public int B
{
get
{
return b;
}
}
}
public class MyClass
{
SomeObject obj;
RefactoredValues v;
public MyClass(SomeObject obj)
{
#region Require
if(obj == null)
{
throw new ArgumentNullException("obj");
}
#endregion
this.obj = obj;
v = new RefactoredValues(obj);
}
public void DoSomething()
{
v = new RefactoredValues(obj);
}
}
Any time DoSomething is called, it creates a new object representing the updated values. If an exception is thrown, the MyClass 's values are left intact.
-- modified at 14:09 Wednesday 27th June, 2007
|
|
|
|
|
This is a post[^] I made to the Lounge. I wanted to give a basic overview of the concept of messaging in object oriented programming. Specifically, I wanted to show how anonymous methods help facilitate passing one message from one object to another in a decoupled way. This is similar to my "Anonymous Methods as Glue"[^] entry.
The post got a little long; I really enjoy talking about these concepts. So I thought I would repost it here:
From a theoretical point of view, invoking a method on an object can be considered sending it a message:
SomeObject obj = new SomeObject();
obj.DoSomething();
The challenge becomes implementing this in such a way as to make it extensible and flexible to change. Polymorphism helps by lowering the coupling between objects:
public interface ISomeInterface
{
void DoSomething();
}
ISomeInterface obj = new SomeObject();
obj.DoSomething();
Now we can substitute types that implement ISomeInterface and it won't break the rest of our code. Just basic polymorphism.
It would be nice to have objects communicate peer-to-peer through a more explicit messaging system:
public interface ISink
{
void Send(Message msg);
}
Then we can set up relationships between objects when they are created and allow them to send messages directly to each other via the ISink interface:
ISink obj1 = new SomeObject();
AnotherObject obj2 = new AnotherObject(obj1);
obj2.Go();
obj2 can send messages directly to obj1 and does so through the ISink interface.
The problem with this approach is that we have to straightjacket objects into the ISink interface in order to allow our messaging system to work. We can use .NET events to decouple objects even further and use anonymous methods as a means of facilitating communication. Take your typical .NET class that raises an event:
public class ClassWithEvents
{
public event EventHandler SomethingHappened;
}
Take a class with a method that returns void and takes no parameters:
public class SomeObject
{
public void DoSomething()
{
}
}
Anonymous methods glue the objects together allowing them to communicate:
ClassWithEvents obj1 = new ClassWithEvents();
SomeObject obj2 = new SomeObject();
ob1.SomethingHappened += delegate(object sender, EventArgs e)
{
ob2.DoSomething();
};
The objects are completely decoupled. The anonymous method provides a way for events raised by one object to be delegated to another object. Things can get fancier if our anonymous method adapts the event raised by one object by transforming the message into something the receiving object can understand.
From my point of view, since taking up C#, messaging has become a ubiquitous to my programming. And I think it's improved as a result.
|
|
|
|
|
Consider an oscillator in your standard analog synthesizer. It cycles through its waveform endlessly. Let's say you want to model this using C#. Why not use the IEnumerator interface? The MoveNext method moves the oscillator to the next sample of the waveform. The Reset method resets the oscillator to the beginning of the cycle of its waveform. And the Current property represents the current output of the oscillator.
In addition to implementing the IEnumerator interface, an Oscillator class could implement methods allowing you to modulate it's amplitude and frequency; it could have ModulateAmplitude and ModulateFrequency methods that take a double value.
In the case of ModulateAmplitude , the Oscillator would multiply the specified value to its current amplitude. In the case of ModulateFrequency , the Oscillator would add the specified value to its current index into the waveform buffer. When MoveNext is called, it calculates its current value based on the modulated amplitude value and modulated frequency value.
Since this is an oscillator, MoveNext would never return false. However, we could model other synth components using the IEnumerator interface, such as an ADSR envelope. It this case, MoveNext would eventually return false when the envelope has reached the end of its release segment.
I've written code to implement the above descriptions, and I've liked the results. Hopefully, I'll post an article with an accompanying demo project sometime before the end of the year.
|
|
|
|
|
I've posted a lot here about flow-based programming. I've studied this from a lot of different viewpoints: Messages can flow between object synchronously or asynchronously. Messages can be pushed from one object to another or pulled to one object from another.
For my MIDI toolkit, I settled on a synchronous push architecture using a Source/Sink metaphor. Delegates representing a method in the Sink are connected to a Source. When the Source creates a message, it pushes the message to all Sinks connected to it.
This is fine, but the implementation of this approach has felt to me to be non-standard. If you were looking at my libraries for the first time, it might not be obvious what's going on. So I've been thinking of going back to designing my classes in a more traditional .NET framework style.
What this means is that the Source infrastructure is done away with. In its place are .NET style events. So instead of a Source pushing messages to its Sinks, it simply raises an event. Sources look just like any other .NET class that raises an event.
But this leaves the problem of how to connect objects together. With the Source infrastructure no longer in place, how can sources and sinks be connected?
The answer is anonymous methods. Anonymous methods provide a glue that sticks objects together. Let me give you an example:
public delegate void MessageReceivedEventHandler(object sender, MessageReceivedEventArgs e);
public class Device
{
public event MessageReceivedEventHandler MessageReceived;
}
public class Client
{
public void Send(Message msg)
{
}
}
public class Application
{
public static void Main()
{
Device dev = new Device();
Client c = new Client();
dev.MessageReceived += delegate(object sender, MessageReceivedEventArgs e)
{
c.Send(e.Message);
};
}
}
The Application class creates instances of Device and Client and creates
an anonymous method that delegate events raised by Device to Client. The
anonymous method acts as glue connecting the two objects together.
I think this is a form of the Adapter pattern from GoF, but someone pointed out to me on comp.object that it may be the Mediator pattern instead. Regardless, anonymous methods provide a clean way to connect objects together. The beauty is that the classes are completely decoupled. There's no need to provide any special infrastructure for connecting objects; anonymous methods do it for us.
|
|
|
|
|
In my last post I definied an IChannelSink interface. What I have since found useful is to distinquish between sinks and sources. A link in a chain of objects can be a source of messages, a sink for messages, or both. So...
public interface IChannelSink
{
void ProcessMessage(IChannelMessage message);
}
And...
public interface IChannelSource
{
IChannelSink ChannelSink
{
get;
set;
}
}
An IChannelSource contains the next sink in the chain. Typically, the first link in a chain of objects only implements the IChannelSource interface. And the last object only implements the IChannelSink interface. The intermediate objects implement both.
|
|
|
|
|
Well, I've been all over the place on how to implement flow-based programming in C#. I've discovered several ways of doing it, a couple of which you can read about in my previous blog entry.
What I've settled on is an approach inspired by the System.Runtime.Remoting.Channels namespace. This namespace uses the concept of sinks and sink chains. You have a chain of objects that process messages. Well, that's very much like what I'm trying to accomplish with my MIDI toolkit!
So I've created a set of sink interfaces, one for each message type. Classes that implement these interfaces can be placed in a chain of objects which process MIDI messages. For example, take the IChannelSink from the MIDI toolkit:
public interface IChannelSink
{
void ProcessMessage(ChannelMessage message);
IChannelSink NextChannelSink
{
get;
set;
}
}
This represents the basic functionality for an object to serve in a sink chain. An example of chaining objects together:
InputDevice inDevice = new InputDevice(0);
UserSink userSink = new UserSink();
ChannelStopper stopper = new ChannelStopper();
OutputDevice outDevice = new OutputDevice(0);
inDevice.ChannelSink = userSink;
userSink.NextChannelSink = stopper;
stopper.NextChannelSink = outDevice;
inDevice.StartRecording();
The chain begins with an InputDevice class. This class doesn't actually implement the IChannelSink interface since it is the first link in the chain. It simply receives messages and does not process them.
The second object in the chain is a hypothetical user defined sink. This sink processes messages in a user defined way.
The ChannelStopper sink keeps track of any sounding notes. When the flow of messages stop, the stopper's AllSoundsOff method can be called to stop any sounding notes so that they are not left hanging.
And finally, the output device serves as the last link in the chain. It simply sends the messages to an output device.
So after much consideration, I think I've settled on the way in which I want to implement flow-based programming in C#.
BTW, this implementation looks a lot like the Chain of Responsibility design pattern. The main difference is that this approach is about processing messages rather than handling requests, but they are very similar.
|
|
|
|
|
In writing my MIDI toolkit, one of the things I've had to deal with is handling the flow of MIDI messages throughout my system. MIDI messages arrive at an input device or are read from a MIDI file track and then flow through the system until reaching their final destination, an output device. How should I structure this? What approach will be the most flexible and extendable?
Researching this problem led me to J. Paul Morrison's[^] website on flow-based programming. He has written a book on the subject, and it is available on his website as well as on Amazon[^].
I've found his writings fascinating and just what I needed to approach the problem of (re)designing my MIDI toolkit.
I'm not going to say a lot about flow-based programming itself; for that, please refer to Mr. Morrison's book. I'll just say that it's simply about using components to handle the flow of information throughout a system. That's an oversimplification, but it's enough to get us started. I should also provide a disclaimer that what follows is my take on how flow-based programming works and can be implemented in C#. In other words, I'm mixing in my own terms and ideas, so don't judge the merits of flow-based programming just on this blog entry alone. Read the book.
Ok, how does one implement flow-based programming in C#?
There are several ways, and the first thing we should talk about are sources and sinks. A source is a component that is a source of a message, data packet, whatever (I'll refer to the objects that flow through a system from here on out as messages). A sink is a component capable of receiving a message. A component can be a both a source and a sink.
Let's create interfaces for sinks and source for a mythical "channel message":
public interface IChannelSource
{
event EventHandler<ChannelEventArgs> ChannelMessageOccurred;
}
This interface defines functionality for a source of channel messages. The channel message data is encapsulated in a ChannelEventArgs class. When a class that implements this interface receives, reads, generates, etc. a channel message, it will raise the ChannelMessageOccurred event.
Next, let's create a sink for channel messages:
public interface IChannelSink
{
void Connect(IChannelSource source);
void Disconnect(IChannelSource source);
}
This interface defines functionality for connecting to and disconnecting from an IChannelSource . When an IChannelSink is connected to an IChannelSource it receives channel message events from the IChannelSource .
We can use generics to make our interfaces more reusable:
public interface ISource<T> where T : EventArgs
{
event EventHandler<T> MessageOccurred;
}
public interface ISink<T> where T : EventArgs
{
void Connect(ISource<T> source);
void Disconnect(ISource<T> source);
}
A component can be the source of several kinds of messages, and a sink can be capable of receiving several kinds of messages as well. This means that if you have a component that is the source of several kinds of messages, it will need to implement the ISource interface more than once, which means that each implementation after the first one will need to be explicit. This may obfuscate your code more than you'd like, and you may want to bypass using generics in this way and stick to having a seperate interface for each message type.
A class implementing the IChannelSink interface would implement the Connect and Disconnect methods as follows:
public void Connect(IChannelSource source)
{
source.ChannelMessageOccurred += new EventHandler<ChannelEventArgs>(HandleChannelMessage);
}
public void Disconnect(IChannelSource source)
{
source.ChannelMessageOccurred -= new EventHandler<ChannelEventArgs>(HandleChannelMessage);
}
If our IChannelSink class also implements IDisposable , we may want to keep track of the IChannelSource s connected to it so that the class can disconnect from the sources when it is disposed:
public void Connect(IChannelSource source)
{
if(sources.Contains(source))
{
return;
}
source.ChannelMessageOccurred += new EventHandler<ChannelEventArgs>(HandleChannelMessage);
sources.Add(source);
}
public void Disconnect(IChannelSource source)
{
source.ChannelMessageOccurred -= new EventHandler<ChannelEventArgs>(HandleChannelMessage);
sources.Remove(source);
}
public void Dispose()
{
foreach(IChannelSource source in sources)
{
source.ChannelMessageOccurred -= new EventHandler<ChannelEventArgs>(HandleChannelMessage);
}
}
This extra infrastructure automates disconnecting from sources when a sink is being disposed. Sources do not have to be explicitely disconnected by a third party.
Earlier, we created source and sink interfaces for channel messages. Assume that we also have source and sink interfaces for several other kinds of messages, e.g. Meta, SysEx, SysRealtime (those of you familiar with MIDI will recoginize these message names. If you aren't familiar with MIDI, don't worry about it. The important point is that there are several kinds of messages).
What we're doing with the above interfaces is using events to fascilitate flow-based programming. The beauty of this approach is that several sinks can be connected to the same source. And each sink can do something different with the message it receives.
For example, imagine a MIDI application in which notes are received by an input device. This input device is a source of note messages. A component capable of transposing the notes it receives up or down in pitch is connected to the input device.
After transposing the note messages, it passes the altered message along to the next sink. This sink could be an output device or yet another component capable of transforming note messages in some other way. At the same time, our output device could be connected to the input device so that it also receives the note message as well. Thus the original note as well as the transposed note are mixed together at the output device.
It's important to note (no pun intended) that messages should be immutable. You don't want one component altering the original message and that alteration affecting unrelated components that receive the same message object. Each time a component alters a message, it's not changing the original message but creating a new message that represents the altered message.
A Simpler Way
Well, there is a more straightforward way to achieve flow-based programming using delegates and events. You simply make a classes event handlers public. Say you have one class that has an event:
public class SomeClass
{
public event EventHandler SomethingOccurred;
}
And another class capable of handling the event:
public class AnotherClass
{
public void Send(object sender, EventArgs e)
{
}
}
And you can connect the two like this:
SomeClass sc = new SomeClass();
AnotherClass ac = new AnotherClass();
sc.SomethingOccurred += as.Send;
The advantage to this approach is that there is no need for source or sink interfaces. There is less coupling with this approach. As long as the method matches the required delegate type for the event, they can be connected.
There are two disadvantages:
One, you have the event handler public. This may look strange to clients. An event handler doesn't look like a normal method. So there should be some understanding of the purpose behind making the event handler public.
As an aside, this is one of those instances in which I really dislike the .NET Framework event convention. I'd rather bypass the convention and have the methods look normal without the "object sender, EventArgs e" noise. You're mileage may vary.
Two, if the class receiving the event implements IDisposable , extra care should be taken to disconnect the class from the event before it is disposed so that when the event is raised, it's event handler is not called. Basically, the class responsible for connecting the two classes should be responsible for disposing of the classes and disconnecting them.
This simpler approach is the one I'm now leaning towards in implementing flow-based programming
-- modified at 2:05 Sunday 15th January, 2006
|
|
|
|
|
I've been using the Visitor, Observer, and Iterator design patterns together recently with some satisfying results. I'll go through step by step how I'm combining these three patterns.
First, we have various types of classes that implement an interface. These are the classes that accepts a visitor:
public interface IMessage
{
void Accept(IMessageVisitor);
}
public class MessageA : IMessage
{
public void Accept(IMessageVisitor visitor)
{
visitor.Visit(this);
}
}
public class MessageA : IMessage
{
public void Accept(IMessageVisitor visitor)
{
visitor.Visit(this);
}
}
This assumes the existence of an IMessageVisitor interface, so let's declare it next:
public interface IMessageVisitor
{
void Visit(MessageA message);
void Visit(MessageB message);
}
The basic interfaces and classes are in place to implement the Visitor design pattern. Now for combining Visitor with Observer. We will create a MessageDispatcher class that implements the IMessageVisitor interface. It will provide functionality for raising events for each of the message classes it visits:
public class MessageDispatcher : IMessageVisitor
{
public event EventHandler MessageAOccurred;
public event EventHandler MessageBOccurred;
public void Visit(MessageA message)
{
OnMessageAOccurred();
}
public void Visit(MessageB message)
{
OnMessageBOccurred();
}
protected virtual void OnMessageAOccurred()
{
EventHandler handler = MessageAOccurred;
if(handler != null)
{
handler(this, EventArgs.Empty);
}
}
protected virtual void OnMessageBOccurred()
{
EventHandler handler = MessageBOccurred;
if(handler != null)
{
handler(this, EventArgs.Empty);
}
}
}
The purpose of the MessageDispatch class is simple; it raises events in response to visiting message objects.
The beauty of this class is that it can be reused in many different contexts. In fact, it may turn out that this is the only visitor class we need. Instead of creating many implementations of the IMessageVisitor interface, we can use this class instead and register with the events to be notified when the visitor visits a message type we are interested in. I'll build on this idea in another post when I explore flow-based programming.
Before I show the MessageDispatch class in action, let's now combine Visitor/Observer with Iterator. I'll use C# v2.0 iterators.
Say we have a class that has a collection of IMessage objects. We'll give it an iterator for iterating over the collection, returning the index of each object in the collection, and visiting each object in the collection:
public class MessageCollection
{
private List<IMessage> messages = new List<IMessage>();
public IEnumerable<int> Iterator(IMessageVisitor visitor)
{
int index = 0;
foreach(IMessage message in messages)
{
yield return index;
message.Accept(visitor);
index++;
}
}
}
Here, all we're doing is returning the collection index of each message before visiting the message. It isn't very impressive, but this is a very generic and simple example. In other situations, what your iterator returns can be just about anything, some sort of on the fly calculation, or whatever. Anything that is relevant to the traversal. And you can create several interators for your class that return different values and have different traversal strategies.
The position of the yield return statement is important and requires some thought. When the yield return is executed, the iterator returns. Viewed from the outside, this return causes the MoveNext call to complete. The rest of the code that comes after the yield return is not executed until MoveNext is called again. When used with Visitor, this is important to consider.
If you need to use the value stored in the Current property at the conclusion of a MoveNext before you visit the current object, it's important to put the yield return before the object is visited; otherwise, you may want to visit the object first.
C# Iterators are a little hard to reason about because of the jump that can happen in the middle of the iterator's code. In fact, you can have several yield returns throughout the iterator. It becomes tricky, and one is reminded of gotos, but at least so far, I'm finding iterators to be very powerful.
Now let's look at all of this in action:
public class Program
{
private MessageCollection messages = new MessageCollection();
private MessageDispatcher dispatcher = new MessageDispatcher();
public Program()
{
dispatcher.MessageAOccurred += EventHandler(HandleMessageA);
}
private void HandleMessageA(object sender, EventArgs e)
{
}
public void ProcessMessages()
{
foreach(int index in messages.Iterator(dispatcher))
{
}
}
}
This admittedly doesn't look impressive. But one advantage that's apparent even from this simple example is that our Program class doesn't have to implement the IMessageVisitor class to visit the messages. The MessageDispatcher does all of the work.
Using events in this way may seem like overkill, and if it ended with our Program class, I would agree. The real power comes in when more than one class responds to the events generated by the Visitor/Observer class. You can design classes to respond to the events raised by the Visitor/Observer class that has no knowledge of the mechanics going on behind the scenes. All they are interested in is doing something interesting with the objects being visited.
To give a more concrete example, I'm using the above approach with my MIDI toolkit. I've rewritten the playback engine to use Visitor/Observer/Iterator. Various compenents are connected to my Visitor/Observer class to process and react to MIDI events. For example, I have a clock class that responds to tempo events raised by the Visitor/Observer by changing its tempo. Also, the iterators are driven by the ticks the clock generates. It's all working well so far and has made my code more expressive.
Well, that's about it. I may return to this idea and use the above as the basis for an article at some point. That's for your time.
|
|
|
|
|