Need help with workflow responsibility

Need help with workflow responsibility

Old forum URL: forums.lhotka.net/forums/t/614.aspx


hurcane posted on Thursday, July 13, 2006

I hope that I can get some advice from the community on a design issue that is vexing me.

Here is an example workflow scenario that is troubling me. This scenario occurs throughout our business processes, but with different players. We are dealing with a WinForms UI.

In one scenario an editable Order object has a CustomerID property. The order also contains a property for a shipping address, which is its own editable object. When the customer property is set, the order object collaborates with a read-only customer object to set the order's shipping address from the customer's predefined shipping address.

Customers have 0, 1, or multiple predefined shipping addresses. When there are 0 shipping addresses, the order generates a new editable address object. When there is a single address, the order object uses it. When there are multiple addresses, the user is supposed to select the appropriate address (probably from a modal dialog).

The problem I have is deciding who has the responsibility of dealing with the multiple addresses scenario.

Should the UI be responsible for the workflow? In this case, I envision the UI checking the Address property on the Order after the customer ID has been bound. If the address is null, the UI instantiates the address list, which is available through the customer info object. The UI invokes the dialog and assigns the result to the Order.

Should the Order be responsible for the workflow? In this case, I envision the Order raising an event. The event arguments would include a read-only property for the address list and a writable property for the selected address. The UI would respond to this event, invoke the dialog, and set the selected value.

No matter what, I know the UI is involved. Now that I have formally described the scenarios, I can see that my core issue is who should be responsible for initiating the workflow. If the UI starts it, I get a feeling that I'm putting business logic into the UI. If the Order starts the workflow, I get the feeling that I'm coupling the business object too tightly with the UI.

The opinions of others on this group is greatly appreciated. If there's another approach that I haven't considered, I'd be happy to consider that as well.

Thanks in advance!

ajj3085 replied on Thursday, July 13, 2006

Hi,

We were going to do a workflow system, and the idea we had was that the workflow was represented by various queues for each stage, and certain users accessed certain queues.  So there wasn't an object raising an event; to move it along the workflow, its stage changed, which would make it appear in another queue. 

Would that kind of model be approperiate?  I did work on a system like this for a RIS (radiological information system).  Images were taken, needed reviewing would go into the physicans review queue.  He'd mark it as reviewed, and it'd move to another queue for another user.

HTH
Andy

phowatt replied on Thursday, July 13, 2006

I am not sure just how your Order class is designed but this scenario sounds a lot like discussions that were held in the previous forum.  It sounds like to old issue of whether your classes are defined by data or behavior.  When I first hooked up with CSLA it was also my first experience with OOP.  I found myself running into the same wall.  I had used CodeSmith to generate most of my business classes and of course code generated this way produce business classes that were defined by the data tables.  I started have the same problem you describe trying to decide whether the more complicated business rules belonged in the business class or the UI.  I decided my problem had to do with having business classes designed according to my database.  My solution was to use a two tiered business class approach.  I kept the data related business classes like Order, Customer, and so forth but then I also developed a layer of behavior oriented business classes that utilized my data oriented classes.  In your case you might create an OrderManager class that has all of the business logic, or workflow for processing orders.  That way the business logic does not have to go into the UI and the OrderManager class can use the other classes like Order, OrderItem, Customer, CustomerAddresses and so forth as needed.  The data oriented classes can stay simple and clean while the complex business rules can go into you manager classes.

Others, I am sure will have a different approach but I think the problem is related to the data oriented vs behavior oriented design.

RockfordLhotka replied on Thursday, July 13, 2006

Conceptually, workflow is an almost perfect place to practice "pure" OO. A workflow is, by definition, composed of a set of independent, atomic tasks/activities. Each activity must stand alone, with clearly defined inputs and outputs, and a clearly defined business goal that it must achieve.

There's another name for such a thing: a use case.

When you think about it this way, each "step" in your workflow should be treated like a standalone use case. As such, you can bring the full capabilities of OO design to bear on that use case and you can create a set of objects to achieve that (typically) clearly and narrowly defined requirement.

