Introduction
First of all, this is my first article and I am in a quandary. I wrote this article with the hope to give vision to beginners who wants to expand their horizon in architectural styles. This style is first proposed by a team from the Institute for Software Research at the University of California, Irvine. They have a framework for it too which is implemented with Java. I just want to remind it to old coders and instruct for beginners. I tried to implemented it with C#, they are not exactly the same, but seems enough to make some demonstration.
For more details, please see:
Background
C2 is an asynchronous, event-based architectural style, which promotes reuse, dynamism, and flexibility through limited visibility. Components and connectors have a defined top and bottom that cause them to be arranged in layers. Components are aware of elements that reside above them but not below. Hence, they may send requests, events that travel up an architecture, with an expectation that they will be fulfilled by some set of components above. Components may also send out notifications, messages that travel down an architecture, without any expectation of whether they will be handled.
As I mentioned, a component within this architecture has limited
visibility or “substrate independence”; components are assembled in a layered
manner, and a component is entirely unknowledgeable of components that exist
“beneath” it. This independence has obvious possibilities for promoting the
interchangeability and reuse of components across architectures.
Components request services from components “above“ it via message passing, and are not in possession of knowledge of components “below” it (in this architecture, messages are passed “up”, meaning the interface layer is considered to be closer to the “bottom” and the “application” layer is considered to be closer to the top. Request flow upwards and message or notifications flow downwards). The C2 style is characterized as an association of components linked by communication forwarders known as connectors.
Benefits
The benefits of this architectural style according to the Institute for Software Research include:
Separation of Concerns, the concept that architectural design should be separate from its implementation, arranging and decomposing software into more manageable and understandable pieces; Open Architectures that encourage a modular strategy where there is a clear separation of module design from the implementation mechanisms through which that design materializes.
Scalability, understanding operational constraints and the support of multiple levels of component interface granularity.
Extensibility, the substitution of components sharing the same interface, limiting component interdependence.
Flexibility, the modification of systems architecture by incorporating additional components or reconfiguring existing components prior to or during execution.
Reliability, leveraging existing components that have been carefully designed, implemented, and verified; Cost Reduction, the reduction in the development effort through component reuse and architectural guidance.
Understandability, increasing the comprehensibility of complex systems through the use of high level models.
C2 is well suited for use in a distributed system. As there is no assumption of shared memory or address space, which allows composition of these components in a highly distributed, heterogeneous environment. Components interact via connectors, the components themselves do not need to reside on the same physical machine or network. Services may be employed using a complex chain of mediators and multiple distributed servers, distributing the processes. The C2 style provides for the development of distributed, dynamic applications by focusing on the structured handling of connectors to achieve layer independence.
Using the Code
Java class framework for C2 uses a Vector of Port objects to facilitate communication between instances of Component and Connector, another option could be using an event bases, implicit invocation model. With C#, it would be possible to maintain the integrity of the architecture event model based on the use of multicast delegates . Components could then register
its methods by subscribing to the events.
I decided to implement it like they did, seems easier to me, maybe I will change it later.
The Signal
class is the base class for other types of messages.
Each message has a name, a type, and parameters. In C2, the type is either Request or Notification. Parameters are named and may be of any type.
The base class for messaging structure is Signal
class;
public class Signal
{
...
protected string NameOfMessage;
protected Hashtable @params;
protected string type;
public virtual void AddParameter(string name, object value)
{
@params[name] = value;
}
public virtual object GetParameter(string name)
{
return @params[name];
}
public virtual void RemoveParameter(string name)
{
@params.Remove(name);
}
public virtual bool HasParameter(string name)
{
return @params.ContainsKey(name);
}
}
And we have two message classes derived from signal class;
Notification
; messages that travel down an architecture:
public sealed class Notification : Signal
{
public Notification()
: base(TypeNotification)
{
}
public Notification(string name)
: base(TypeNotification, name)
{
}
}
Request
; messages that travel up an architecture:
public sealed class Request : Signal
{
public Request()
: base(TypeRequest)
{
}
public Request(string name)
: base(TypeRequest, name)
{
}
public Notification Reply()
{
return Reply(MessageName());
}
public Notification Reply(string name)
{
var n = new Notification(name);
object token = GetParameter("REPLY_TOKEN");
if (null != token)
{
n.AddParameter("REPLY_TOKEN", token);
}
return n;
}
}
Other classes are Brick
and Bus
, I derived another class named BrickGUI
to support applications with Graphical User Interface. Brick
class is base type for components, and Bus
class stands for connectors. All these classes implements IBrick
interface.
public interface IBrick
{
void Send(Signal message);
void Handle(Signal message);
void AddTop(IBrick component);
void Weld(IBrick component);
}
Implementation of Brick
class:
public abstract class Brick : IBrick
{
readonly List<IBrick> _bottomBusList;
readonly List<IBrick> _topBusList;
protected Brick()
{
_bottomBusList = new List<IBrick>();
_topBusList = new List<IBrick>();
}
protected Brick(string name)
: this()
{
BrickName = name;
}
public string BrickName { get; set; }
public void Weld(IBrick bus)
{
if (bus is Bus)
{
bus.AddTop(this);
_bottomBusList.Add(bus);
}
}
public void AddTop(IBrick bus)
{
if (bus is Bus)
{
_topBusList.Add(bus);
}
}
public void Send(Signal message)
{
new Thread((() =>
{
if (message is Request)
{
_topBusList.ForEach(p => p.Handle(message));
}
else if (message is Notification)
{
_bottomBusList.ForEach(p => p.Handle(message));
}
})).Start();
}
public void Handle(Signal message)
{
DoEvents(message);
Send(message);
}
public abstract void DoEvents(Signal message);
}
and Bus
class:
public class Bus : IBrick
{
private readonly List<IBrick> _bottomLinks;
private readonly List<IBrick> _topLinks;
public Bus()
{
_bottomLinks = new List<IBrick>();
_topLinks = new List<IBrick>();
}
public Bus(string name) : this()
{
Name = name;
}
public string Name { get; set; }
public void Weld(IBrick component)
{
_bottomLinks.Add(component);
component.AddTop(this);
}
public void AddTop(IBrick component)
{
_topLinks.Add(component);
}
public void Send(Signal message)
{
new Thread((() =>
{
if (message is Request)
{
_topLinks.ForEach(p => p.Handle(message));
}
else if (message is Notification)
{
_bottomLinks.ForEach(p => p.Handle(message));
}
})).Start();
}
public void Handle(Signal message)
{
Send(message);
}
}
Now let's see it in action.
Simple Application
This is a simple application of C2 architecture, in here we have one clock, two devices, one monitor and two buses. Our goal is synchronizing two devices with the help of clock and monitoring value of Device 2.
Let's first start with Clock
:
public class Clock : Brick
{
public Clock()
{
Timer tmr = new Timer {Interval = 1000};
tmr.Tick += tmr_Tick;
tmr.Start();
}
void tmr_Tick(object sender, EventArgs e)
{
Signal sig = new Notification("clock");
this.Send(sig);
}
protected Clock(string name)
: base(name)
{
}
public override void DoEvents(Signal message)
{
}
}
We can leave DoEvents
method blank, because Clock
does not expect any message. A simple timer is created in ctor
of Clock
, it pushes Notification message with "clock
" tag once in a second. Then Bus1
takes this message and pushes to Device1
& Device2
. Let's see their implementations:
public partial class Device1 : BrickForm
{
public Device1()
{
InitializeComponent();
}
public Device1(string name)
: base(name)
{
InitializeComponent();
Text = name;
}
public override void DoEvents(Signal message)
{
if (message is Notification && message.MessageName().Equals("clock"))
{
if (dateTimePicker1.InvokeRequired)
{
dateTimePicker1.BeginInvoke(new MethodInvoker(() =>
{
dateTimePicker1.Value = dateTimePicker1.Value.AddSeconds(1);
}));
}
else
{
dateTimePicker1.Value = dateTimePicker1.Value.AddSeconds(1);
}
}
}
}
Device1
simply checks incoming signal and increase its value if message is clock notification.
public partial class Device2 : BrickForm
{
public Device2()
{
InitializeComponent();
}
public Device2(string name)
: base(name)
{
InitializeComponent();
Text = name;
}
public override void DoEvents(Signal message)
{
if (message is Notification && message.MessageName().Equals("clock"))
{
if (progressBar1.InvokeRequired)
{
progressBar1.BeginInvoke(new MethodInvoker(() =>
{
progressBar1.Value++;
}));
}
else
{
progressBar1.Value++;
}
Signal n = new Notification("device2state");
n.AddParameter("percentage", progressBar1.Value);
Send(n);
}
}
}
Device2
increases its progress percentage with clock
signal, then pushes this value with Notification
message.
And Monitor
is printing grabbed values to screen:
public partial class FormMonitor : BrickForm
{
public override void DoEvents(Signal message)
{
if (message is Notification &&
message.MessageName().Equals("device2state") &&
message.HasParameter("percentage"))
{
if (richTextBox1.InvokeRequired)
{
richTextBox1.BeginInvoke(new MethodInvoker(() =>
richTextBox1.AppendText("Device 2 state : "+
message.GetParameter("percentage").ToString() +"% \r\n")));
}
else
{
richTextBox1.AppendText(message.GetParameter("percentage").ToString());
}
}
}
}
Last part is binding them together:
private void CreateComponents()
{
Bus bus1 = new Bus("bus1");
Bus bus2 = new Bus("bus2");
var clock = new Clock();
BrickForm device1 = new Device1("device 1")
{
Location = new Point(200, 200)
};
BrickForm device2 = new Device2("device 2")
{
Location = new Point(300, 200)
};
BrickForm monitor = new FormMonitor("monitor")
{
Location = new Point(100, 200)
};
clock.Weld(bus1);
bus1.Weld(device1);
bus1.Weld(device2);
device2.Weld(bus2);
bus2.Weld(monitor);
monitor.Show();
device1.Show();
device2.Show();
}
Result will become something like this, components are aware of elements that reside above them. Closing monitor will not effect workflow of devices or clock.
Little Bit Complicated Sample
I made this sample to visualize how things work under the hood.
In this scenario, FormMain
class sends a Request
message with name of target and text parameter (remember, bottom is aware of top), message is distributed as far as it can go. If message is delivered, a notification message is sent by target, main prints incoming notification.
I have to create just two components; one FormMain
and one Banner
class. Later, I will create 5 instances from Banner
.
public partial class FormMain : BrickForm
{
...
public override void DoEvents(Signal message)
{
if (message.HasParameter("dest") && message.HasParameter("text"))
{
AddToLog(String.Format("{0} is set to \"{1}\"",
message.GetParameter("dest"), message.GetParameter("text")));
}
}
public void AddToLog(string text)
{
lock (richTextBox1)
{
richTextBox1.Clear();
richTextBox1.AppendText(text);
richTextBox1.AppendText(Environment.NewLine);
}
}
private void buttonSendMessage_Click(object sender, EventArgs e)
{
Application.OpenForms.OfType<IBrick>().OfType<Form>().ToList().ForEach
(p => p.BackColor = SystemColors.Control);
Signal sig = new Request(textBoxTarget.Text);
sig.AddParameter("text",textBoxMessage.Text);
this.Send(sig);
}
}
public partial class Banner : BrickForm
{
#region ctor
public Banner()
{
InitializeComponent();
}
public Banner(string name)
: base(name)
{
InitializeComponent();
Text = name;
}
public override void DoEvents(Signal message)
{
if (message is Request)
{
this.BackColor = Color.Yellow;
if (message.MessageName() == this.BrickName)
{
if (message.HasParameter("text"))
textBox1.Text = message.GetParameter("text").ToString();
this.BackColor = Color.Green;
Thread.Sleep(1000);
Signal n = (message as Request).Reply("main");
n.AddParameter("dest", this.BrickName);
n.AddParameter("text", message.GetParameter("text"));
this.Send(n);
}
else
{
Thread.Sleep(1000);
this.BackColor = SystemColors.Control;
}
}
else
{
this.BackColor = Color.Yellow;
Thread.Sleep(1000);
this.BackColor = SystemColors.Control;
}
}
}
I added some Thread.Sleep
and some coloring to be able to visualize what's happening during communication.
And last step, creating & welding components:
private void CreateForms()
{
BrickForm t1 = new Banner("t1")
{
Location = new Point(200, 200)
};
BrickForm t2 = new Banner("t2")
{
Location = new Point(400, 200)
};
BrickForm t3 = new Banner("t3")
{
Location = new Point(600, 150)
};
BrickForm t4 = new Banner("t4")
{
Location = new Point(600, 250)
};
BrickForm t5 = new Banner("t5")
{
Location = new Point(800, 200)
};
BrickForm main = new FormMain();
var bus1 = new Bus("Bus1");
var bus2 = new Bus("Bus2");
var bus3 = new Bus("Bus3");
var bus4 = new Bus("Bus4");
t5.Weld(bus4);
bus4.Weld(t3);
bus4.Weld(t4);
t3.Weld(bus3);
t4.Weld(bus3);
bus3.Weld(t2);
t2.Weld(bus2);
bus2.Weld(t1);
t1.Weld(bus1);
bus1.Weld(main);
main.Show();
t1.Show();
t2.Show();
t3.Show();
t4.Show();
t5.Show();
}
Request is propagating:
Target is found:
Points of Interest
You can check this article for further thought.
Conclusion
Separation of concerns is an important benefit of architecture. The concept that architectural design should be separate from its implementation, arranging and decomposing software into more manageable and understandable pieces. But keep in mind that each component means extra threads for program and decreases overall performance of system. C2 is well suited for use in a distributed system as there is no assumption of shared memory or address space, which allows composition of these components in a highly distributed, heterogeneous environment.
History
- 10.05.2014 - Initial version