Find my posts on IT strategy, enterprise architecture, and digital transformation at ArchitectElevator.com.
My second talk at TechEd Europe was a rapid journey through call-stack semantics, coupling, composability, and visualizations, with a bit of test-driven development, inversion of control, and Resharper mixed in. Since I had a large audience and got decent feedback ratings (aside from the comment that I tend to look like I am running for a train...) I figured it might be worth sharing some of the talk as a blog post. The talk is obviously inspired by some of my earlier postings, namely Good Composers are Far and Few in Between, Look Ma -- No Middleware! , and Visualizing Dependencies so I won't go into a ton of detail on those topics but invite you to (re)read these earlier posts. Also, since this talk was part of a Microsoft conference all code examples are in C#. However, I won't dwell on C# language features. Everything I show here would look very similar in Java (except for the stupid "I" in front of the interface names).
The call stack is one of these things that we rarely think about (except when we get a stack trace) but that actually governs the way we think and write software in a fairly significant way. Most high-level programming languages like Java, C#, C++, VB are based on a stack model. Even declarative languages like XSLT support a call-stack model through elements like call-template (even though this can lead to more pain than gain). The call stack actually provides a few critical functions for us:
OK, so you might feel like you are having back flashs from CompSci 101. Why is this interesting to professional developers? It becomes interesting when you use a programming model that does not have a built-in call stack. Particularly, because all the assumptions that you became used to making are no longer given. This can give you quite a headache.
For example, a messaging model does not support a call stack. When I send a request message and receive a response message at a later time, none of the mechanisms mentioned above are at work. There is no coordination unless I put in an active wait loop. The message does actually not come back to me unless I specifically supply a Return Address. Lastly, I do not have my local state when I receive the response message unless I use a Correlation Identifier or include all necessary state in the message.
Some messaging approaches are trying to put the call stack back into the model but this is generally a bad idea.
Another key ingredient into event-driven architectures is the notion of composability. Composability is the ability to build new things from existing pieces. This can occur because the individual pieces make fewer assumptions about other components and therefore do not presuppose the overall structure of the system. This is one of the key benefits of loose coupling: the fewer assumptions I make about the components I interact with the easier it is to change the other components. Composability is a great benefit because it promotes reuse and adaptability. It is also great during testing -- I might want to swap out another component for a test or mock component during testing. If my system is composable, that is very easy to do.
A key aspect of composability is that something must perform the composition. We can distinguish there between explicit composition and implicit composition. Explicit composition is one where a special Assembler wires two (or more) components together. Implicit composition takes place when one
component decides by itself (or is configure to) publish data to Channel X
and another component is setup to receive data from Channel X
. Because both components independently decided to work with the same channel they
will exchange data without knowing the other party. Because there is no Assembler
needed, we refer to this approach as implicit composition.
Event-driven architectures are those that expose a top-layer structure that is not dependent on a call stack and is highly composable. The high-level components communicate solely through events that are passed through event channels. For example, one component might receive orders via a Web site or a Web service. Once the component did some basic validation it publishes a message "Order Received". It is completely oblivious to which other components listen and react to this event. It also does not presuppose what actions should happen next. This is very different from a method call where I specifically instruct another component to perform a function of my choice.
I mentioned "top-layer structure" because each individual component is most likely constructed using regular, call-stack oriented techniques. This is just fine because the inside of a component benefits from the much richer interaction model that a method call gives us. It would be a huge pain to have to correlate and synchronize for every method call! Also, the tighter coupling inside a component is a non-issue because typically one person or one team develops and controls all pieces of the component.
Alright, let's go ahead and build some of the core pieces to build an EDA. The first important piece we need are the event channels, i.e. the pieces that allow us to publish events and subscribe to them. We'll build those from scratch here. In order to focus on architectural trade-offs instead of language features I intentionally keep things very simple. This is partly just practicality (call it laziness if you wish) but that aside it is still a good idea to keep your channels simple. The channel behavior and the events are the key coupling point between components. The more complex these things are the more implicit coupling occurs between the components.
What does a very simple channel have to do? It has to allow on component to send message and another one to receive them. Going back to basics here, the interface can look like this:
public delegate void OnMsgEvent(XmlDocument msg); public interface IMessageReceiver { void Subscribe(OnMsgEvent handler); } public interface IMessageSender { void Send(XmlDocument msg); } public interface IChannel : IMessageSender, IMessageReceiver {}
It is common wisdom that a good design is not the one where nothing can be added but rather one where nothing can be taking away. It seems like we are pretty close to that design goal here :-) We have a method to send an event in form of an XML document and a method to signify our interest in receiving all events coming in on this channel. This is done through delegates, making for a Event-driven Consumer (note that an Event-driven consumer itself has little to do with Event-driven architectures -- one is a microscoping implementation decision inside an endpoint and the other is an architectural style).
So far so good, all we need to do now is implement this interface. That should not be too hard given that all we have is two methods. But not so fast, there are a few more things we need to decide on...
The IChannel
interface merely specifies the syntax of our channel interface. The name and parameters
of our methods suggest some semantics, i.e. a it is fairly likely that a method named
send
that takes an XmlDocument
does in fact send a message. But this assumption leaves quite a bit to be desired
in terms of precision. Let's look at a few more details that I would like to know
about a channel:
Send
method does not return anything we might still be interested to know whether the
call blocks until the receiving end has executed the function or not. This is critical
when multiple messages are sent and we are tempted to make timing assumptions across
messages.
In some of these cases you might be inclined to argue that a component should not have to know how the channel behaves in detail. This is true as long as the component does not make any implicit assumptions that might be fulfilled by some channels and not others. For example, if a component that receives a message modifies that message it makes an implicit assumption that it received its own copy of the message. This will work for distibuted systems but may not work for a simple local channel that simply sends a copy of the same message.
Because the interface itself does not say much about the expected behavior of the channel we need to find a better way to express what we decided. The best way to do that is code -- test code. So let's whoop up some nUnit test cases to specify the behavior we discussed. First, we want to make sure that basic composition works. We also want to verify that our channel is a Publish-subscribe Channel and that it sends copies of the inbound messages. This makes for three test cases:
The code for the first test looks like this:
[Test] public void SequentialComposition() { DebugChannel inChannel = new DebugChannel(new StraightThroughChannel()); DebugChannel connChannel = new DebugChannel(new StraightThroughChannel()); DebugChannel outChannel = new DebugChannel(new StraightThroughChannel()); new TraceFilter(inChannel, connChannel, "filter1"); new TraceFilter(connChannel, outChannel, "filter2"); inChannel.Send(doc); Assert.AreEqual("filter1", outChannel.LastMessage.DocumentElement.ChildNodes[0].Name); Assert.AreEqual("filter2", outChannel.LastMessage.DocumentElement.ChildNodes[1].Name); }
This code uses two helper classes. One is the DebugChannel
. This channel is a Decorator around our simple channel that allows us to inspect the last message that was sent.
The other one is the TraceFilter
, a filter that appends its name to any message that passes through. The filter takes
an input and an output channel in its constructor along with the name of the channel.
The test explicitly composes two filters to that the message passes through them sequentially
(see figure). At the end it verifies that the message passed through filter1
first and through filter2
last. By the way, the doc
variable has been initialized by the [SetUp]
method that nUnit calls before every test case.
In order to verify that we have a Pub-Sub Channel we need to hook two subscribers to the same channel. We do this again with two TraceFilter
s. This time we verify that each output event has passed through exactly on filter
and has been tagged by that filter:
[Test] public void ChannelSendsMessageToAllSubscribers() { DebugChannel inChannel = new DebugChannel(new StraightThroughChannel()); DebugChannel out1Channel = new DebugChannel(new StraightThroughChannel()); DebugChannel out2Channel = new DebugChannel(new StraightThroughChannel()); new TraceFilter(inChannel, out1Channel, "filter1"); new TraceFilter(inChannel, out2Channel, "filter2"); inChannel.Send(doc); Assert.AreEqual(1, out1Channel.LastMessage.DocumentElement.ChildNodes.Count); Assert.AreEqual("filter1", out1Channel.LastMessage.DocumentElement.ChildNodes[0].Name); Assert.AreEqual(1, out2Channel.LastMessage.DocumentElement.ChildNodes.Count); Assert.AreEqual("filter2", out2Channel.LastMessage.DocumentElement.ChildNodes[0].Name); }
Lastly, we want to make sure the channel delivers copies of the original message instead
of references to the same in-memory instance. We can do this by creating another filter,
the ManglingFilter
. A ManglingFilter
modifies the message it receives from the channel. Our test makes sure that we can
send a message to a ManglingFilter
without our message object being harmed:
[Test] public void ChannelMakesCopyOfMessage() { StraightThroughChannel inChannel = new StraightThroughChannel(); StraightThroughChannel outChannel = new StraightThroughChannel(); new ManglingFilter(inChannel, outChannel); inChannel.Send(doc); Assert.AreEqual(0, doc.DocumentElement.ChildNodes.Count); }
Now we are ready to implement a channel that supports all these test cases. The code is surprisingly simple because we use a local, i.e. distributable, channel:
public class StraightThroughChannel : IChannel { OnMsgEvent subscriber; public StraightThroughChannel() { subscriber = new OnMsgEvent(NullEvent); } public void Subscribe (OnMsgEvent handler) { subscriber += handler; } public virtual void Send (XmlDocument msg) { subscriber((XmlDocument)msg.Clone()); } private void NullEvent(XmlDocument doc) {} }
Note the call to Clone
that ensures we deliver a copy of the message that passed to the Send
method. Actually, this implementation has a minor flaw. If multiple subscribers subscribe
to the StraightThroughChannel
, they each receive a copy of the original message. However, they receive the same copy. So if one subscriber mangles the message, other subscribers will see the change.
If this is undesired, we have to replace the elegant one-liner delegate invocation
with a foreach
statement.
The complete solution does not contain a lot of code but still a little more that
I showed here. There is a SimpleFilter
base class that does nothing but simply forward a message, I created an asynchronous
channel and a Null Channel that acts like /dev/null
for messages. You can download the Visual Studio solution here. Note that this solution requires nUnit 2.2.
Composability is a major ingredient into making system adaptable and promoting reuse. As simple as introducing a channel between two components may look, the devil is often in the detailed behavior. Defining a simple channel interface and specifying its behavior using coded unit tests is an important starting point to creating a composable architecture.
Our simple example used explicit conposition using code. One could easily imagine using a configuration file (more on configuration) or the new Visual Studio 2005 Software Factories.