Я разрабатываю систему для выполнения макросов во время выполнения в автоматической тестовой среде. Макросы определены в xml и предназначены для настройки нескольких устройств / DUT для конкретного случая использования. Разбор xml этих макросов выходит за рамки этого вопроса.
Для создания макросов в коде я создал код, показанный ниже. В этом примере нет класса ‘Macro’, а скорее коллекция ‘Statement’s. Это можно рассматривать как одно и то же.
Макрос состоит из операторов, и некоторые операторы должны выполняться только при определенных условиях (например, IfStatement).
Для выполнения операторов объект контекста передается между операторами и условиями. Этот контекст затем может использоваться / изменяться операторами и проверяться условиями.
Для этого я использую 2 интерфейса: IStatement и ICondition:
Каждый оператор реализует IStatement — имея метод Execute (контекст объекта) — и пытается привести контекст (типа объекта) к одному или нескольким интерфейсам, которые требуются оператору. Некоторым операторам на самом деле не нужен контекст (например, DelayStatement)
То же самое и с Условиями, они реализуют ICondition — имея метод IsTrue (контекст объекта) — и приводят контекст к ожидаемым интерфейсам. Некоторые условия не нуждаются в контексте (например, True, False), но явно реализуют интерфейс.
Существует проект Core, который определяет основы макросов, но его можно расширить для более конкретных проектов. Объект контекста может варьироваться в зависимости от проекта и даже может иметь другой тип (например, Context, SpecificContext). Для каждого проекта можно добавить больше утверждений / условий.
У меня есть пара вопросов по этому поводу:
- Подходит ли способ передачи контекста для такого случая использования? Должно ли утверждение / условие «знать» о контексте, который оно будет получать / ожидать? Или это должно быть ответственность вызывающего абонента?
- Если да, то какой для этого подход лучше?
- Должен ли я определять несколько интерфейсов для операторов, которые требуют контекста, и операторов, которые этого не делают? То же самое и с условиями.
- Если эта передача контекста является шаблоном проектирования, как оно называется?
- В заявлении / условии: следует ли мне проверять наличие нескольких интерфейсов / классов? (например, в DeviceConnectedCondition: должен ли я проверять, является ли контекст устройством, а затем проверять, является ли контекст IDeviceContainer, или я должен просто проверить наличие IDeviceContainer?)
Некоторые примечания:
- Я могу создать интерфейс IContext, который имеет как минимум аргументы, сообщения и результат, чтобы уменьшить количество приведений.
- Есть больше утверждений / условий, чем показано ниже, это просто песочница.
- Я начал использовать шаблон Visitor и не использовал общий метод Execute (object) / IsTrue (object), но наткнулся на проблемы на этапе конкретных проектов, когда я не знал, как правильно расширить Core.Visitor. В этом случае только Посетитель знал о конкретном классе / объекте контекста и знал, какие методы вызывать для утверждений / условий (например: он знал, что условие True не требует контекста, но для SendI2CToDevice он только предоставил Device вызывая определенный метод)
Большое спасибо!
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);
}
}
}
```