This post is part of a series on building a complex .NET application from scratch. In part 1 I introduced business entities and refactored code out of the data layer. Now I'm going to build out the business logic and add unit tests to support it.
A new BusinessObject project (assembly) has been created and a CustomerBO class added to the project. This class contains the business logic for the customer domain entity. The class diagram for this CustomerBO is shown on the right. As the diagram indicates there are methods to get a customer instance as well as store one. In the case of GetCustomer() an instance of the Customer business entity previously described is returned with fully populated attributes. StoreCustomer() takes a Customer entity instance previously populated by a client layer and passes it to the appropriate data access object for storage.
Looking at the code shown on the left, the red arrow coming in from the left side indicates where an external caller would call in to the CustomerBO class to retrieve a customer instance. The additional parameter includeAddress allows the caller to control how "deep" the retrieval goes. If only basic customer attributes are needed then setting includeAddress to false will return just the "primary" attributes. However, setting includeAddress to true will cause the customer business object to populate associated addresses by passing the customer instance to LoadAddresses. The LoadAddresses method invokes the GetAddresses method which returns the list of associated addresses and then LoadAddresses sets the customer entity's Addresses property to the result.
Since a customer address doesn't have business meaning outside of a customer I've decided not to expose a Customer Address business object. Instead, the few needed methods for manipulating the addresses have been added to the Customer business object. As the code shows, the object is "smart enough" to retrieve, set, and store associated addresses.
Notice that the Customer business entity (a.k.a.the data transfer object) is passed by the CustomerBO business object to the CustomerDAO data access object. The data access object "knows" how and where to persist the attributes of a customer, including the associated addresses. Another approach might be to have the business object, CustomerBO in this case, decompose the business entity and make decisions regarding what to store. Doing so introduces other side effects such as the business layer needing to manage transactional semantics when called upon to store information. That is, if the business object decomposes a Customer entity into its component parts - a Customer and a CustomerAddress - it will have to invoke the data access layer twice, once to store the customer and a second call to the CustomerAddressDAO to store its data. In the event of a failure, the database could be left in an indeterminate state. The usual way to handle this possibility is to wrap both calls inside a transaction. This causes the business objects to have references to and use a transaction manager. The real question is does transactional storage semantics belong up in the business layer or down in the data layer.
Now that the original data layer has been refactored we can return to the unit tests. Firstly, the DataLayer tests have been updated slightly to create instances of a Customer business entity and pass to the CustomerDAO methods. A second set of tests have been added to test the business layer. Once again, by taking the time early on to put the testing framework into place, it is reaping rewards every time we make a change to the code. We're able to exercise each layer as we go and ensure that all the moving parts line up correctly.
The code for this version of the project can be downloaded here. You'll find the new Business Object project as well as updated unit tests.