The Globals Communication Pattern
A design pattern to abstract away all communication logic.
- Mathijs Beentjes, 18-5-2020
Motivation
In order to send data from one location to another, no matter what technique you use (like REST, Microsoft Service Bus, ActiveMQ, RabbitMQ, Sockets, OPC-UA, etc.), it always has a learning curve and you will have to deal with a lot of concepts and code. As the action itself is conceptually very simple and always the same –send *some* data from A to B- it suggests some level of abstraction is still missing.
In this paper this last level of abstraction is presented, which is a small layer on top of the existing communication protocols, hiding away all complexity.
An implementation of this pattern is already created, with RabbitMQ as the communication layer and C# implementing this last level of abstraction.
The Path to the Pattern
In the quest for the best communication pattern, we should come with a solution where:
- The number of concepts used is minimized.
- The amount of code to be written is minimized.
We start with visualizing the part “sending some piece of information from one place to another”.
See the example below, written in C#:
The Sender:
string SomeText = "Hello, World!";
// <Communication logic to send the data>
// .
// .
The Receiver:
string SomeText=
// <Communication logic to receive the data>
// .
// .
Console.WriteLine(SomeText);
Receiver output:
Now if we recall the wished properties of our pattern -minimize the number of concepts and the amount of code- it is suddenly clear how the ideal situation should look like: NO extra concepts, NO extra lines of code.
In the ideal case we end with:
The Sender:
string SomeText = "Hello, World!";
The Receiver:
string SomeText;
Console.WriteLine(SomeText);
Receiver output:
Interpretation: In the ideal case, assigning a value to a (special) object should be enough to send that value. The underlying framework should do the rest, abstracting away all complexity. On the receiving side the declaration of an object with the same name (and type) should be enough to let the underlying framework retrieve that value.
In this ideal case only one extra concept is added: same names = same values.
This concept has some similarities with global variables, hence the name ‘Global’ for this pattern. Is it possible to implement this with the current techniques?
Yes it is – with some additions.
Implementation of the Pattern
In our implementation we define a ‘Global’ as the communication class, providing a generic type as the object to be communicated. In addition it should have a name to distinguish the Global from other named Globals. We get:
Global<T> Name = new Global<T>("Name")
{
Value = <An object of type T>
};
On the moment the Value property of the Global is assigned, the Global will send the data to all other existing same named Globals connected to the framework.
In this implementation, you are able to declare, instantiate and send a value in one statement.
On the receiving side, you have to declare a Global of the same type and name, and that declaration should be enough to receive the data. In addition, an event is added that will be triggered every time the value of the underlying T-object of the Global is changed.
In the next section use of this implementation is given.
Using the Pattern
We start with an implementation of the Hello, World! example.
Instead of:
string SomeText = "Hello, World!";
We get:
Global<string> SomeText = new Global<string>("SomeText")
{
Value = "Hello, World!"
};
On the receiving side, we get:
// a declaration plus event handler
Global<string> SomeText =
new Global<string>("SomeText", handler: SomeText_DataChanged);
.
.
.
private static void SomeText_DataChanged(object sender, GlobalEventData<string> e)
{
Console.WriteLine(e.Data); // The data is processed here!
}
As long as the name string, in this case, “SomeText”, is the same for the sender and receiver, then the data is synchronized. Every assignment of the Value property of the global with some text on the sender side, results in a Value change and, if a handler is present, a DataChanged event on the receiving side – in that order
Putting it together
Putting everything together, we get:
The Sender:
using System;
using Globals.NET.RabbitMQ; // the package
namespace Sender
{
class Program
{
static void Main()
{
using Global <string> SomeText = new Global <string> ("SomeText");
// assign a value, and we are done!
SomeText.Value = "Hello, World!";
Console.ReadLine();
Console.WriteLine("Stopping...");
}
}
}
The Receiver:
using System;
using Globals.NET.RabbitMQ; // the package
namespace Receiver
{
class Program
{
static void Main()
{
// A declaration plus event handler
using Global <string> SomeText = new Global <string> ("SomeText", handler: SomText_DataChanged);
Console.ReadLine();
Console.WriteLine("Stopping...");
}
private static void SomeText_DataChanged(object sender, GlobalEventData <string> e)
{
// The data is received here!
Console.WriteLine(e.Data);
}
}
}
The App.Config:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="HostName" value="192.168.178.144"/>
<add key="Port" value="5672"/>
<add key="UserName" value="MrSmith"/>
<add key="Password" value="MsSmith"/>
<add key="VirtualHost" value="/"/>
</appSettings>
</configuration>
In the ‘using’ section, the package Globals.NET.RabbitMQ is added, containing the Globals communication object. In order to make communication via RabbitMQ possible, you should have a RabbitMQ server somewhere running. In addition, 5 RabbitMQ parameters should be set, in this example via the App.Config file.
As Globals are in practice heavy duty objects, they should be disposed by the user. In this example, the C# 8 using statement is implemented, assuring the object is disposed automatically.
Global Visibility
All Globals connecting to the same server exist in the same “Global Universe”. Globals with the same names (and types) get synchronized. If you think this is too global, you can create a separate world in the Global Universe, by adding an extra “WorldName” in the constructor:
using Global <string>SomeText =
new Global <string> (world: "Hello!", "SomeText", handler: SomeText_DataChanged);
On the receiving side, the same worldname should be added in the constructor. If you want to be death sure that you and your pile at other end of the line are the only ones in your world, then you could stringify a Guid and use this as your unique world name.
Summary
The Globals Communication Pattern defines named communication objects, enforcing the rule:
If the names (and type) are the same, then the data is the same.
Assigning a value to a named communication object should be enough to send its value.
Declaring that same named communication object should be enough to receive its value.
The Power of the Pattern
All acts of communication boils down to the basic principle of sending *some* data from one place to another. The Globals pattern is a direct translation of this principle.
The next sections will dive deeper into the current implementation of the Globals Communication Pattern, including some working examples.
Global Lifetime
In the current implementation, Globals have no history and are not persisted. If a variable somewhere in the Globals world get a value, then this value is only bound to this global as long the global exists. When the last global gets out of scope, the data is lost. On that moment there is no Global, and so there is no data. Let’s see how this works in our Hello, World! example:
We first start the Sender:
The Sender has assigned SomeText a Value, and is waiting to get stopped.
Now we start up the Receiver:
As the Sender was still alive, the Globals exists in the world and the Receiver gets this value.
Now we close the Sender, keeping the Receiver alive, and start another Receiver:
As there was one Global still alive, the data existed and was passed to the next receiver as well.
But if we now close all the console applications and then start another receiver:
The Global data was out of scope, so the data disappeared as well, until you start another sender:
And the data is back again. Let’s start another sender:
In this case, the new sender assigns a value, triggering the DataChanged event again of the Receiver. You might argue this event raising for the second time is not a good idea, as the data was not changed in practice. However, if you want to send an array of data, like measurements, by assigning it to the Value property serially, you want every measurement to trigger an event. If the measurement would accidentally have the same value as the previous measurement, you don’t want that measurement to be filtered away. So every new assignment of the Value property of a Global result in a DataChanged event for every same-named global in the global world.
Example: Chat program
The ease of use of Globals will be demonstrated in a real world example: a simplistic console Chat program.
We start with the code:
namespace Chat1
{
class Program
{
private static string _name;
// The Heavy Duty communication object
static Global <string> MyText;
static void Main()
{
Console.Write("Your Name: ");
_name = Console.ReadLine();
using (MyText = new Global <string> ("MyText", handler: MyText_DataChanged) )
{
MyText.Value = _name + " has entered the building";
string txt;
do
{
txt = Console.ReadLine();
// Send text
if (txt == "")
{
MyText.Value = _name + " has left the building";
}
else
{
MyText.Value = _name + " says: " + txt;
}
}
while (!string.IsNullOrEmpty(txt));
}
}
private static void MyText_DataChanged(object sender, GlobalEventData <string> e)
{
if (!e.isInitialValue && !e.fromSelf)
{
Console.WriteLine(e.Data);
}
}
}
}
Walking through the program:
In the main section, first your name is asked and stored in a private string. Then the Global MyText is instantiated and a first value is assigned: “ has entered the building”. Every chat program connected to the same world will receive this text via a DataChanged event and will display it in the console. Now a loop is started, reading the text a user types, and assigns it to MyText: “ says: ”. If you just hit enter, it is interpreted you are leaving, so a last message will be sent: “ has left the building” .
If we look at the MyText_DataChanged handler, we see a check on e.isInitialValue. This flag is set if the initial value is received. That is the current value of MyText, assigned before this instance of the chat program was instantiated. As we are interested only in the value changes after entering the chat room, we blocked the display of the initial value. Another flag is e.fromSelf. If this is true, it was changed by yourself. As we don’t want to see messages from ourselves, we block it as well.
How the chat program works in practice: First I start my chat for the first time:
Now I start a second instance. This time, I’m Mr White:
After pressing enter…
A message is displayed in Mr Blacks console. Now the third Chat will be started: Mr Pink
After entering the name, the other 2 consoles are notified. Now I type a message in Mr Pink’s console:
And the others get notified.
Now I press enter with no text in Mr Pinks console, which is interpreted as leaving:
The chat program of Mr Pink closes and the others get notified. In this example, some properties of the GlobalEventData object were described.
In the next section its complete structure is given.
The GlobalEventData <T> class
The structure is:
public class GlobalEventData <T> : EventArgs
{
public T PrevData;
public T Data;
// true if the value received is initial. That is: the last value of the global,
// assigned some moment in the past.
// false if the value is just changed by another global.
public bool isInitialValue;
// If there are no other globals with a value, then the global is assigned a
//default value.
public bool isDefault;
// true if the change comes from the global itself.
public bool fromSelf;
}
Please note that on the moment a DataChanged event is received, the data is already changed. PrevData contains its previous value, Data contains the new value, reflecting the current value of the Value property.
There are 3 properties defining the state of the data received. The property isDefault states if the received value is a default value. This property is only true if a Global was instantiated while there were no other Globals with that same name having a non-default value. The default value can be passed via a parameter in the constructor, or it can be omitted, using the default value of the underlying type. isInitialValue is true if the value is assigned before this instance of the Global was created. fromSelf is true if the data was changed by the Global itself.
The different states of a Global
A description of the different states a Global goes through after instantiation, will be given here:
On the moment a Global is created, it has no value, not even a default value. The first thing a Global does after instantiation, is find out if there are same named Globals in his universe or world having a non-default value. If this is not the case, the DataChanged event is called, with a default value of type T in its Data field, the flag isDefault is true, and so are isInitialValue and fromSelf. Only from this moment on the Global has a Value.
If there are other same named Globals having a non-default value, then the just created Global requests and receives this value and a DataChanged event is raised containing that existing value. In that case isDefault is false, isInitialValue is true and fromSelf is false.
After the initialization phase, next changes on the data will be received with flags isDefault and isInitialValue set to false.
If we now look back at the Hello World example, we see that the event handler of the receiver omits the check on isDefault. With the result that if the Receiver is started before the sender, being the first named Global in the universe, the DataChanged event is raised containing a default string value. With the result an empty line is displayed. If we look back at the Receiver windows displayed in this example, we do see empty lines on moments a Receiver was started with no other Sender / Receiver programs running.
The DataChanged event handler
In all previous examples, the DataChanged event handler is assigned to the Global via its constructor.
It is possible to assign it later, for example:
using var SomeText = new Global<string>("SomeText");
SomeText.DataChanged +=SomeText_DataChanged;
As the Global starts to look for its data during its instantiation, it is possible that on the moment the event handler is assigned, the initial data is already set: the first event is missed then. The Global’s Value property is set but no notification is received.
To avoid this, It is recommended to assign the event handler in the constructor. In that case no events are missed.
Example: Data Pump
In many situations where communication is involved, it is not one item that should be send over the line, but a continuous array of data.
We start with a simple implementation. In this case an array of simulated plant data is sent.
The Sender:
using System;
using Globals.NET.RabbitMQ;
namespace SendStructuredData0
{
public class PlantData
{
public int ID { get; set; }
public double LA { get; set; }
public int Class { get; set; }
public override string ToString()
{
return $"ID: {ID}, Class: {Class}, LA: {LA}";
}
}
class Program
{
private static Global <PlantData> gPlantData;
static void Main()
{
// Define the PlantData global
using (gPlantData = new Global("PlantData"))
{
// Create a random generator helping to simulate the data
Random r = new Random();
for (int i = 0; i < 100; i++)
{
var pd = new PlantData()
{
ID = i + 1,
Class = (int)(r.NextDouble() * 4),
LA = r.NextDouble() * 100
};
Console.WriteLine("Sending: " + pd);
// The actual sending
gPlantData.Value = pd;
}
}
Console.ReadLine();
}
}
}
Most code here is used to create and generate the PlantData. Just two lines are used to declare and
instantiate the gPlantData Global, one line is used to send the data.
The Receiver:
using System;
using Globals.NET.RabbitMQ;
namespace ReceiveStructuredData0
{
public class PlantData
{
public int ID { get; set;}
public double LA{ get; set;}
public int Class{get; set;}
public override string ToString()
{
return $"ID: {ID}, Class: {Class}, LA: {LA}"; }
}
class Program
{
private static Global gPlantData;
static void Main()
{
// Define the PlantData global
using (gPlantData =
new Global("PlantData", handler: GPlantData_DataChanged))
{
Console.ReadLine();
}
}
private static void GPlantData_DataChanged(object sender,
GlobalEventData<plantdata> e)
{
if (!e.isInitialValue)
{
Console.WriteLine("Received: " + e.Data);
}
}
}
}
If we first start the receiver, and then the sender, everything goes fine:
If we first start the Sender and then the Receiver, nothing seems to happen on the receiver side:
Explanation:
on the moment the Receiver is started, the data was already sent, that is: the gPlantData global already went through its different data changes, ending with only the last PlantData measurement as its value. As the DataReader filters away initial data, even that last value is blocked from display. In order to solve this synchronization issue, we declare the Global Start, both on the Sender- and receiver side. On the Receiver side, a value of true is assigned to the Global, notifying the Sender it can start pumping its data.
The Sender:
using System;
using Globals.NET.RabbitMQ;
namespace SendStructuredData
{
public class PlantData
{
public int ID { get; set; }
public double LA { get; set; }
public int Class { get; set; }
public override string ToString()
{
return $"ID: {ID}, Class: {Class}, LA: {LA}";
}
}
class Program
{
private static Global gPlantData;
static void Main()
{
// Define the PlantData global
using (gPlantData = new Global <plantdata>("PlantData"))
using (var start = new Global <bool>("Start", handler: Start_DataChanged))
{
Console.ReadLine();
}
}
private static void Start_DataChanged(object sender, GlobalEventData <bool> e)
{
if (e.isDefault)
{ // initialization completed.
// No other Globals with the same name yet
return;
}
if (e.Data)
{
// Create a random generator helping to simulate the data
Random r = new Random();
for (int i = 0; i < 100; i++)
{
var pd = new PlantData()
{
ID = i + 1,
Class = (int)(r.NextDouble() * 4),
LA = r.NextDouble() * 100
};
Console.WriteLine("Sending: " + pd);
// The actual sending
gPlantData.Value = pd;
}
}
In the main method we now see the declaration of a Global Start with its event handler. On the moment Start receives a non-default DataChanged event with the value true, the data is sent over the line.
On the Receiver side, we get:
using System;
using Globals.NET.RabbitMQ;
namespace ReceiveStructuredData
{
public class PlantData
{
public int ID { get; set; }
public double LA { get; set; }
public int Class { get; set; }
public override string ToString()
{
return $"ID: {ID}, Class: {Class}, LA: {LA}";
}
}
class Program
{
private static Global <plantdata> gPlantData;
static void Main()
{
// Define the PlantData global
using (gPlantData = new <plantdata> Global("PlantData", handler:
GPlantData_DataChanged))
using (var start = new Global <bool> ("Start"))
{
start.Value = true;
Console.ReadLine();
}
}
private static void GPlantData_DataChanged(object sender,
GlobalEventData e)
{
if (!e.isInitialValue)
{
Console.WriteLine("Received: " + e.Data);
}
}
}
}
Now it does not matter which program is started first, as the Sender waits for the receiver to get online.
So let’s first start the Sender:
Nothing happens, as it did not receive a start event yet.
Now we start the receiver:
Once the Receiver is started, the Sender gets notified and sends its data. It doesn’t matter anymore which program starts first, as long as there is one moment in time both programs are active, then data is sent: The communication has been synchronized.
This works well if we have a one-to-one configuration, but suppose we have one sender and multiple
receivers. If we continue with our previous example, and start a second receiver:
What happens is:
the new Receiver assigns true to start, triggering the DataChanged event of the start on the Sender – please keep in mind that assigning a value to the Value property of a Global will always trigger the DataChanged event, even if you assign a same value. With the result the Sender sends another bunch of 100 records – to all receivers that are listening. What we actually want in a one-to-many configuration, is that every Receiver only gets one bunch of data, and not with every receiver that has come to life. Instead of assigning true to the Global Start variable, we let the receiver send some unique value to the sender and use that unique value to receive the data privately.
Sender:
using System;
using Globals.NET.RabbitMQ;
namespace SendStructuredData
{
public class PlantData
{
public int ID { get; set; }
public double LA { get; set; }
public int Class { get; set; }
public override string ToString()
{
return $"ID: {ID}, Class: {Class}, LA: {LA}";
}
}
class Program
{
private static GlobalWriter <plantdata> gPlantData;
static void Main()
{
// When the receiver is ready to receive, it sends its ID.
using var MyId = new GlobalReader("MyId", handler: MyId_DataChanged);
// And now sit and wait
Console.ReadLine();
}
private static void MyId_DataChanged(object sender, GlobalEventData e)
{
if (e.isDefault)
{
// Initialized, no other Globals with this name yet
return;
}
using (gPlantData = new GlobalWriter <plantdata> (e.Data.ToString()))
{
// Create a random generator helping to simulate the data
Random r = new Random();
for (int i = 0; i < 100; i++)
{
var pd = new PlantData()
{
ID = i + 1,
Class = (int)(r.NextDouble() * 4),
LA = r.NextDouble() * 100
};
Console.WriteLine("Sending: " + pd);
// The actual sending
gPlantData.Value = pd;
}
}
}
}
Receiver:
using System;
using Globals.NET.RabbitMQ;
namespace ReceiveStructuredData
{
public class PlantData
{
public int ID { get; set; }
public double LA { get; set; }
public int Class { get; set; }
public override string ToString()
{
return $"ID: {ID}, Class: {Class}, LA: {LA}";
}
}
class Program
{
private static GlobalReader <plantdata>gPlantData;
static void Main()
{
Guid myId = Guid.NewGuid();
using (gPlantData = new GlobalReader(myId.ToString(),
handler: GPlantData_DataChanged))
using (Global MyId = new Global("MyId"))
{
MyId.Value = myId;
Console.ReadLine();
}
}
private static void GPlantData_DataChanged(object sender,
GlobalEventData e)
{
if (!e.isInitialValue)
{
Console.WriteLine("Received: " + e.Data);
}
}
}
}
The Receiver first creates a Guid myId, then creates a Global with that identifier stringified as the name, and then send this original Guid to the sender via assignment of the myId Global. The sender then creates a Global with the stringified identifier as its name, being the same Global as defined on the receiving side, and use that Global to communicate the data.
Limitations of the current Globals Implementation
There exists some limitations on the use of Globals. In this implementation, the limitations are:
- Not every class can be send by this implementation. Only classes that are Json serializable are supported – as we use this mechanism to serialize and send the data over the internet. In most cases, this won’t be a problem.
- - As we use RabbitMQ as the underlying communication mechanism, the size of the data is limited. A structure of 10Mb is still transferred, but 100Mb will let RabbitMQ crash. In practice this limitation won’t be a problem, as most structures are much smaller.
- Only a direct assignment of the Value property of the Global triggers the sending of data. If you have a complicated class assigned to your Global and you change one of its properties, no data is sent. Only direct assignment of the Value property triggers the sending.
Guaranteed data delivery?
With the current implementation, assigning a value to a Global says nothing about its delivery. You don’t know if data is delivered after assigning a value to that global. In that case some two-way communication has to be created, which can be on many different ways. Below one of the possible solutions is given, where a message is send back to the sender after receiving its data. A one-to-one configuration is assumed here.
Sender:
using System;
using Globals.NET.RabbitMQ;
namespace GuaranteedDeliverySender
{
class Program
{
public class Envellope
{
public Guid ID;
public string Data;
}
static Guid messageId = Guid.NewGuid();
static string data = "Hello there! I sent you a message!";
static void Main(string[] args)
{
using (Global Id = new Global("IdCheck", handler:
Id_DataChanged))
using (Global Message = new Global("message"))
{
Message.Value = new Envellope() { ID = messageId, Data = data };
Console.WriteLine("Just sent my message!");
Console.ReadLine();
}
}
private static void Id_DataChanged(object sender, GlobalEventData e)
{
if (!e.isInitialValue && e.Data == messageId)
{
Console.WriteLine("Message delivery is confirmed!");
}
}
}
}
Receiver:
using System;
using Globals.NET.RabbitMQ;
namespace GuaranteedDeliveryReceiver
{
class Program
{
public class Envellope
{
public Guid ID;
public string Data;
}
static void Main(string[] args)
{
using (Global <Envellope> Message = new Global <Envellope> ("message",
handler: Message_DataChanged))
{
// Waiting for the message until enter is h
Console.ReadLine();
}
}
private static void Message_DataChanged(object sender,
GlobalEventData e)
{
if (!e.isDefault)
{
// Received!
Console.WriteLine(e.Data.Data);
// Confirming...
using (var Id = new Global("IdCheck"))
{
Id.Value = e.Data.ID;
};
}
}
}
}
The consoles:
What happens here: The sender puts its data in another class, the Envellope, containing the data to be send, together with an Id field, in this case a Guid.
The Receiver sends the Id back to the sender after processing. Now the sender is assured (guaranteed) that the data is delivered.
Recommendations
This current implementation of the Globals design pattern uses RabbitMQ as its underlying message broker. However, this implementation of the pattern does not use the full power of RabbitMQ. For example, RabbitMQ has several error signaling and recovery mechanisms, communication patterns like acknowledgements, dead letter queues, etc. I would like to invite RabbitMQ to come with an improved implementation of the Globals design pattern using all the powers of their software.
Like an Acknowledged property and event, which will be set or raised on the moment an acknowledgement is received from the other party. The author could do this himself but in the current implementation of the author extensive knowledge of RabbitMQ -and time- is still missing.
The other vendors of communication software are invited as well to create an implementation of the Globals design pattern using the powers of their software, abstracting away all complex parts, ending with a solution that is strong and complex under water, but death simple in its use. If you accept the invitation, please send me a message at MathijsBeentjes@xs4all.nl.