طراحی گردش کاری با استفاده از State machines - قسمت دوم
نویسنده: وحید نصیری
تاریخ: ۱۳۹۱/۱۰/۱۱ ۲۳:۱۰
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
using System;
using Stateless;
namespace StatelessTests
{
class Program
{
static void Main(string[] args)
{
try
{
string on = "On", off = "Off";
var space = ' ';
var onOffSwitch = new StateMachine<string, char>(initialState: off);
onOffSwitch.Configure(state: off).Permit(trigger: space, destinationState: on);
onOffSwitch.Configure(state: on).Permit(trigger: space, destinationState: off);
Console.WriteLine("Press <space> to toggle the switch. Any other key will raise an error.");
while (true)
{
Console.WriteLine("Switch is in state: " + onOffSwitch.State);
var pressed = Console.ReadKey(true).KeyChar;
onOffSwitch.Fire(trigger: pressed);
}
}
catch (Exception ex)
{
Console.WriteLine("Exception: " + ex.Message);
Console.WriteLine("Press any key to continue...");
Console.ReadKey(true);
}
}
}
}
public class StateMachine<TState, TTrigger>
<statemachine xmlns="http://statelessdesigner.codeplex.com/Schema">
<settings>
<itemname>BlogPostStateMachine</itemname>
<namespace>StatelessTests</namespace>
<class>public</class>
</settings>
<triggers>
<trigger>Save</trigger>
<trigger>RequireEdit</trigger>
<trigger>Accept</trigger>
<trigger>Reject</trigger>
</triggers>
<states>
<state start="yes">Begin</state>
<state>InProgress</state>
<state>Published</state>
<state>Rejected</state>
</states>
<transitions>
<transition trigger="Save" from="Begin" to="InProgress" />
<transition trigger="Accept" from="InProgress" to="Published" />
<transition trigger="Reject" from="InProgress" to="Rejected" />
<transition trigger="Save" from="InProgress" to="InProgress" />
<transition trigger="RequireEdit" from="Published" to="InProgress" />
<transition trigger="RequireEdit" from="Rejected" to="InProgress" />
</transitions>
</statemachine>
using Stateless;
namespace StatelessTests
{
public class BlogPostStateMachine
{
public delegate void UnhandledTriggerDelegate(State state, Trigger trigger);
public delegate void EntryExitDelegate();
public delegate bool GuardClauseDelegate();
public enum Trigger
{
Save,
RequireEdit,
Accept,
Reject,
}
public enum State
{
Begin,
InProgress,
Published,
Rejected,
}
private readonly StateMachine<State, Trigger> stateMachine = null;
public EntryExitDelegate OnBeginEntry = null;
public EntryExitDelegate OnBeginExit = null;
public EntryExitDelegate OnInProgressEntry = null;
public EntryExitDelegate OnInProgressExit = null;
public EntryExitDelegate OnPublishedEntry = null;
public EntryExitDelegate OnPublishedExit = null;
public EntryExitDelegate OnRejectedEntry = null;
public EntryExitDelegate OnRejectedExit = null;
public GuardClauseDelegate GuardClauseFromBeginToInProgressUsingTriggerSave = null;
public GuardClauseDelegate GuardClauseFromInProgressToPublishedUsingTriggerAccept = null;
public GuardClauseDelegate GuardClauseFromInProgressToRejectedUsingTriggerReject = null;
public GuardClauseDelegate GuardClauseFromInProgressToInProgressUsingTriggerSave = null;
public GuardClauseDelegate GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit = null;
public GuardClauseDelegate GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit = null;
public UnhandledTriggerDelegate OnUnhandledTrigger = null;
public BlogPost()
{
stateMachine = new StateMachine<State, Trigger>(State.Begin);
stateMachine.Configure(State.Begin)
.OnEntry(() => { if (OnBeginEntry != null) OnBeginEntry(); })
.OnExit(() => { if (OnBeginExit != null) OnBeginExit(); })
.PermitIf(Trigger.Save, State.InProgress , () => { if (GuardClauseFromBeginToInProgressUsingTriggerSave != null) return GuardClauseFromBeginToInProgressUsingTriggerSave(); return true; } )
;
stateMachine.Configure(State.InProgress)
.OnEntry(() => { if (OnInProgressEntry != null) OnInProgressEntry(); })
.OnExit(() => { if (OnInProgressExit != null) OnInProgressExit(); })
.PermitIf(Trigger.Accept, State.Published , () => { if (GuardClauseFromInProgressToPublishedUsingTriggerAccept != null) return GuardClauseFromInProgressToPublishedUsingTriggerAccept(); return true; } )
.PermitIf(Trigger.Reject, State.Rejected , () => { if (GuardClauseFromInProgressToRejectedUsingTriggerReject != null) return GuardClauseFromInProgressToRejectedUsingTriggerReject(); return true; } )
.PermitReentryIf(Trigger.Save , () => { if (GuardClauseFromInProgressToInProgressUsingTriggerSave != null) return GuardClauseFromInProgressToInProgressUsingTriggerSave(); return true; } )
;
stateMachine.Configure(State.Published)
.OnEntry(() => { if (OnPublishedEntry != null) OnPublishedEntry(); })
.OnExit(() => { if (OnPublishedExit != null) OnPublishedExit(); })
.PermitIf(Trigger.RequireEdit, State.InProgress , () => { if (GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit != null) return GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit(); return true; } )
;
stateMachine.Configure(State.Rejected)
.OnEntry(() => { if (OnRejectedEntry != null) OnRejectedEntry(); })
.OnExit(() => { if (OnRejectedExit != null) OnRejectedExit(); })
.PermitIf(Trigger.RequireEdit, State.InProgress , () => { if (GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit != null) return GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit(); return true; } )
;
stateMachine.OnUnhandledTrigger((state, trigger) => { if (OnUnhandledTrigger != null) OnUnhandledTrigger(state, trigger); });
}
public bool TryFireTrigger(Trigger trigger)
{
if (!stateMachine.CanFire(trigger))
{
return false;
}
stateMachine.Fire(trigger);
return true;
}
public State GetState
{
get
{
return stateMachine.State;
}
}
}
}
namespace StatelessTests
{
public class BlogPostManager
{
private BlogPostStateMachine _stateMachine;
public BlogPostManager()
{
configureWorkflow();
}
private void configureWorkflow()
{
_stateMachine = new BlogPostStateMachine();
_stateMachine.GuardClauseFromBeginToInProgressUsingTriggerSave = () => { return UserCanPost; };
_stateMachine.OnBeginExit = () => { /* save data + save state + send an email to admin */ };
_stateMachine.GuardClauseFromInProgressToPublishedUsingTriggerAccept = () => { return UserIsAdmin; };
_stateMachine.GuardClauseFromInProgressToRejectedUsingTriggerReject = () => { return UserIsAdmin; };
_stateMachine.GuardClauseFromInProgressToInProgressUsingTriggerSave = () => { return UserHasEditRights; };
_stateMachine.OnInProgressExit = () => { /* save data + save state + send an email to user */ };
_stateMachine.OnPublishedExit = () => { /* save data + save state + send an email to admin */ };
_stateMachine.GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit = () => { return UserHasEditRights; };
_stateMachine.OnRejectedExit = () => { /* save data + save state + send an email to admin */ };
_stateMachine.GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit = () => { return UserHasEditRights; };
}
public bool UserIsAdmin
{
get
{
return true; // TODO: Evaluate if user is an admin.
}
}
public bool UserCanPost
{
get
{
return true; // TODO: Evaluate if user is authenticated
}
}
public bool UserHasEditRights
{
get
{
return true; // TODO: Evaluate if user is owner or admin
}
}
// User actions
public void Save() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.Save); }
public void RequireEdit() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.RequireEdit); }
// Admin actions
public void Accept() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.Accept); }
public void Reject() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.Reject); }
}
}
_stateMachine.OnPublishedExit = () => { /* save data + save state + send an email to admin */ };
_stateMachine.GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit = () => { return UserHasEditRights };
public BlogPostStateMachine(State state)
{
stateMachine = new StateMachine<State, Trigger>(state);
به عنوان مثال اگر Trigger1 همان رویداد Save ما بوده که با توجه به Domain ای که در آن قرار داریم انجام آن در حالتهای مشخص شده رویداد معتبری باشد، ماشین حالت بالا صحیح میباشد یا اینکه باید با یک رویداد دیگری به حالت State1 برگشته و دوباره انتقال به حالت State2 را انجام دهیم. البته متوجه هستم که به ازای تک تک رویدادها همیشه و نه لزوما باید دکمه متناظری را باتوجه به حالت فعلی شیء مورد، توسط کاربر قابل مشاهده باشد. با توجه به اینکه اگر کاربر نهایی به ازای تک تک رویدادهای موجود، لزوما دکمههای متناظری را در فرم مشاهده نکند و نیاز باشد یک دکمه Shortcut مانندی قرار بگیرد که احتمال دارد باعث چندین انتقال شود، Fire کردن چندین رویداد در پشت صحنه، کار اصولی میباشد؟
public InvoiceStateMachine(InvoiceModel model)
{
_stateMachine =
new StateMachine<InvoiceStatus, Trigger>(() => model.Status, state => model.Status = state); public Func<InvoiceModel, Task> OnSaveAsync = null; private readonly StateMachine<InvoiceStatus, Trigger>.TriggerWithParameters<InvoiceModel> _saveTrigger;
_stateMachine.Configure(InvoiceStatus.Pending)
.OnEntryFromAsync(_saveTrigger, async invoice => await OnSaveAsync(invoice)) private void InitializeStateMachine(InvoiceModel model)
{
_stateMachine = new InvoiceStateMachine(model)
{
OnSaveAsync = async invoiceModel =>
{
var result = model.IsNew() ? await CreateAsync(model) : await EditAsync(model);
if (!result.Succeeded)
throw new BusinessRuleValidationException(result.Message);
}
};
} [Transactional]
public async Task<Result> SaveAsync(InvoiceModel model)
{
try
{
InitializeStateMachine(model);
await _stateMachine.TryFireSaveAsync(model);
return Success();
}
catch (BusinessRuleValidationException e)
{
return Failed(e.Message);
}
} // فقط یک ترد امکان دسترسی به کد را داشته باشد
private static readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1,1);
public async Task RunAsync()
{
// شروع تمام تردها معلق خواهند شد تا این سمافور به پایان برسد
// پس از پایان کار ترد جاری، فقط یک ترد در حال انتظار، مجوز دسترسی به قطعه کد بعدی را خواهد یافت و به همین ترتیب برای سایر تردها
await _semaphoreSlim.WaitAsync();
try
{
await Task.Delay(20000); // Your code here
}
finally
{
_semaphoreSlim.Release();
}
}