Task
I’m trying to handle input string that looks like /commnd param1 param2 ...
. But if user missed some parameters or parameter was incorrect, I would like to provide option to re-input only those parameters, that seemed incorrect.
Research
I’ve made an interface which works similar to FSM. But it works more directly. I could use FSM but it’s idea slightly different. In State Machine pattern a state knows the condition, when it has change to a next state and it creates next state inside itself. In my case there is no necessity to change state by state on each itteration, because plan have to execute step by step as much as possible at once.
public interface IPlan<TReport>{
IObservable<TReport> Execute();
}
public interface IStep{
IObservable<bool> Make();
}
The plan is executed as long as all steps are completed successfully. The method IPlan.Execute()
returns the report with the details of execution. And if plan fails, user will be able to update input parameters and resume plan execution from the last breakpoint.
It would be nice if there was no necessity to input some additional data. But unfortunately in number of cases the plan executes with some TContext
, it calculates some data during execution and stores them inside TBuffer
. And if IPlan.Execute
breaks on some Step
, it provides TReport
which allows code to send an information to user about what exactly goes wrong. And user would have option to provide only the missing parameter.
Solution
So I’ve implemented the ObservablePlan<TStep, TReport>: IPlan<TReport>
without TContext
and TBuffer
. And right now I’m trying to make an implementation of ObservablePlan
that allows included Step
s to work with TContext
that could be updated from outside and TBuffer
that should be updated from inside of plan execution. I thought about decorators, but the nested steps couldn’t have access to the fields of the decorator
ObservablePlan
:
public abstract class ObservablePlan<TStep, TReport>: IPlan<TReport> where TStep : IStep
{
public TStep? CurrentStep { get; private set; } = default;
public IEnumerable<TStep> Steps { get; }
IEnumerator<TStep>? enumerator = null;
public ObservablePlan(IEnumerable<TStep> steps)
{
if (steps == null || !steps.Any())
throw new ArgumentNullException("steps", "Steps couldn't be empty or equal `Null`");
Steps = steps;
}
protected virtual void Init()
{
enumerator = Steps.GetEnumerator();
}
public virtual void Restart()
{
Init();
}
public IObservable<TReport> Execute()
{
if (enumerator == null)
Init();
if (enumerator.Current == null && !enumerator.MoveNext())
{
throw new ArgumentException("No more steps to do");
}
return RecursiveCallMake(enumerator).Select(CreateReport);
}
public IObservable<bool> RecursiveCallMake(IEnumerator<TStep> enumerator)
{
CurrentStep = enumerator.Current;
return CurrentStep.Make()
.SelectMany(x =>
{
if (!x)
{
Console.WriteLine("Step didn't passed: " + x.ToString());
return Observable.Return(x);
}
else
Console.WriteLine("Step passed: " + x.ToString());
if (!enumerator.MoveNext())
{
Console.WriteLine("No more steps!");
return Observable.Return(x);
}
return RecursiveCallMake(enumerator);
});
}
protected abstract TReport CreateReport(bool success);
}
And the Step
implementation:
public abstract class Step<T, K> : IStep<T> where K : class
{
public K Buffer{get;}
public Step(K buffer){
Buffer = buffer;
}
public abstract IObservable<T> Make();
}
Issues
As you can see I can easily share Buffer
without changing interface, because it represents inner data, and it’s data can’t be changed by user directly. But I have issue with the TContext
. Because if I add it to IPlan.Execute(TContext context)
as a parameter, then it will spoils the interface. Because some plan can be executed without context. And if I add the Context
parameter to a constructor of an Plan
implementation, than I’ll have to create some kind of update method to IPlan interface, which spoils interface as well. Furthermore, I have to update somehow TContext
variable for Step
as well in both cases. Please keep in the mind, that I’m going to store intstances of IPlan
or ObservablePlan
inside collection. There are different plans for different operations: create, delete, menu, etc… All of them will have the same type of context and the different type of buffers.
So I’m looking for a solution to use Context
variable in such a way, that I can update the current plan with a new instance of Context
without compromising the interface.