Then you can use a workflow tool, or another object, to orchestrate the workflow itself - calling various activities in the required order. In this light, the workflow merely becomes a higher level use case, consisting of a set of more detailed use cases.

I know this is a higher level point, but I think it is an important one. Especially looking forward to .NET 3.0 and Workflow Foundation - where you really do create a set of standalone activities and string them together work an orchestration tool. Even if you don't choose to use WF in the short term (though you should look at it!), you don't want to be caught in a position where you can't migrate to it in the future.

hurcane replied on Thursday, July 13, 2006

I'm trying to teach myself OOP practices. I clearly have hurdles still to jump, including this specific case. I can read what people are saying conceptually about the problem, but I'm not sure that I'm getting how it is put into practice. I hope that by seeing concrete examples of solving this problem, something might "click".

I am dealing with use cases. Using Alistair Cockburn's model, I am primarily dealing with a seal-level use case that defines the task of creating a new order. For my specific question, I am dealing with a specific field that has side effects. That is apparently a fish-level use case, a sub-case of the primary use case.

Consider this simplified sub-case:
  1. User enters customer ID.
  2. Order assigns predefined address from the Customer to itself.
My implementation for this use case would be to have the Order object collaborate directly with a CustomerInfo object inside the Set of the customer ID. Is it being suggested that this should be handled by an OrderManager object? If so, what is the purpose of the Order object besides holding data? Then it seems to me that my Order object is not much better than a Dataset.

If I did have an OrderManager to do this, where does it fit in my code? I'm using Data Binding, which acts as a controller between the textbox and the Order object. How does the OrderManager know that the Customer ID is being changed? Does all data entry have to pass through the OrderManager before it can go to the Order? If so, does this mean that I can't use data binding? Or does the OrderManager handle the PropertyChanged event of the Order and respond to that?

Or am I misunderstanding the intent of the OrderManager class, and it is not necessary in this simpler use case?

Let me go back to the more complicated example:
  1. User enters Customer ID
  2. Order assigns predefined from the Customer to itself
Exceptions:
2a) Customer has multiple predefined addresses: User Picks an address for the Order
 
Getting really detailed, "User picks an address for the Order" is another use case.
  1. "System" presents a list of addresses.
  2. User selects a specific address.
  3. Address is assigned to Order.
Is this where an OrderManager object is necessary? Is it the OrderManagement object that generates the list, receives the user response, and sets it on the order? I'm still not sure who should be invoking the process, the OrderManagement object (as a controller that replaces data binding) or the Order object (with the OrderManagement object as an observer).

Should I be a little more fine-grained and create an OrderAddressAssignment object that handles the details for this specific sub-case? As I think about it, I could design an object that handles the general use case for "User picks an object for assignment to a property on another object." This makes sense to me, but I still don't know who instantiates this object and provides the contextual data it needs. The object itself or an external manager object? I've read that too many manager/controller objects is an example of a bad OO design. Is this what is meant?

Typing all this has made me even more confused. I guess the core question is how do we transition from a parent use case to a child use case that involves user-interface elements? Who initiates the transition, who responds to the transition, and who completes the transition?

RockfordLhotka replied on Friday, July 14, 2006

Maybe it is just me, but what you are describing doesn't sound like "workflow". Of course anything can be described as "workflow" I suppose...

But my point is that you are describing event-driven UI interaction with an object. Now UI interaction with your business logic can be handled in many ways - block mode processing, event-driven interaction, linear prompting and even workflow (typically through a state machine model).

When programming in a GUI though, the event-driven interaction model has risen to be the defacto standard over the past 15 years or so. This isn't a problem space where most people (outside the Micorosft Workflow Foundation team) typically think about using "workflow" to solve issues.

So no, for this purpose I don't think OrderManager is very useful. In fact any object with the word "manager" or "controller" should be suspect, because those type of objects almost always assume too much control over other objects. Objects should be free to pursue their own responsibility, and not micro-managed by other objects.

To me what you are describing are use cases that should be resolved through the use of collaboration. If the user needs to add an address to the order, I agree with your first two steps - in that the list of addresses needs to be displayed and the user must choose one. But what happens next is that the address is added to the _order.Addresses collection.

