2
\$\begingroup\$

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);
        }
    }
}



```
\$\endgroup\$
1
\$\begingroup\$

Passing the context that way is acceptable but the problem with the context that you have is that it can grow larger the more you add custom statements. This is an anti pattern, because it can be a GOD Object. And if you create new custom statements in the future you may need to touch the context again, which tainted the SOLID principle.

The better approach for this is if you have specific dependency, like device in this case, I'd suggest to inject it either into constructor or as a Field in the context.

The following is my version of the code

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ContextPassing.Core;
using ContextPassing.Specific;
using SetField = ContextPassing.Core.SetFieldStatement;
using Delay = ContextPassing.Core.DelayStatement;
using SendToDevice = ContextPassing.Specific.SendToDeviceStatement;
using LogField = ContextPassing.Core.LogFieldStatement;
using Log = ContextPassing.Core.LogStatement;
using If = ContextPassing.Core.IfStatement;
using static System.Console;

namespace ContextPassing.Client
{
    class Program
    {
        static void Main(string[] args)
        {
            WriteLine("Please write a random text and press enter.");
            string randomText = ReadLine();
            Clear();

            WriteLine("Do you want to send the the text to the DUT/Device ? (y for yes)");
            string printInput = ReadKey().KeyChar.ToString();
            Clear();

            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 = ReadKey().KeyChar.ToString();
            Clear();

            var device = new Device
                         {
                             IsConnected = true
                         };
            // declarative initialisation
            IStatement rootStatement = new Statements(
                    new SetField("RandomText", randomText),
                    new SetField("SendInput", printInput),
                    new SetField("AddDelay", addDelay),
                    new If(new FieldCondition("SendInput", "y"),
                        new Statements(
                            new If(new FieldCondition("AddDelay", "y"),
                                new Delay {Time = TimeSpan.FromSeconds(1)}),
                            new LogField("RandomText"),
                            new If(new CheckDeviceCondition(device), 
                                new SendToDevice("RandomText", device),
                                new Log("Error: Device is not connected!"))),
                        new If(new FieldCondition("SendInput", "k"),
                            new Log("This is an easter egg."),
                            new Log("Input was not sent."))));

            // normal initialisation
            // IStatement rootStatement = new Statements(
            //     new SetField("RandomText", randomText),
            //     new SetField("SendInput", printInput),
            //     new SetField("AddDelay", addDelay));
            //
            // var printInputIfStatement = new If(new FieldCondition("SendInput", "y"));
            // var addDelayIfStatement = new If(new FieldCondition("AddDelay", "y"));
            //
            // addDelayIfStatement.IfTrue(new Delay {Time = TimeSpan.FromSeconds(1)});
            //
            // var sendToDeviceIfStatement = new If(new CheckDeviceCondition(device));
            //
            // sendToDeviceIfStatement.IfTrue(new SendToDevice("RandomText", device));
            // sendToDeviceIfStatement.IfFalse(new Log("Error: Device is not connected!"));
            //
            // printInputIfStatement.IfTrue(
            //     new Statements(
            //         addDelayIfStatement,
            //         new LogField("RandomText"),
            //         sendToDeviceIfStatement));
            //
            // var elseif = new IfStatement(new FieldCondition("SendInput", "k"));
            //
            // elseif.IfTrue(new Log("This is an easter egg."));
            // elseif.IfFalse(new Log("Input was not sent.")); // else
            // printInputIfStatement.IfFalse(elseif);
            //
            // rootStatement.Enqueue(printInputIfStatement);

            var context = new Context();
            rootStatement.Execute(context);
            WriteLine("*** Execution complete - Message overview: ***");

            var msgContainer = MessageContainer.GetContainer(context);
            foreach (string msg in msgContainer.Messages)
            {
                WriteLine(msg);
            }

            WriteLine("***");

            WriteLine("Press any key to exit.");
            ReadKey();
        }
    }
}

namespace ContextPassing.Core
{
    public interface IFieldContainer
    {
        IDictionary<string, object> Fields { get; }
    }

    public interface IMessageContainer
    {
        ICollection<string> Messages { get; }
    }

    public class Context : IFieldContainer
    {
        public IDictionary<string, object> Fields { get; } = new Dictionary<string, object>();
    }

    public class MessageContainer : IMessageContainer
    {
        private static readonly string MESSAGE_KEY = typeof(IMessageContainer).FullName;
        public ICollection<string> Messages { get; } = new Collection<string>();
        public static IMessageContainer GetContainer(object context)
        {
            if (context is not IFieldContainer fieldsContainer)
                throw new InvalidOperationException("Context needs to be an IFieldContainer.");
            if (fieldsContainer.Fields.TryGetValue(MESSAGE_KEY, out var value) && value is IMessageContainer messageContainer)
                return messageContainer;
            messageContainer = new MessageContainer();
            fieldsContainer.Fields.Add(MESSAGE_KEY, messageContainer);
            return messageContainer;
        }
    }

    public interface IStatement
    {
        void Enqueue(IStatement nextStatement);
        IStatement Dequeue();
        void Execute(object context);
    }

    public interface ICondition
    {
        bool IsTrue(object context);
    }

    public class True : ICondition
    {
        public bool IsTrue(object context) => true;
    }

    public class False : ICondition
    {
        public bool IsTrue(object context) => false;
    }

    public class Not : ICondition
    {
        private readonly ICondition _condition;
        public Not(ICondition condition) => _condition = condition;
        public bool IsTrue(object context) => !_condition.IsTrue(context);
    }

    public abstract class AggregateConditionBase : ICondition
    {
        protected readonly IReadOnlyCollection<ICondition> _conditions;
        protected AggregateConditionBase(params ICondition[] conditions)
        {
            _conditions = conditions ?? Array.Empty<ICondition>();
        }
        
        public abstract bool IsTrue(object context);
    }

    public class Or : AggregateConditionBase
    {
        public override bool IsTrue(object context) => _conditions.Any(c => c.IsTrue(context));
    }

    public class And : AggregateConditionBase
    {
        public override bool IsTrue(object context) => _conditions.All(c => c.IsTrue(context));
    }

    public class FieldCondition : ICondition
    {
        private readonly string _field;
        private readonly string _value;
        public FieldCondition(string field, string value)
        {
            _field = field;
            _value = value;
        }
        public bool IsTrue(object context)
        {
            if (context is not IFieldContainer fieldContainer)
                throw new InvalidOperationException("Context needs to be an IFieldContainer.");
            return fieldContainer.Fields[_field].Equals(_value);
        }
    }

    public abstract class StatementBase : IStatement
    {
        private readonly Queue<IStatement> _nextStatements = new();
        protected StatementBase(params IStatement[] nextStatements)
        {
            if (nextStatements == null) return;
            foreach (var nextStatement in nextStatements)
            {
                Enqueue(nextStatement);
            }
        }
        public void Enqueue(IStatement nextStatement) => _nextStatements.Enqueue(nextStatement);
        public IStatement Dequeue() => _nextStatements.Dequeue();
        protected abstract void Execute(object context);
        void IStatement.Execute(object context)
        {
            Execute(context);
            foreach (var nextStatement in _nextStatements)
            {
                nextStatement.Execute(context);
            }
        }
    }

    // this is just a wrapper of statements
    public class Statements : StatementBase
    {
        public Statements(params IStatement[] nextStatements)
            : base(nextStatements)
        { }
        protected override void Execute(object context)
        { /* do nothing */ }
    }

    public class IfStatement : StatementBase
    {
        private readonly ICondition _condition;
        private IStatement _ifTrue;
        private IStatement _ifFalse;
        public IfStatement(ICondition condition, IStatement ifTrue = null, IStatement ifFalse = null, params IStatement[] nextStatements)
            : base(nextStatements)
        {
            _condition = condition;
            _ifTrue = ifTrue;
            _ifFalse = ifFalse;
        }
        public void IfTrue(IStatement ifTrue)
        {
            _ifTrue = ifTrue;
        }
        public void IfFalse(IStatement ifFalse)
        {
            _ifFalse = ifFalse;
        }
        protected override void Execute(object context)
        {
            if (_condition.IsTrue(context))
            {
                _ifTrue?.Execute(context);
                return;
            }
            
            _ifFalse?.Execute(context);
        }
    }
    
    public class SetFieldStatement : StatementBase
    {
        private readonly string _field;
        private readonly string _value;
        public SetFieldStatement(string field, string value, params IStatement[] nextStatements)
            : base(nextStatements)
        {
            _field = field;
            _value = value;
        }
        protected override void Execute(object context)
        {
            if (context is not IFieldContainer fieldsContainer)
                throw new InvalidOperationException("Context needs to be an IFieldContainer.");
            fieldsContainer.Fields[_field] = _value;
        }
    }

    public class LogStatement : StatementBase
    {
        private readonly string _message;
        public LogStatement(string message, params IStatement[] nextStatements) 
            : base(nextStatements) => _message = message;
        protected override void Execute(object context)
        {
            var msgContainer = MessageContainer.GetContainer(context);
            msgContainer.Messages.Add(_message);
        }
    }

    public class LogFieldStatement : StatementBase
    {
        private readonly string _field;
        public LogFieldStatement(string field, params IStatement[] nextStatements) 
            : base(nextStatements) => _field = field;
        protected override void Execute(object context)
        {
            if (context is not IFieldContainer fieldContainer)
                throw new InvalidOperationException("Context needs to be an IFieldContainer.");
            var msgContainer = MessageContainer.GetContainer(context);
            msgContainer.Messages.Add($"Field {_field} its value is: {fieldContainer.Fields[_field]}");
        }
    }

    public class DelayStatement : StatementBase
    {
        public DelayStatement(params IStatement[] nextStatements) 
            : base(nextStatements)
        { }
        public TimeSpan Time { get; init; } = TimeSpan.FromSeconds(1);
        protected override void Execute(object context) => Thread.Sleep(Time);
    }
}

namespace ContextPassing.Specific
{
    public interface IDevice
    {
        public void Send(object data);
        public bool IsConnected();
    }
    
    public class Device : IDevice
    {
        public bool IsConnected { get; set; }
        public void SendI2cBytes(byte[] bytes) => WriteLine($"Sending bytes bleep bloep blaap: {BitConverter.ToString(bytes)}");
        void IDevice.Send(object data) => SendI2cBytes(Encoding.ASCII.GetBytes(data.ToString() ?? string.Empty));
        bool IDevice.IsConnected() => IsConnected;
    }

    public class CheckDeviceCondition : ICondition
    {
        private readonly IDevice _device;
        public CheckDeviceCondition(IDevice device) => _device = device;
        public bool IsTrue(object context) => _device.IsConnected();
    }

    public class SendToDeviceStatement : StatementBase
    {
        private readonly string _field;
        private readonly IDevice _device;
        public SendToDeviceStatement(string field, IDevice device, params IStatement[] nextStatements)
            : base(nextStatements)
        {
            _field = field;
            _device = device;
        }
        protected override void Execute(object context)
        {
            if (context is not IFieldContainer fieldContainer)
                throw new ArgumentException("Context should be a device container.");
            _device.Send(fieldContainer.Fields[_field]);
        }
    }
}

Notes:

  • I use a behavioral Pattern called Chain of Responsibility
  • Introduced Enqueue and Dequeue methods in IStatement to add/remove next statements. I also add nextStatements as part of constructor so we can do declarative initialisation.
  • Context contains only Fields. If you think there wont be any use case that we need to introduce new interfaces into the context, I would suggest to just use IFieldContainer as context type (not object)
  • Introduced IDevice in case there'll be multiple kind of devices. This is injected into CheckDeviceConnectionCondition and SendToDeviceStatement. You can also use context to store the field (like IMessageContainer)
  • IMessageContainer now is injected into Fields. I add a static Method in MessageContainer to grab the message container instance from context.

If you need to introduce new custom statements you don't need to update the context or any existing class or funcationlities.

\$\endgroup\$
6
  • \$\begingroup\$ Thank you for your clear and crisp explanation of the shortcomings and improvements on my current implementation. Very helpful feedback! I'm glad that I was on the right track, but you are right on the context becoming a GOD object and having to adjust it when new statements are added. I was a bit confused by the two Execute methods, since the StatementBase.Execute implementation executes only the current instance, while the IStatement.Execute executes the current statements and the next in the queue. Is it best to rename one of them not to confuse future readers of my code? \$\endgroup\$ – Cédric Moers Apr 26 at 8:34
  • \$\begingroup\$ Merely out of curiosity: I was also wondering if MessageContainer.Get(...) does not violate the single-responsibility-principle? Since the MessageContainer extracts/injects itself from/into the context. \$\endgroup\$ – Cédric Moers Apr 26 at 8:43
  • 1
    \$\begingroup\$ StatementBase, the first Execute method is an abstract protected method, which is the real implementation/execution of the statement. And the other Execute method is the explicit implementation of interface IStatement. This is final, and it determines the flow of execution. They have same name, but different intention. \$\endgroup\$ – Moch Yusup Apr 26 at 13:33
  • 1
    \$\begingroup\$ The reason I didn't merge that into one is to prevent subclass to call base.Execute() which in this case the position is very important, wrong position would cause problem. But in this case, the subclass don't have to worry about that and just focus on the implementation code. You can change the abstract method to different name but I prefer the same name. \$\endgroup\$ – Moch Yusup Apr 26 at 13:34
  • 1
    \$\begingroup\$ As far as the MessageContainer.GetContainer(object context), it's a static method so it doesn't violate the single-responsibility-principle of that class. It's merely an extension/helper to get the message container from and into the context, which you can extract to any static class if needs to. I put it in the same class to make it cleaner, because the MessageContainer is a small class, and putting it there make it easier to read. \$\endgroup\$ – Moch Yusup Apr 26 at 13:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.