This project is read-only.

Contraints on operations

Mar 6, 2011 at 10:35 AM
Edited Mar 6, 2011 at 10:36 AM

Now we can configure a workflow genrically it would look nicer if constraints could be defined for the previous operation.  For exampe, we can configure a workflow like this;

Workflow<StarBucks>().Definition().Do<MakeCoffe>().Do<ChargeCustomer>()

If we want to constrain the ChargeCustomer operation to execute only on the success of MakeCoffee we would currently have to create an MakeCoffee instance;

var makeCoffee = new MakeCoffee();

Workflow<StarBucks>().Definition()
    .Do(makeCoffee)
    .Do<ChargeCustomer>(If.Success(makeCoffee);

I've been thinking about how we can always use the generic syntax.  Here's an example;
Workflow().Definition()
    .Do()
    .Do(If.Success(Step.Previous);


Mar 7, 2011 at 2:51 AM

I don't like the idea. Doing the previous makes more sense to me:

Workflow<StarBucks>().Definition()
    .Do(makeCoffee)
    .Do<ChargeCustomer>(If.Success(makeCoffee));

The Step.Previous idea sounds very procedural. Maybe you have a good idea. If so, I guess I just don't understand it yet. The only way I might want to change the previous is allow the If to be specified first so it reads more like English:

Workflow<StarBucks>().Definition()
    .Do(makeCoffee)
    .If(makeCoffee).IsSuccessful
    .Do<ChargeCustomer>();

Or maybe:

Workflow<StarBucks>().Definition()
    .Do(makeCoffee)
    .IfSuccessful()
    .Do<ChargeCustomer>();

Mar 7, 2011 at 12:43 PM
Edited Mar 7, 2011 at 12:46 PM

I should have started with the problem rather than the solution.  The problem is that when I started the framework is was to help a failing project, therefore I had to meet the requirements quickly.  The initial release had to have some way of conditionally executing an operation.  The easiest way of meeting this was the constraint syntax as a parameter on the Do();

Workflow<StarBucks>().Definition()
    .Do(makeCoffee)
    .Do<ChargeCustomer>(If.Success(makeCoffee));

I've always been concerned that this syntax is not very discoverable.  Additionally, there is now the problem is that it requires a reference to an instance (makeCoffee variable), which doesn't fit in with the new generic Do<T>() syntax.

This is solved by the syntax you have come up with;

Workflow<StarBucks>().Definition()
    .Do(makeCoffee)
    .IfSuccessful()
    .Do<ChargeCustomer>();

However, I've played around with variations on this theme and it always seems to break down when complex conditions are introduced;
Workflow<StarBucks>().Definition()
    .Do(makeCoffee)
    .IfSuccessful()
    .Do<ChargeCustomer>().Otherwise.Do<Something>().Do<SomethingElse>();
Is SomethingElse executed as part of the otherwise clause or executed as part of a new path, after the otherwise clause has completed.  
This would be solved by introducing Begin and End commands for workflows but this seems too artificial.
So I came up with the Step.Previous to remove the need for the instance variable because I haven't decided how to handle conditions other than as a parameter.  I agree that it is procedural.
Mar 7, 2011 at 2:42 PM

Ok, this makes more sense now. Thanks for explaining.

I actually think the .Do<ChargeCustomer>(<<condition>>) is easily discoverable. I only say this because I had no trouble discovering it. As for the syntaxes I come up with, yes they're all faulty and making them accept blocks of .Do()'s would be an epic kludge. You're original syntax is still winning out in my mind.

A question though, perhaps I'm misunderstanding it. What should Step.Previous do? Is it different than Repeat?

Mar 7, 2011 at 9:48 PM
Edited Mar 7, 2011 at 9:52 PM

Repeat is an implementation of the Arbitrary cycles workflow pattern.  

Step.Previous is an argument supplied to the If.Successful() constraint so that it evaluates the previous operation success.  Simply a way to remove the need for an instance variable as in the example;

var firstOperation = new MakeCoffee(); // don't want to have to instantiate this for the constraint to work

Workflow().Definition()
   .Do(makeCoffee)
   .Do<ChargeCustomer>(IfSuccessful(makeCoffee))
Could be refactored as;
Workflow().Definition()
   .Do<MakeCoffee>() // not using an instance variable
   .Do<ChargeCustomer>(IfSuccessful(Step.Previous)) // but need some way to refer to the previous step
I don't think it's great but haven't come up with a better way yet.
Mar 7, 2011 at 11:11 PM

Ok, this is going to sound strangely like I'm advocating the use of goto's but hear me out

What if we add an optional parameter to .Do() methods => 

.Do("Make Coffee")
.Do(IfSuccess(Step.To("Make Coffee")))
We could also allow Step.Previous(-1) or Step.Previous(1) to go forward & back. Although I really don't like the numbered steps because they're starting to sound a lot like the bad parts of GoTo's. Even the labeled Step.To(<<label>>) is a little dangerous.

Mar 8, 2011 at 2:50 AM

I had a thought. We could use "out" parameters.

IFluentStep<T> makeCoffee;
Workflow.Definition()
    .Do<MakeCoffee>(out makeCoffee)
    .Do<ChargeCustomer>(IfSuccessful(makeCoffee));

By using the out parameter we can easily identify which step it is and go back to it. I think in C# 4.0 you don't have to declare makeCoffee before it's used. That would make it even more fluent.

Would this work?

Mar 8, 2011 at 11:15 PM

@djnz_gea - I'm getting to a point in my own project where I need this feature badly. I'm going to go ahead and implement it using the out parameter method. It's going to look like

IDeclaredOperation<T> makeCoffee;
Workflow.Definition()
    .Do<MakeCoffee>(out makeCoffee)
    .Do<ChargeCustomer>(If.Not.Successful(makeCoffee));

OR

IDeclaredOperation<T> makeCoffee;
Workflow.Definition()
    .Do<MakeCoffee>(out makeCoffee)
    .Do(If.Not.IsTrue(x => x.IsCustomerCharged, makeCoffee));
If the constraint fails (in either case) it will loop back to the makeCoffee operation.

Mar 9, 2011 at 8:13 AM
Edited Mar 9, 2011 at 10:06 AM

I've thought about your syntax with the out parameter and can't see what it gives the framework that isn't there already.

You will need a variable declared for every operation you want to test. You can already do this using the instance variable syntax rather than the generic syntax (see below);

Var makecoffee = new MakeCoffee();

Workflow.Do(makecoffee).Do<ChargeCustomer>(if.successful(makecoffee);

The only difference between this and your first syntax only seems to be that the framework creates the instance variable.
Mar 9, 2011 at 3:46 PM
It's more complicated than that:

var w = new Workflow<SiteVisit>()
.Do<OpenVisit>()
.Do<HaveDistrictApprove>(If.Not.IsTrue(/* Check some criteria */, /* Go back to OpenVisit */))
.Do<HaveCorporateApprove>(If.Not.IsTrue(/* Check more criteria, like if District rejected it */, /* Go back to OpenVisit */)
.Do<PostApprovalProcess>(If.Not.IsTrue(/* Corporate rejected it */, /* Go back to OpenVisit */);

Several different steps have to have the ability to reject back to a single step and redo all approval steps after that. From what I understand, the code you have above doesn't allow that.
Mar 9, 2011 at 4:41 PM
Edited Mar 9, 2011 at 5:03 PM

You could do this with a combination of Retry and a Repeat().Until mechanism.  I was planning to add this soon.