I am developing a system for runtime macro executions in an automatic test environment. Macros are defined in an xml and are for configuring multiple devices/DUTs for a specific use case. The xml parsing of these macros is outside of the scope of this question.
For building the macros in code I created the code shown below. In this example, there is no 'Macro' class, but rather a collection of 'Statement's. This can be regarded as the same thing.
A macro consists out of statements and some statements are only to be executed under certain conditions (e.g. IfStatement).
For execution of the statements a context object is passed between the statements and the conditions. This context can then be used/modified by the statements and checked by the conditions.
For this I use 2 interfaces: IStatement and ICondition:
Each Statement implements IStatement - having an Execute(object context) method - and tries to cast the context (of type object) to one or more interfaces which the statement needs. Some statements do not actually need the context (e.g. DelayStatement)
The same goes for the Conditions, they implement ICondition - having an IsTrue(object context) method - and cast the context to expected interfaces. Some conditions do not need the context (e.g. True, False), but implement the interface explicitly.
There is a Core project, that defines the basics of the macros, but this can be extended for more specific projects. The context object can vary per project and can even have a different type (e.g. Context, SpecificContext). More statements/conditions can be added per project.
I have a couple of questions about this:
- Is the way of passing context a good way for a use case like this? Should the statement/condition 'know' about the context it is going to receive/expect? Or should that be the responsibility of the caller?
- If so, what is a better approach for this?
- Should I define multiple interfaces for Statements that require context, and statements that do not? Same goes for conditions.
- If this context-passing is a design pattern, what is the name of it?
- Within a statement/condition: should I check for multiple interfaces/classes? (e.g. in the DeviceConnectedCondition: should I check if the context is a Device and then check if the context is a IDeviceContainer, or should I just check for IDeviceContainer?)
Some notes:
- I can create an interface IContext, that has at least the Arguments, Messages, and Result to reduce some castings.
- There are more statements/conditions than shown below, this is just a sandbox.
- I started out using the Visitor pattern and no common Execute(object)/IsTrue(object) method, but stumbled upon problems in the stage of the Specific projects where I did not know how to properly extend the Core.Visitor. In this case, only the Visitor knew about the specific Context class/object and knew which methods to invoke on the Statements/Conditions (e.g: it knew that a True condition does not need context, but for the SendI2CToDevice, it only provided the Device by invoking a specific method)
Many thanks!
using ContextPassing.Core;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ContextPassing.Client
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Please write a random text and press enter.");
string randomText = Console.ReadLine();
Console.Clear();
Console.WriteLine("Do you want to send the the text to the DUT/Device ? (y for yes)");
string printInput = Console.ReadKey().KeyChar.ToString();
Console.Clear();
Console.WriteLine("If previous answer was yes, do you want to add a delay before sending it to the DUT/Device ? (y for yes)");
string addDelay = Console.ReadKey().KeyChar.ToString();
Console.Clear();
var statements = new List<IStatement>();
statements.Add(new SetFieldStatement { FieldName = "RandomText", Value = randomText });
statements.Add(new SetFieldStatement { FieldName = "SendInput", Value = printInput });
statements.Add(new SetFieldStatement { FieldName = "AddDelay", Value = addDelay });
var printInputIfStatement = new IfStatement() { Condition = new FieldCondition { FieldName = "SendInput", Value = "y" } };
var elseif = new ElseIf { Condition = new FieldCondition { FieldName = "SendInput", Value = "k" } };
elseif.Statements.Add(new LogStatement { Message = "This is an easter egg." });
printInputIfStatement.ElseIfs.Add(elseif);
printInputIfStatement.Else.Add(new LogStatement { Message = "Input was not sent." });
var addDelayIfStatement = new IfStatement() { Condition = new FieldCondition { FieldName = "AddDelay", Value = "y" } };
addDelayIfStatement.Statements.Add(new DelayStatement() { Time = TimeSpan.FromSeconds(1) });
printInputIfStatement.Statements.Add(addDelayIfStatement);
printInputIfStatement.Statements.Add(new LogFieldStatement() { FieldName = "RandomText" });
printInputIfStatement.Statements.Add(new ContextPassing.Specific.SendI2cBytesToDevice() { Bytes = Encoding.ASCII.GetBytes(randomText) });
statements.Add(printInputIfStatement);
var context = new ContextPassing.Specific.SpecificContext();
context.Device = new ContextPassing.Specific.Device();
new StatementExecutor().Execute(statements, context);
Console.WriteLine("*** Execution complete - Message overview: ***");
foreach (string msg in (context as IMessageContainer).Messages)
{
Console.WriteLine(msg);
}
Console.WriteLine("***");
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
}
namespace ContextPassing.Core
{
public interface IArgumentContainer
{
IEnumerable<object> Arguments { get; }
}
public interface IFieldContainer
{
IDictionary<string, object> Fields { get; }
}
public interface IMessageContainer
{
ICollection<string> Messages { get; }
}
public class Context : IArgumentContainer, IMessageContainer, IFieldContainer
{
public Collection<object> Arguments { get; } = new Collection<object>();
public Collection<string> Messages { get; } = new Collection<string>();
public Dictionary<string, object> Fields { get; } = new Dictionary<string, object>();
object Result { get; set; }
IEnumerable<object> IArgumentContainer.Arguments => Arguments;
ICollection<string> IMessageContainer.Messages => Messages;
IDictionary<string, object> IFieldContainer.Fields => Fields;
}
public class StatementExecutor
{
public void Execute(IEnumerable<IStatement> statements, object context)
{
foreach (IStatement statement in statements)
{
statement.Execute(context);
}
}
}
public interface IStatement
{
void Execute(object context);
}
public interface ICondition
{
bool IsTrue(object context);
}
public class True : ICondition
{
public bool IsTrue() => true;
public bool IsTrue(object context) => IsTrue();
}
public class False : ICondition
{
public bool IsTrue() => false;
public bool IsTrue(object context) => throw new NotImplementedException();
}
public class Not : ICondition
{
ICondition Condition { get; set; }
public bool IsTrue(object context) => !Condition.IsTrue(context);
}
public class Or : ICondition
{
Collection<ICondition> Conditions { get; } = new Collection<ICondition>();
public bool IsTrue(object context) => Conditions.Any(x => x.IsTrue(context));
}
public class FieldCondition : ICondition
{
public string FieldName { get; set; }
public object Value { get; set; }
public bool IsTrue(object context)
{
var fieldContainer = context as IFieldContainer;
if (fieldContainer is null)
throw new InvalidOperationException("Context needs to be an IFieldContainer.");
return fieldContainer.Fields[FieldName].Equals(Value);
}
}
public class IfStatement : IStatement
{
public Collection<IStatement> Statements { get; } = new Collection<IStatement>();
public Collection<ElseIf> ElseIfs { get; } = new Collection<ElseIf>();
public Collection<IStatement> Else { get; set; } = new Collection<IStatement>();
public ICondition Condition { get; set; }
public void Execute(object context)
{
if (Condition.IsTrue(context))
{
foreach (IStatement statement in Statements)
statement.Execute(context);
return;
}
foreach (ElseIf elseIf in ElseIfs)
{
if (elseIf.Condition.IsTrue(context))
{
foreach (IStatement statement in elseIf.Statements)
statement.Execute(context);
return;
}
}
if (Else != null)
{
foreach (IStatement statement in Else)
statement.Execute(context);
}
}
}
public class ElseIf
{
public ICondition Condition { get; set; }
public Collection<IStatement> Statements { get; } = new Collection<IStatement>();
}
public class SetFieldStatement : IStatement
{
public string FieldName { get; set; }
public object Value { get; set; }
public void Execute(object context)
{
var fieldsContainer = context as IFieldContainer;
if (fieldsContainer is null)
throw new InvalidOperationException("Context needs to be an IFieldContainer.");
fieldsContainer.Fields[FieldName] = Value;
}
}
public class LogStatement : IStatement
{
public string Message { get; set; }
public void Execute(object context)
{
var msgsContainer = context as IMessageContainer;
if (msgsContainer is null)
throw new ArgumentException("Context needs to be an IMessageContainer.");
msgsContainer.Messages.Add(Message);
}
}
public class LogFieldStatement : IStatement
{
public string FieldName { get; set; }
public void Execute(object context)
{
var fieldContainer = context as IFieldContainer;
var msgsContainer = context as IMessageContainer;
if (fieldContainer is null)
throw new InvalidOperationException("Context needs to be an IFieldContainer.");
if (msgsContainer is null)
throw new ArgumentException("Context needs to be an IMessageContainer.");
msgsContainer.Messages.Add($"Field {FieldName} its value is: {fieldContainer.Fields[FieldName]}");
}
}
public class DelayStatement : IStatement
{
public TimeSpan Time { get; set; } = TimeSpan.FromSeconds(1);
public void Execute()
{
Thread.Sleep(Time);
}
// This statement does not need any context.
void IStatement.Execute(object context) => Execute();
}
}
namespace ContextPassing.Specific
{
public class Device
{
public void SendI2cBytes(byte[] bytes) => Console.WriteLine($"Sending bytes bleep bloep blaap: {BitConverter.ToString(bytes)}");
public bool IsConnected => true;
}
public interface IDeviceContainer
{
Device Device { get; }
}
public class SpecificContext : ContextPassing.Core.Context, IDeviceContainer
{
public Device Device { get; set; } = new Device();
}
public class IsConnected : ICondition
{
public bool IsTrue(object context)
{
var dc = context as IDeviceContainer;
if (dc is null)
throw new ArgumentException("Context should be a device container.");
return dc.Device.IsConnected;
}
}
public class SendI2cBytesToDevice : IStatement
{
public byte[] Bytes;
public void Execute(object context)
{
var dc = context as IDeviceContainer;
if (dc is null)
throw new ArgumentException("Context should be a device container.");
dc.Device.SendI2cBytes(Bytes);
}
}
}
```