vendredi 15 mai 2020

Is there a pattern for dealing with interface instances that require different classes of data?

I'm currently working on a workflow system. Simply put, a workflow consists of several steps. Each step has 0 or more transitions to other steps based on the result of the step. If there are no transitions, then it's the end of the workflow.

Background

Steps are stored in the database with a unique id, which is used to instantiate them through reflection. I have the following right now:

IEnumerable<Workflow> workflows = _workflowRepository.GetWorkflows();
foreach(Workflow workflow in workflows) {
    // Retrieve the current step that must be execute
    WorkflowStep currentStep = getCurrentStep(); 
    var flowRunning = true;
    while (flowRunning) {
        IWorkflowStep instance = WorkflowStepFactory.GetStep(currentStep.StepId);
        Result result = instance.Execute();
        // Set currentStep based on defined transitions and the value of result
    }
}

The IWorkflowStep is the interface that steps must inherit:

public interface IWorkflowStep
{
    Result Execute();
}

Each class that is a step has a StepIdAttribute placed on the class with the id:

[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
public sealed class StepIdAttribute : Attribute
{
    public StepIdAttribute(int stepId)
    {
      StepId = stepId;
    }

    public int StepId { get; }
}

A step must implement IWorkflowStep and must have the StepIdAttribute placed on that it:

[StepId(Constants.FIRST_STEP_ID)]
public sealed class FirstStep : IWorkflowStep
{
    public Result Execute()
    {
        // Execute step
    }
}

Lastly, the WorkflowStepFactory creates an instance of the interface through reflection:

public static class WorkflowStepFactory
{
    public static IWorkflowStep GetStep(int stepId)
    {
        var iWorkflowStepType = typeof(IWorkflowStep);
        var assemblies = AppDomain.CurrentDomain.GetAssemblies();

        foreach (var assembly in assemblies)
        {
            var typesThatImplementIWorkflowStep = assembly.GetTypes().Where(t => iWorkflowStepType.IsAssignableFrom(t));
            foreach (var type in typesThatImplementIWorkflowStep)
            {
                var appliedStepIdAttributes = type.GetCustomAttributes(typeof(StepIdAttribute), false).Cast<StepIdAttribute>();
                foreach (var appliedAttribute in appliedStepIdAttributes)
                {
                    if (appliedAttribute.StepId == stepId)
                    {
                        return (IWorkflowStep)Activator.CreateInstance(type);
                    }
                }
            }
        }

        throw new Exception("No implementation found");
    }
}

Problem

My problem is that none of the steps are guaranteed to require the same data. For instance, step one might need the following class:

public class Step1Data {
  public string SomeId { get; set; }
}

Whereas step two might need the following class:

public class Step2Data {
  public int MinimumThreshold { get; set; }
  public int MaximumThreshold { get; set; }
}

What I don't want, is for the main loop to become responsible for getting the data for a variety of steps. I want to make it as easy as possible to add new steps and data, so if possible, I want reflection to be able to care of it all.

What I want, is for the instance itself to request the data, that some class picks it up and gives it back to the instance.

My ideas

I was thinking of accomplishing it through delegates and events. To that end, I introduced a delegate:

public delegate void RequestingDataEventHandler<T>(object source, T e);

And added an event to the IWorkflowStep interface:

    public interface IWorkflowStep<T>
    {
        event RequestingDataEventHandler<T> RequestingData;
        Result Execute();
    }

A step could then be like this:

public sealed class FirstStep : IWorkflowStep<Step1Data>
{
    public event RequestingDataEventHandler<Step1Data> RequestingData;

    public Result Execute()
    {
        var myData = new Step1Data();
        if (RequestingData != null)
        {
            RequestingData.Invoke(this, myData);
        }

        // Execute step based on retrieved
    }
}

Then, there could be a class that listens to the event, a StepDataBuilder if you will. There would be one for each T and T could be the Step1Data type or the Step2Data type, etc.. Essentially: T represents the type that should hold the data that the step needs. Something like this:

public class Step1DataBuilder<Step1Data> {
    public void WireEvent(IWorkflowStep<Step1Data> step) {
      step.RequestingData += GetData;
    }

    public void GetData(object source, Step1Data dataObj) {
        dataObj.SomeId = "some id retrieved from somewhere else";
    }
}

However, the above setup requires the loop or outer program to know what type T should be, which I don't think it really doesn't need to at all. I want the outer program to instantiate the classes, wire up the events and to execute the steps. So I'm not really sure what T I should supply, if any. Maybe I can create the classes use Type.MakeGenericType but I'm not too sure if it could accomplish what I want.

Question(s)

  1. What would I need to do to get the above working if I want to supply data using events and delegates?
  2. Is this even a relatively sane approach? It feels a bit convoluted and I'm wondering if there's an easier way that I'm overlooking. An extra layer of indirection, a long-lost design pattern or something else entirely.

If you need any more information, please let me know.





Aucun commentaire:

Enregistrer un commentaire