So the real question is how to do this. It is one line of code, and the question is whether that line of code goes in the UI, behind the Add Address button, or whether the Add Address button calls an intermediary that contains this line of code.

To me this seems like a lot of code to write to abstract one operation that has already been abstracted by the AddAddress() method on your Addresses collection object.

With all abstractions, you need to ask whether you gain enough from the abstraction to make it worth the cost and complexity of coding and maintaining the abstraction. This is why design patterns are not always useful - sometimes they cause you to abstract things for no tangible reason.

Of course, other times they cause you to abstract things for unforseen reasons that do arise later...

And that's the gamble. Pay the price now to abstract things in the (hope?) that hell breaks loose someday and you need the abstraction. Or don't pay the price now in the hope that you don't need ultimate flexibility in that given area.

I think most experienced developers get some intuitive sense for what's likely to break in the future. What's likely to change, and where they'll likely need abstractions.

The UI is a dicey one though. Because the UI changes more than any other part of the app (in most cases). And so any abstraction layer immediately beneath the UI had better be really, really flexible or it will just get in the way. Data binding is a good example, because it saves you tons of code, but really doesn't do much when you get down to it.

But as you move more and more into the business domain it gets much harder to be flexible enough to be useful. And as soon as your objects start dictating a specific user experience then you are doomed. At that point changing the UI due to the user changing the order of processing or whatever will end up forcing you to change your object model.

In my view, the object model should exist to support the business use case, not the user's whim about how they want to do the work this week. Because next week they'll want to do it differently, and the week after that they'll want a web UI instead of a Windows UI. And your objects (ideally) shouldn't need to change to accomodate all these silly requests.

In other words, the business layer should support the business process, not the detailed user-level process.

So in your order/address example, there's obviously a business use case where users need to add addresses to orders, and the object model needs to support this - in as generic a manner as possible.

Then the UI layer needs to implement the specific flow required by the user. You can do that by putting the code in the forms, or you can create UI layer objects that provide abstractions.

So in the end, your OrderManager is not a business object. It is a UI artifact, and thus is a UI object. It exists to abstract part of the interaction between your particular UI and the underlying business model.

If you implement a web UI it is almost certain that OrderManager would need to be somewhat different, and if the user decides that they'd prefer a wizard-driven Windows UI it would need to be different yet again.

And that's OK, because it is part of the UI layer.

(of course I just realized that it is very late and I should have been asleep a couple hours ago - so perhaps these are just the ramblings of a sleep-deprived mind... Geeked [8-|])

SonOfPirate replied on Friday, July 14, 2006

Just a couple quick follow-up points on Rocky's typically thorough comments - whether rambling or not! :-)

First, Rocky already pointed out the first thing I noticed when reading through the thread: this is not a "workflow" issue.  While there is a process involved, the term "workflow" does typically apply to state management, or, in other words: a process where an object moves between pre-defined 'states' as a result of various 'events'.  In your case, the workflow would be managing the state of the Order, not the content.  So, when first created, the Order may be in a "New Order" state but when all information has been completed and submitted, it would transition to the "Order Pending" state/status and when fulfilled to the "Order Complete" state.  That is where workflow fits in.

Second, on the concept of a "manager" or "controller" class.  I again agree with Rocky, that it is not applicable to what you are doing and should typically be avoided if possible.  Where you will find good justification for such an object is when you want to abstract a superset-type operation that involves multiple object types or you have the need to instantiate a superclass based on some criteria but your code is only dealing with references to the subclass type.  The 'x'Manager class is a business object in and of itself, although most likely not data-driven, that collaborates with other objects to perform the desired operation.  The best example I can think of off the top of my head (unlike Rocky, my problem is that it's too early and the coffee hasn't quite kicked in yet!) is the CacheManager class in the MS Enterprise Library Application Blocks which abstracts the inner workings required to implement all of the flexibility built-into the app block such as multiple cache "backing stores" (objects that all implement the IBackingStore interface), etc.  This object does eventually delegate everything to the actual BO to perform the work, but the "manager" class handles interacting with the configuration file to instantiate and initialize the proper object, etc.

