Every developer who designs and implements software wants to get it right the first time. Nothing is more frustrating than having to scrap or rework a solution after having decided on an approach, and after traveling many miles down the path, because the approach you’ve chosen cannot adapt to new requirements. To help you make more informed decisions about the approaches you take in your .NET applications, I’ll lay out common patterns for representing domain logic in this article, and I'll cover data access code in a future article.
The code for these patterns and the terminology are borrowed from Martin Fowler’s book Patterns of Enterprise Application Architecture from Addison-Wesley, 2003.
There are many different ways of organizing domain logic in components that encapsulate calculations, validations, and other logic that drives the central functionality of the application. Fowler defines three architectural patterns: transaction script, table module, and domain module (here discussed in increasing order of complexity) that designers use to organize domain logic.
This pattern involves creating methods in one or a few business components (classes) that map directly to the functionality that the application requires. As the name implies, each transaction—for example processing an order—is encapsulated in its own script housed in a method of a business component. The body of each method then executes the logic, often starting a transaction at the beginning and committing it at the end (hence the name).
This pattern can be implemented by using multiple transactions per component. This is the most common technique and involves factoring the transactions into higher-level groupings and then creating a business component for each grouping and a public or shared method for each transaction. For example, in a retail application the process of ordering a product involves several steps and can be encapsulated in a PlaceOrder method in the OrderProcessing component, as shown in Listing A.
The second way in which this pattern can be implemented is to separate each transaction into its own component. Using this technique, each transaction script is implemented in its own class, which uses implementation or interface inheritance in order to provide polymorphism (following the command pattern from the GoF). For example, the PlaceOrder and SaveCustomer scripts could be implemented as classes that inherit from the IProcessing interface as shown in Figure A.
This pattern is often the most intuitive but is not as flexible as other techniques and does not lead to code reuse. The benefit of this approach is that it is conceptually straightforward to design by looking at the actions that the application needs to perform. This pattern tends to view the application as a series of transactions.
The second pattern for representing domain logic is the table module. As the name implies, this pattern calls for a business component to map to a table in the database. The component then contains all the domain logic methods required to manipulate the data. There are two key considerations here.
First, although the name refers to a table, it can also be used to abstract frequently used sets of data created by joining multiple tables together in a view or query. This makes dealing with more complicated data much easier for the caller to the business components.
Second, the core characteristic of each component built as a table module is that, unlike domain model (discussed next), it has no notion of identity. In other words, each table module object represents a set of rows rather than a single row, and therefore in order to operate on a single row, the methods of the table module must be passed in identifiers.
When a method of a table module is called, it performs its logic against a set of rows passed into it. In .NET Framework applications, this maps to a DataSet and is therefore a natural way to represent domain logic. In fact, using Typed DataSets with table module is particularly effective since it promotes strong typing at design time, leading to fewer runtime errors and better control over the data flowing through the application.
A common approach to handling this is to create a Layer Supertype (an abstract base class for the domain layer) for all of the table modules that accept a DataSet in their constructors. This also allows the business components to be tested without a live connection to a database.
For example, to implement a table module approach in a retail application you could create an abstract BusinessComponentBase class like the one shown in Listing B.
This class is responsible for accepting the DataSet that the class will work on, exposing individual rows in the DataSet using the default (VB) or indexer (C#) property, and exposing the entire set of rows in a read-only property. This class can then be inherited by domain logic classes as shown in Figure B.
This pattern takes advantage of the ADO.NET DataSet and is a good midway point between the other two. This pattern tends to view the application as sets of tabular data.
The third pattern for representing domain logic in an application is the domain model. In this pattern the application is viewed as a set of interrelated objects. The core feature of these objects is that, unlike the table module approach, each object maps to an entity (not necessarily a database table) in the database. In other words, each primary object represents a single record in the database rather than a set of records and the object couples the object’s data (its properties) with its behavior (business logic and data validation methods and events). Additional objects may represent calculations or other behavior. Ultimately, this approach requires at least the following:
- Object-relational mapping. Since this pattern does not rely on the DataSet object or even Typed DataSet objects, a layer of mapping code is required. At present the .NET Framework does not contain built-in support for this layer. Look for support in the ObjectSpaces technology to be released in the Whidbey release of VS.NET. Techniques to perform this mapping will be covered in the next article.
- ID generation. Since each domain model object maps to an entity in the database, the objects need to store the database identity within the object (the CLR’s object system uniquely identifies each object based on its address). Several techniques to generate and store IDs include using system assigned keys, GUIDs, and using the Identity Field Pattern documented by Fowler.
- Strongly-typed collection classes. Because objects in the domain model map intrinsically to an individual entity, representing multiple objects is handled through collections. .NET Framework developers can therefore create strongly-typed collection classes to handle representing multiple domain objects.
The end result of a domain model is that the application manages many individual and interrelated objects (using aggregation) during a user’s session. Although this may seem wasteful, the CLR’s efficiency at managing objects makes the domain model a valid approach. Such an approach would not have been efficient in the world of COM, however.
To build an object used in a domain model, a best practice is to follow the Layer SuperType pattern. Using this pattern, an architect would develop a base class from which all domain objects would inherit, as shown in Figure C.
The advantage of the domain model is that the logic needed for any particular operation is loosely coupled across the objects rather than being contained in a single method. This pattern tends to view the application as a set of interrelated objects.
Which one is best?
So which architectural pattern should be used in a .NET Framework application? All three patterns discussed have pros and cons:
- Transaction Script. This is the easiest to understand conceptually and the quickest to develop. Fewer layers of abstraction mean that there is the potential for duplicate code and more difficult rework as requirements change.
- Table Module. This pattern offers a good midway point in complexity between the other two. Fits very well with the DataSet in ADO.NET but is not fully object-oriented.
- Domain Model. This is the most flexible option and the most purely object-oriented. Does not rely on ADO.NET so heavily from a public perspective and so is insulated from changes to ADO.NET in newer releases of the .NET Framework.