web-dev-qa-db-ja.com

.NETステートレスステートマシンをカプセル化する方法

ほぼ線形のワークフローがあるプロジェクトがあります。 .NETステートレス ライブラリ を使用してワークフローエンジン/ステートマシンとして機能しようとしています。そこにある例の数は限られていますが、私は次のコードをまとめました:

private StateMachine<WorkflowStateType, WorkflowStateTrigger> stateMachine;
private StateMachine<WorkflowStateType, WorkflowStateTrigger>.TriggerWithParameters<Guid, DateTime> registrationTrigger;
private Patient patient;

public Patient RegisterPatient(DateTime dateOfBirth)
{
    configureStateMachine(WorkflowState.Unregistered);
    stateMachine.Fire<DateTime>(registrationTrigger, dateOfBirth);
    logger.Info("State changed to: " + stateMachine.State);
    return patient;
}

private void configureStateMachine(WorkflowState state)
{
    stateMachine = new StateMachine<WorkflowState, WorkflowTrigger>(state);

    registrationTrigger = stateMachine.SetTriggerParameters<DateTime>(WorkflowTrigger.Register);

    stateMachine.Configure(WorkflowState.Unregistered)
        .Permit(WorkflowTrigger.Register, WorkflowStateType.Registered);

    stateMachine.Configure(WorkflowState.Registered)
        .Permit(WorkflowTrigger.ScheduleSampling, WorkflowState.SamplingScheduled)
        .OnEntryFrom(registrationTrigger, (dateOfBirth) => registerPatient(dateOfBirth));
}

private void registerPatient(DateTime dateOfBirth)
{
    //Registration code
}

ご覧のとおり、トリガーを渡すことができるステートレスFire()オーバーロードを使用しています。これは、ステートマシンにビジネスロジック(この場合は、新しい患者を登録するためのコード)を処理させるためです。

これはすべて機能しますが、すべてのステートマシンコードを別のクラスに移動してカプセル化したいと思います。これを行うのに問題があります。これを行う際に私が抱えていた課題は次のとおりです。

  • StateMachineオブジェクトをインスタンス化するには、状態を指定する必要があります。Stateは、インスタンス化時にのみ設定できる読み取り専用プロパティです。
  • my registrationTriggerは、ステートマシンの構成中にインスタンス化する必要があり、呼び出し元のクラスでも使用できる必要があります。

これらの項目を克服し、ステートマシンコードをカプセル化するにはどうすればよいですか?

11
im1dermike

スコット・ハンゼルマンによる 記事 の例とライブラリの紹介があります。また、ステートマシンをカプセル化するScottの記事で言及されている バグの実装例 など、GitHubで利用できる例はほとんどありません。

以下は、動作から状態を抽出する方法の例です。

public class PatientRegistrationState
{
    private StateMachine<WorkflowState, WorkflowTrigger> stateMachine;
    private StateMachine<WorkflowState, WorkflowStateTrigger>.TriggerWithParameters<DateTime> registrationTrigger;

    public PatientRegistrationState(State initialState = default(State)) {
        stateMachine = new StateMachine<WorkflowState, WorkflowTrigger>(initialState);

        stateMachine.Configure(WorkflowState.Unregistered)
            .Permit(WorkflowTrigger.Register, WorkflowStateType.Registered);

        stateMachine.Configure(WorkflowState.Registered)
            .Permit(WorkflowTrigger.ScheduleSampling, WorkflowState.SamplingScheduled)
            .OnEntryFrom(registrationTrigger, (date) => OnPatientRegistered(date));
    }

    public WorkflowState State => stateMachine.State;
    public Action<DateTime> OnPatientRegistered {get; set;} = (date) => { };

    // For state changes that do not require parameters.
    public void ChangeTo(WorkflowTrigger trigger)
    {
        stateMachine.Fire<DateTime>(trigger);
    }

    // For state changes that require parameters.
    public void ChangeToRegistered(DateTime dateOfBirth)
    {
        stateMachine.Fire<DateTime>(registrationTrigger, dateOfBirth);        
    }

    // Change to other states that require parameters...
}

public class PatientRegistration
{
    private PatientRegistrationState registrationState;
    private Patient patient;

    public PatientRegistration()
    {
        registrationState = PatientRegistrationState(WorkflowState.Unregistered)
        {
            OnPatientRegistered = RegisterPatient;
        }
    }

    public Patient RegisterPatient(DateTime dateOfBirth)
    {
        registrationState.ChangeToRegistered(dateOfBirth);
        logger.Info("State changed to: " + registrationState.State);
        return patient;
    }

    private void RegisterPatient(DateTime dateOfBirth)
    {
        // Registration code
    }
}
7
Andrii Litvinov

これが私のプロジェクトでそれを達成した方法です。

クラスを分離するための分離されたワークフローロジック。リクエストオブジェクトに存在するフラグの1つに基づいたワークフローがいくつかありました。以下はワークフロークラスの1つです。

_public class NationalWorkflow : BaseWorkflow
{
    public NationalWorkflow(SwiftRequest request) : this(request, Objects.RBDb)
    { }

    public NationalWorkflow(SwiftRequest request, RBDbContext dbContext)
    {
        this.request = request;
        this.dbContext = dbContext;
        this.ConfigureWorkflow();
    }

    protected override void ConfigureWorkflow()
    {
        workflow = new StateMachine<SwiftRequestStatus, SwiftRequestTriggers>(
           () => request.SwiftRequestStatus, state => request.SwiftRequestStatus = state);

        workflow.OnTransitioned(Transitioned);

        workflow.Configure(SwiftRequestStatus.New)
            .OnEntry(NotifyRequestCreation)
            .Permit(SwiftRequestTriggers.ProcessRequest, SwiftRequestStatus.InProgress);

        workflow.Configure(SwiftRequestStatus.InProgress)
            .OnEntry(ValidateRequestEligibility)
            .Permit(SwiftRequestTriggers.AutoApprove, SwiftRequestStatus.Approved)
            .Permit(SwiftRequestTriggers.AdvancedServicesReview, SwiftRequestStatus.PendingAdvancedServices);

.....................
}
_

これは、コントローラー/その他のレイヤーからトリガーされます。

_private static void UpdateRequest(SwiftRequestDTO dtoRequest)
    {
            var workflow = WorkflowFactory.Get(request);
            workflow.UpdateRequest();
    }
_

前述のように、リクエストオブジェクトの条件に基づいて異なるワークフロールールがあったため、ファクトリパターンWorkflowFactory.Get(request)を使用しました。ワークフローのインスタンスを作成/必要に応じて挿入できます

そして、ワークフロークラス(私の場合はBaseWorkflowクラス)内で、アクションを公開しました。

_    public void UpdateRequest()
    {
        using (var trans = this.dbContext.Database.BeginTransaction())
        {
            this.actionComments = "Updating the request";
            this.TryFire(SwiftRequestTriggers.Update);

            SaveChanges();
            trans.Commit();
        }
    }

  protected void TryFire(SwiftRequestTriggers trigger)
    {
        if (!workflow.CanFire(trigger))
        {
            throw new Exception("Cannot fire " + trigger.ToString() + " from state- " + workflow.State);
        }
        workflow.Fire(trigger);
    }
_
4
Developer