Finally, as for your original question on how to perform the desire task.  I think the key to the solution is to stop separating the process into three distinct operations.  Instead, look at it as three 'variations' on the same.  Let's look at what you say the three variations are and how they vary from the others:

  1. Customer has no address pre-defined.
  2. Customer has exactly one (1) address pre-defined.
  3. Customer has more than one (1) address pre-defined.

All three of these scenerios, presumably, make use of the CustomerInfo classes "Addresses" property.  In the first case, CustomerInfo.Addresses.Count = 0; the second = 1; and, the third > 1.  Since the Addresses property is returning a collection, the contents can be listed easily by binding the collection to a listbox or similar control.  So, if instead of trying to make your UI determine which path to follow, you always display the dialog showing this listbox along with buttons to "Add Address", "Remove Address", "Edit Address" and "Select Address" (or whatever combination apply), it really doesn't matter how many items are listed.

In other words, if the customer has no addresses defined, the list will be empty.  You'll have the "Remove", "Edit" and "Select" buttons' Enabled property tied to having an item selected, so in this case, since there are no items to select, the only button that is available will be the "Add Address" button.

If the customer has exactly one (1) address defined, then there will be only one address shown in the list box.  If it matches the one that goes with the order, the user simply selects it (causing the "Remove", "Edit" and "Select" buttons to enable) and clicks the "Select" button to continue.

If the customer has more than one (1) address, all of them will be listed and the user can select the appropriate one and click "Select" to continue.

So, as you see, these really are the same operations and are just variations on the same.

The beauty of this approach is that it handles the fourth scenario that you didn't include in your posting - what if the address that is to be used for the Order isn't in the customer's list of existing addresses?  In this case it won't matter if they had none, one or many addresses already defined because none of them are the one you want.  You will want your user to be able to add the new address and select it for the Order.  Using the approach and UI elements described above, no matter what the contents of the list are (how many addresses are pre-existing), the user can always click "Add Address" to create a new one, data-binding will update the list, the user can select it and click the "Select" button and assign it to the order.

Of course, it will be up to your UI code to handle the assignment of the selected Address to the Order.  But, since you're using data-binding with everything, your "Select Address" dialog will have a reference, via the listbox, to the actual Address object and that can be used by your OrderForm (?) class to set the assignment in the Order.

That's how I'd approach it and I think this is pretty consistent with most order-entry applications you'll find that support multiple addresses like this.  If you are concerned about speed of operations, may I suggest using a ComboBox control on your order form bound to the same CustomerInfo.Addresses property that will allow for quick selection and have a button adjacent to the combobox with an ellipsis (...) as the text that when clicked brings up your "Select Address" form.  Then, your users will be able to quickly select the desired address when it is available and only have to deal with the dialog when needed.  You can bind the combobox's list to the CustomerInfo.Addresses collection and bind the selected item to the Order.ShippingAddress property and everything will be wired up for you in the UI.

Hope my ramblings help(ed).

 

hurcane replied on Friday, July 14, 2006

First, I want to apologize for misusing the word "workflow". As it happens, I will be dealing with some workflow issues shortly, where the Order object has to be taken from a "quote" state into a "booked" state. That part I seem to have worked out. Smile [:)]

I like Rocky's term of "event-driven UI interaction". The points you guys have made helped me decide this morning that we will have the form handle raising the address dialog and assign the address to the Order. The form will respond to an event from the Order object.

As Rocky pointed out, this interaction is driven by the user's demands, which are rather fickle. If their demands change, it's going to affect the form anyway. It also keeps this change out of the Order object itself, keeping it more generic.

I still have to make a decision: Does the form have primary responsibility with this use case, or does the Order object take primary responsibility and it raises an event for the form to potentially resolve the ambiguity.

Both designs will work. Your responses have given me enough additional points to consider where I think I can determine what's best for our particular circumstances.

Thank you! Big Smile [:D]

SonOfPirate replied on Friday, July 14, 2006

