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.
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.
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... )
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:
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).
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.
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.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.
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.
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?
Good luck with your app.
Copyright (c) Marimer LLC