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