All of your interaction with the user and thereby any activity/operation that is triggered by something that the user does or initiates is going to be coming through the UI.  The event-driven model that has been referred to is going to be based on UI events, such as clicking a button to display the dialog.  The thing to keep in mind is that your BO's are to be interface-agnostic meaning that your Order object should not be dependant upon or directly coupled with your UI.  The beauty of working with Win apps and using data-binding is that this an be accomplished easily.

In the example scenerio I provided earlier, in the event-handler that is triggered when the customer's ID value is supplied to the form, you would initiate binding all bound controls to the newly instantiated CustomerInfo object, including the combobox bound to the CustomerInfo.Addresses property.  That accomplishes that part - all you have to do at the BO level is populate the objects with data.

When the ellipsis button is clicked, that event-handler will instantiate your SelectAddressesDialog, set the Addresses property to the CustomerInfo.Addresses and call the ShowDialog() method.

When an item is selected in the listbox, the event-handler that is triggered will enable the Select, Remove and Edit buttons.

When the Add button is clicked, the corresponding event-handler will instantiate your AddressPropertiesDialog and call the ShowDialog() method.  When it returns, the Address property return value will be passed to the SelectAddressesDialog's Addresses.Add(...) method and the listbox will be refreshed by the data-binding mechanism (through the collection's ListChanged event).

When the Edit button is clicked, that event-handler will instantiate the AddressPropertiesDialog, set the Address property to the selected item in the list box and call the ShowDialog() method.  Nothing else is necessary because data-binding will take care of the rest and automatically update everything with any changes the user makes.

When the Remove button is clicked, that event-handler will pass the selected item to the Addresses.Remove(...) method.  The rest is handled by data-binding.

When the Select button is clicked, the SelectAddressesDialog sets its SelectedAddress property to the selected item and returns.

Back in the ellipsis event-handler, when the dialog returns, the combobox's selected item is set to the dialog's SelectedAddress property.  Any changes to the list will be automatically updated in the combobox via data-binding and setting the selected item will assign the address to the Order since you've bound the Order's ShippingAddress property to the combobox's SelectedItem property.

That is the ideal way for the interaction to take place.  You don't need your Order object to communicate anything to the form.

If you are trying to put logic in your CustomerID property's set accessor as you eluded earlier to set the ShippingAddress property automatically, I would first recommend against this because this automation is a UI 'enhancement' and not necessarily a business logic function (i.e. something to make it easier for the operators and not something that is part of the Order's dominion).  If they decide later to change the way this is done, you'll have to make changes to your business object AND your UI code.  It would be much easier to only have to change the UI.

Furthermore, does your Order object really care what address is assigned or where it came from?  I don't believe so.  To your Order object it is just an address.  And, while it may be required to pass validation, the logic to determine which address to use and whether some UI element needs to be provided so the user can select the correct one is poor design in my books.

Under your approach, putting this logic in the Order object, means that the Order object becomes dependant upon something that happens in the UI.  What happens to the set accessor when this condition arises?  Do you block until the event returns and grab the selected Address object from the custom EventArgs passed with the event?  That violates OO and interface-agnosticity.  Again, your BO should not require, rely on or be dependant on anything to do with your UI.  It is certainly reasonable to set a 'default' address, but to have the Order object trigger display of the form and be connected to this dialog so tightly is a bad idea in my book.

If you want to implement this automation, I would do it in the event that is raised in the form (UI) when the CustomerID is assigned.  So, for instance, you could instantiate the CustomerInfo object then apply the logic of determining which Address to use and if there is more than one (CustomerInfo.Addresses.Count > 1), display your dialog.  Then, if something changes in this approach, you only have to change the UI code and you are able to maintain a better OO design.

That's just my opinion.

 

hurcane replied on Friday, July 14, 2006

SonOfPirate:

Under your approach, putting this logic in the Order object, means that the Order object becomes dependant upon something that happens in the UI.  What happens to the set accessor when this condition arises?  Do you block until the event returns and grab the selected Address object from the custom EventArgs passed with the event?  That violates OO and interface-agnosticity.  Again, your BO should not require, rely on or be dependant on anything to do with your UI.  It is certainly reasonable to set a 'default' address, but to have the Order object trigger display of the form and be connected to this dialog so tightly is a bad idea in my book.

