Introduction
It is some times useful for objects in an application to collaborate and share information without maintaining direct references to each other (an example might be an application with plugins, some of which want to communicate with each other). So let's assume we have multiple objects, some contributing certain data, and others consuming different subsets of that data. Instead of tightly coupling data-producer and data-consumer objects by having them maintain references to each other, we can instead opt for a more decoupled approach, namely, creating a "blackboard" object which allows objects to freely read and write data to/from it. This decouples the producer and consumer objects by allowing the consumer to get a hold of the data it needs without knowing or caring where the data came from. For more on the blackboard pattern...well as they say...google is your friend.
A simplistic blackboard object could be a Dictionary<string, object>
- a simple dictionary of named values. All interested objects would share a reference to the dictionary allowing them to freely exchange named data. The problems with this approach are name and type safety - the data producer and data consumer objects must share a string identifier of each data value, and also the consumer does not have compile-time type checking of the value in the dictionary (i.e., it may expect to read a decimal, and instead at run-time get a string value). This article demonstrates one solution to these two problems.
Background
Recently I was developing an engine for asynchronous execution of general purpose tasks. My "tasks" were units of work with Do/Undo functionality, in principle independent of each other, but some of the tasks I implemented required information from already executed tasks. For instance, one task could set up an API object for a hardware device and subsequent tasks could then use the created API object to manipulate the hardware device in different ways. But I did not want my execution engine to know anything about the tasks it executes and I did not want to have to wire up references manually from task to task.
The Blackboard class
The blackboard class is basically a wrapper for a Dictionary<string, object>
. It exposes a Get
and a Set
method. The blackboard allows clients to store and retrieve data from it, but it requires that the data be accessed using an identifier of type BlackboardProperty<T>
. The BlackboardProperty instance should be shared between objets that read or write the property to the blackboard, so it should be a static member in a shared location, usually on the class that uses it or the class that provides it (much like dependency properties in WPF/Silverlight are static members of controls they "belong" to).
Note: Name safety could have been achieved in the same way by sharing a static string reference, but that would still not solve the type safety issue. So anyway... to the meat and potatoes, here is the code for the blackboard class:
public class Blackboard : INotifyPropertyChanged, INotifyPropertyChanging
{
Dictionary<string, object> _dict = new Dictionary<string, object>();
public T Get<T>(BlackboardProperty<T> property)
{
if (!_dict.ContainsKey(property.Name))
_dict[property.Name] = property.GetDefault();
return (T)_dict[property.Name];
}
public void Set<T>(BlackboardProperty<T> property, T value)
{
OnPropertyChanging(property.Name);
_dict[property.Name] = value;
OnPropertyChanged(property.Name);
}
#region property change notification
public event PropertyChangingEventHandler PropertyChanging;
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanging(string propertyName)
{
if (PropertyChanging != null)
PropertyChanging(this, new PropertyChangingEventArgs(propertyName));
}
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
The BlackboardProperty class
The BlackboardProperty
class serves as an identifier for accessing data in a blackboard object. It defines the name, and the type of values it accesses on a blackboard. It also defines the default value which should be returned in case the blackboard does not contain a value for the property.
public class BlackboardProperty<T>
{
public string Name { get; set; }
Func<T> _createDefaultValueFunc;
public BlackboardProperty(string name)
: this(name, default(T))
{
}
public BlackboardProperty(string name, T defaultValue)
{
Name = name;
_createDefaultValueFunc = () => defaultValue;
}
public BlackboardProperty(string name, Func<T> createDefaultValueFunc)
{
Name = name;
_createDefaultValueFunc = createDefaultValueFunc;
}
public BlackboardProperty()
{
Name = Guid.NewGuid().ToString();
}
public T GetDefault()
{
return _createDefaultValueFunc();
}
}
Using the code
Here is a demonstration which illustrates how to use the blackboard:
class ClassA
{
public static BlackboardProperty<int> SomeImportantDataProperty = new BlackboardProperty<int>();
public void DoStuff(Blackboard blackboard)
{
int result = 0;
blackboard.Set(SomeImportantDataProperty, result);
}
}
class ClassB
{
public void DoStuff(Blackboard blackboard)
{
int theImportantDataClassAContributed = blackboard.Get(ClassA.SomeImportantDataProperty);
Console.WriteLine("The result of A's operation was {0}", theImportantDataClassAContributed);
}
}
class ClassC
{
public void DoStuff(Blackboard blackboard)
{
int theImportantDataClassAContributed = blackboard.Get(ClassA.SomeImportantDataProperty);
File.WriteAllText("C:\\importantData.txt", string.Format("This is A's result {0}", theImportantDataClassAContributed));
}
}
class Demo()
{
static void Main()
{
ClassA objA = new ClassA();
ClassB objB = new ClassB();
ClassC objC = new ClassC();
Blackboard blackboard = new Blackboard();
objA.DoStuff(blackboard);
objB.DoStuff(blackboard);
objC.DoStuff(blackboard);
}
}
Not terribly useful code, I admit, but it illustrates the use of the two classes.
This next example is a bit more "real world"-ish, but is of course also quite simplified. In it I define several types of tasks which I use for setting up a connection to a hardware device, manipulating the device, and for closing the connection to the device. Tasks are executed sequentially by an execution engine. The tasks share data through a common blackboard. The implementations of the Task and ExecutionEngine
classes are possibly material for another article...
interface IDevice
{
void Connect();
void Reset();
decimal Read(string obis);
void Close();
}
class InitiateDeviceTask : Task
{
public static BlackboardProperty<IDevice> DeviceAPIProperty = new BlackboardProperty<IDevice>();
protected override void Execute(Blackboard context)
{
IDevice deviceAPI = null;
deviceAPI.Connect();
context.Set(DeviceAPIProperty, deviceAPI);
}
}
class ResetDeviceTask : Task
{
protected override void Execute(Blackboard context)
{
IDevice deviceAPI = context.Get(InitiateDeviceTask.DeviceAPIProperty);
deviceAPI.Reset();
}
}
class ReadRegisterTask : Task
{
public string RegisterName { get; set; }
public static BlackboardProperty<Dictionary<string, decimal>> ReadingsProperty = new BlackboardProperty<Dictionary<string, decimal>>("Readings", () => new Dictionary<string, decimal>());
protected override void DoExecute(Blackboard context)
{
IDevice deviceAPI = context.Get(InitiateDeviceTask.DeviceAPIProperty);
decimal value = deviceAPI.Read(RegisterName);
context.Get(ReadingsProperty)[RegisterName] = value;
}
}
class CloseConnectionTask : Task
{
protected override void Execute(Blackboard context)
{
IDevice deviceAPI = context.Get(InitiateDeviceTask.DeviceAPIProperty);
deviceAPI.Close();
}
}
class Demo
{
public void StartDemo()
{
ExecutionEngine engine = new ExecutionEngine();
engine.Enque(new InitiateDeviceTask());
engine.Enque(new ReadRegisterTask() { RegisterName = "1.8.0" });
engine.Enque(new ReadRegisterTask() { RegisterName = "3.8.0" });
engine.Enque(new ResetDeviceTask());
engine.Enque(new CloseConnectionTask());
engine.Start();
}
}
Another possible use for the blackboard is in a plugin-enabled application, to allow plugins to communicate with each other if they need to.
The property change notification might be useful in this scenario.
One important thing to note is that BlackboardProperty
instances are meant to be static memebers of classes that logically
own the property. So since it is static, the same BlackboardProperty
instance can easily appear in multiple blackboards.
When no data is present on the blackboard for a given property, the Blackboard will ask the BlackboardProperty instance for a default value. The default value might be a reference type, so if you do not wish to share the same reference accross multiple blackboards be sure to use the following constructor when creating a BlackboardProperty
:
public BlackboardProperty(string name, Func<T> createDefaultValueFunc)
That will ensure that the default value created is not shared among multiple blackboards.
Points of Interest
I should note that this solution was in part influenced by the DependencyProperty system in WPF, and also by a very usefull article I had read about enum classes a while ago.
I have been an a(tra)ctive software developer since 2005 mostly working on .NET. Currently living and working in Zagreb Croatia. I have earned my masters degree in Computer Science at the Faculty of Electrical Engineering and Computer Science in Zagreb in 2006.