If you want to implement this automation, I would do it in the event that is raised in the form (UI) when the CustomerID is assigned.  So, for instance, you could instantiate the CustomerInfo object then apply the logic of determining which Address to use and if there is more than one (CustomerInfo.Addresses.Count > 1), display your dialog.  Then, if something changes in this approach, you only have to change the UI code and you are able to maintain a better OO design.

That's just my opinion.

 

Opinions are appreciated. I had a discussion with the UI developer and we agreed that the solution that made the most was sense was for the form to handle the use case. The form will handle the PropertyChanged event of the Order object. When the property being changed is the Customer ID, the form will manage the collaboration to get the address set on the order.

The decision was made when we considered the likely possibility down the road that the order entry may be initiated through another process (a web site or EDI). Most likely, these processes will explicitly set the address. They don't need the Order to set a default address.

This should make you happy, because our opinions agree. Wink [;)]

However, your mention about blocking reminded me of another issue we have already dealt with that does put some interaction logic in the business object. When creating a new PurchaseOrderLine object, the ProductID and Quantity are maintained. These properties (and some properties from the header information) are provided to a PurchaseCost object (a command object) to automatically calculate a default value for the Cost property of the PurchaseOrderLine.

The use case stated that if the quantity is changed and the change would result in a new calculated cost, the user should be prompted to confirm whether the new cost should be used (Yes/No), or the change should be canceled (Cancel). This Yes/No/Cancel prompt occurs frequently throughout our use cases.

We are handling the majority of this use case inside the PurchaseOrderLine object. The confirmation is handled by generating a ChangeConfirmation event that provides arguments that include contextual the information to be displayed to the user and a property for recording the user's response. By keeping all the details inside the business objects, are forms are able to handle this interaction with just a couple lines of code.

The key to making this work is that there is a default response ("Yes" in the example I used), which makes the confirmation optional. The confirmation is not tightly bound to the UI. If the UI does handle the event, then we are blocked, but that is a requirement that we can't avoid, no matter what technique would have been used to implement the interaction. Even with the discussion we have had in this thread, I am still comfortable with this design decision.

This demonstrates that there are no absolute right answers in how to implement things. From a certain perspective (a higher level), both of these interactions are the same. But when you drill down into the details, they are solved in different ways.

RockfordLhotka replied on Friday, July 14, 2006

Using events like this is a fine idea. Just make sure you declare the events like I did in Chapter 3 or you'll be in for some serious hurting with serialization...
 
Rocky

We are handling the majority of this use case inside the PurchaseOrderLine object. The confirmation is handled by generating a ChangeConfirmation event that provides arguments that include contextual the information to be displayed to the user and a property for recording the user's response. By keeping all the details inside the business objects, are forms are able to handle this interaction wi th just a couple lines of code.
 
 

SonOfPirate replied on Friday, July 14, 2006

I like the example with the PurchaseOrder.  It's a good, tangible explanation for that sort of problem.  As you brought it up, I was reminded that this could be construed to be the same type of situation.  The reason I wasn't thinking of it is because it was one we already addressed as well (outta sight, outta mind).  The way we handled this was to add a PropertyChanging event that was of the PropertyChangingEventHandler type (derived from System.ComponentModel.CancelEventHandler).  This allowed us to expose the property name, the current value and new value to the client and provide them with the means to cancel the change.  This event could be handled by the UI to present the user with a prompt as you've described.

It's always good to see fellow developers following the same path to confirm there is a method to our madness, eh? Confused [*-)]

Good luck with your app.

hurcane replied on Friday, July 14, 2006

Well, I wasn't declaring events with the Custom modifier. I thought I was fine because I had successfully done tests with a remote data portal. However, our UI wasn't handling these events yet. I did a quick test by adding a handler from a form and trying to Clone my business object. I know what you mean now. Embarrassed [:$]

Our events now are using the block structure. Because of the narrow formatting in the book, the code looked more complicated than it really is.

I have some Friend-scope events. I've kept these as simple event declarations. As long as I ensure all the objects handling these events are serializable, this should not cause me any problems, I think.

Copyright (c) Marimer LLC