Introduction
There are many of us out there who routinely develop applications involving a 3-tier architecture. By splitting the solution into 3 logical layers - presentation, business logic and data access, we better our development efforts and increase maintainability.
This article focuses on a design approach to the main layers - the business logic and data modelling layers, and tries to formulate a mechanism that allows re-usability and extensibility as much as possible.
While the core idea is to develop a model that supports in-built state management, first we need to become familiar with some terminology we will use in this article to get ahead.
Domain and Model Objects
With regards to an application, the domain refers to the business entities that are involved. For example, in a banking application, the domain objects include Customer
, Account
and Transaction
. Domain objects can contain domain objects; a simple example being Organization
containing Employee
s. Or Customers
having more than one Account
.
The model of an application refers simply as to the way in which domain objects are described. For example, for the domain object Customer
, the model object will consist of Name
, AccountNumber
, Branch
and Occupation
.
Note that for every domain object, we should have at least one model object.
The domain objects reside in the business logic layer. The model objects act as a sort of bridge between the business logic layer and the actual data access layer. Put in a graphical form, we have the following:
State Management
All entities in a domain undergo some sort of state transition. Initially they have to be created, later on fetched, modified and saved and at some point in time, deleted.
This article focuses on providing a set of classes and a design approach which aims to ease state management of domain objects. By encapsulating the functionality required to create, save, load, update and delete domain objects, we arrive at a solution that allows one to focus on the business logic involved. And moreover, as the technique is re-usable, this speeds up development.
To get an idea of what is in store, consider the Customer
example. And the following code snippet:
Customer newCustomer = new Customer("Jack", "457454876", "SimpleBank");
newCustomer.Save();
...
newCustomer.Details.Name = "Jack Fuller";
newCustomer.Save();
newCustomer.Save();
...
newCustomer.Delete();
Note how easy it is to handle creation, modification and deletion of the domain object Customer
. The internal implementation takes care of state, and because of this, while the first Save()
method leads to creation of the entity, the second call leads to updation. The third call to Save
does nothing as no modification has been made.
Using the Code
Implementing a Sample
The set of classes that are part of the framework allow us to extend them to suit our application's purpose. Let us first see the method to adopt when extending these classes for general use. We will proceed with a simple example of managing an Employee
. By following how the Employee
model and domain objects are realized, it should be easy for you to start using the base classes for your own needs.
The Employee
application is straightforward. We need a mechanism to maintain Employee
data, and each Employee
will have a Name
. To realize this, we have the Employee
domain object, and the EmployeeModel
model object.
All the layers are implemented as class libraries. (The sample solution file is organized in this manner, and will be used as a reference for this article; you are advised to open the solution file and go through it side by side when you read this article). The main application layers are:
Citrus.Core.Test.Domain
- The domain layer (Employee
goes here)
Citrus.Core.Test.Model
- The model layer (EmployeeModel
goes here)
Citrus.Core.Test.Data
- The data access layer
Citrus.Core.Test
- The application's presentation layer
Citrus.Core
- The core classes for the framework
Defining the Model
In our application, every employee will have a name. The EmployeeModel
class for this will look like below:
public class EmployeeModel : ModelObject
{
private Wrap<string> _name = new Wrap<string>();
public string Name
{
get { return _name.Value; }
set { _name.Value = value; }
}
public EmployeeModel(DomainObject container) : base(container)
{
_name.ValueUpdated += new Wrap<string>.ValueUpdateEventHandler(NotifyContainer);
}
}
Let's go about this line by line. The first thing to note is that EmployeeModel
extends the ModelObject
class. The ModelObject
class should be the base class for any data model layer entity. As mentioned earlier, every model entity will be contained in a domain entity (EmployeeModel
in Employee
). The containing domain entity is called the container for the model entity. The ModelObject
class provides in-built functionality for notifying its container whenever it gets updated.
Every Employee
has a Name
, and this is implemented as a property. Since we want changes that are made to Name
monitored, the property encapsulates a Wrap<>
field _name
. The Wrap<>
class helps in monitoring value updated being performed on a contained object.
private Wrap<string> _name = new Wrap<string>();
public string Name
{
get { return _name.Value; }
set { _name.Value = value; }
}
The next thing in line is the constructor for the model object. Notice how it is written. Any model layer object must follow this pattern to correctly function. The constructor accepts a DomainObject
container, and first calls the ModelObject
's constructor to do its necessary setup. After that, any property (example Name
) that needs to be monitored for changes needs to be wired up to the ModelObject
's NotifyContainer
method. In our example, therefore, we have _name
's ValueUpdated
event hooked up.
public EmployeeModel(DomainObject container) : base(container)
{
_name.ValueUpdated += new Wrap<string>.ValueUpdateEventHandler(NotifyContainer);
}
That's it. Our EmployeeModel
object is now ready to be used by the domain object, Employee
, which we define next.
Defining the Domain
The implementation of the Employee
domain object is a bit more involving, and requires a few basic concepts to be understood first.
The first concept is that there are four basic operations that can be performed on domain objects:
- Create a new domain object - Create a new employee
- Load an existing domain object - Load an existing employee
- Update an existing domain object - Change the name of an employee
- Delete an existing domain object - Delete an employee
Therefore, any domain object has to support these operations, and this interface is defined by IDomainObject
:
public interface IDomainObject
{
void CreateEntity();
void LoadEntity(ModelObject source);
void UpdateEntity();
void DeleteEntity();
}
Any domain object that is defined must implement the IDomainObject
interface explicitly. Implementing it explicitly is important as we do not want these methods to be accessible directly by clients.
The next concept, one that has been already mentioned earlier, is that every domain object will have a model object. Therefore, the Employee
domain object will contain the EmployeeModel
model object.
The last concept is understanding the way in which new domain objects are constructed. In code, when you create a new domain object instance, it could either be for a new domain object (one that is not already present in the data store), or it could be to represent a domain object that is already present in the data store. There are several mechanisms to implement domain objects considering these two scenarios, and this article will describe one possibility.
Now with all concepts in our head, let us see the Employee
class in its entirety.
public class Employee : DomainObject, IDomainObject
{
private EmployeeModel _details = null;
public EmployeeModel Details
{
get { return _details; }
set { _details = value; }
}
public Employee() : base(DomainObjectState.New)
{
_details = new EmployeeModel(this);
}
public Employee(string name) : base(DomainObjectState.Clean)
{
DataFields["Name"] = name;
_details = new EmployeeModel(this);
}
#region IDomainObject Members
void IDomainObject.CreateEntity()
{
EmployeeData.Create(Details);
}
void IDomainObject.LoadEntity(ModelObject EmployeeDetails)
{
EmployeeData.Load(EmployeeDetails, DataFields);
}
void IDomainObject.UpdateEntity()
{
EmployeeData.Update(Details);
}
void IDomainObject.DeleteEntity()
{
EmployeeData.Delete(Details);
}
#endregion
}
As before, a line-by-line analysis warrants an opportunity here. The first thing to note is that Employee
extends DomainObject
and explicitly implements the IDomainObject
interface. Next, it contains the EmployeeModel
model object Details
. Lastly, it has two constructors, one for creating new domain objects, another for loading existing ones.
Let us have a closer look at the constructors:
public Employee() : base(DomainObjectState.New)
{
_details = new EmployeeModel(this);
}
The first constructor is relatively simple. It first calls the base class constructor passing the New
DomainObjectState
to indicate that a new domain object is being created. Inside the constructor, the EmployeeModel
field _details
is initialized, setting Employee
as the domain container for the model.
The second constructor is a bit more interesting. To load up an existing Employee
from the data store, we need some key values (e.g.: the primary keys) that define that employee. In our simple case, it's the Name
field. Therefore, we have the name as a required parameter to load the existing Employee
object.
public Employee(string name) : base(DomainObjectState.Clean)
{
DataFields["Name"] = name;
_details = new EmployeeModel(this);
}
The first difference between this constructor and the last one is the Clean
DomainObjectState
that is passed to the base constructor. This indicates that this domain object corresponds to an existing object. Now, we need a mechanism to load the data corresponding to this Employee
onto the EmployeeModel
object. If you look at the constructor code, you may not find any mechanism that fills out the EmployeeModel
with the existing employee data. The only thing noticeable is that we create a new EmployeeModel
object and leave it at that. However, internally, the ModelObject
constructor that is eventually invoked figures out that the Employee
domain object is in Clean
state, and so calls the LoadEntity()
IDomainObject
method. Within the LoadEntity()
method you explicitly implement in Employee
you can write the necessary initialization code for the model object. The LoadEntity()
method is called with a reference to the model entity that was just created. (This is required because if a domain object contains multiple model objects, there needs to be a mechanism to identify which model object called the LoadEntity()
method.)
The calls go along from Employee.Employee()
-> EmployeeModel.EmployeeModel()
-> Employee.LoadEntity()
via the IDomainObject
interface.
That's it. We have now completed the domain layer and the data model layer. For the purpose of this article, the data access layer is simple enough in that it just prints to the Console what needs to be done at every stage. The data access layer will not be discussed.
Finally, some application of what we have done so far. A simple program shows how to use the classes:
class Program
{
static void Main(string[] args)
{
Employee newEmployee = new Employee();
Employee newEmployee2 = new Employee("Jack Filler");
newEmployee.Details.Name = "Jack";
newEmployee.Save();
newEmployee.Details.Name = "Jack12";
newEmployee.Save();
newEmployee.Save();
newEmployee = newEmployee2;
newEmployee.Save();
Console.ReadKey();
}
}
With that, we come to the end of how to extend the base classes for developing our custom layers. As for exactly how the base classes are implemented is left as an exercise to the reader. The following are present in the base class library Citrus.Core
.
DomainObjectState
- An enumeration listing all the possible states a domain model object can have
IDomainObject
- An interface every DomainObject
must implement
DomainObject
- The base class for every domain object that is to be abstracted in the business logic layer
ModelObject
- The base class for every data model object that is to be abstracted
Wrap<>
- The generic utility class that helps in monitoring updates being made to a wrapped object
Hope that gets you started.
Points of Interest
Advancing Ahead: Domain Object Containers
Domain objects can become very complex at times. There could be scenarios in which domain objects contain other domain objects. To facilitate for these scenarios, the DomainObject
class includes another DomainObject
called the Container
object. This can be used to implement IsAChildOf
, HasAParent
, IsASiblingOf
, etc. relationships. The following flags help in determining the behaviour when the Container
object is used:
NotifyContainerOnStateChange
- Should the object notify its container when its state changes
MarkContainerOnStateChange
- Should the object mark its container as dirty when its own state changes
MarkSelfAsDirtyOnContainerChange
- Should the object mark itself as dirty when the container changes
MarkSelfAsDirtyOnContainerStateChange
- Should the object mark itself as dirty when the container's state changes
Describing these scenarios is out of the scope of this article, but is relatively easy to implement if required.
Wrap Me Up!
The Wrap<>
class that is described here came out of an earlier experiment of mine to develop a generic data access layer revolving around the concepts of attribute based programming. The Wrap<>
class is extremely useful in various scenarios ranging from implementing a CommandHistory
classes, internal triggers, monitors, etc.
History
Credits
The ideas described here are not entirely my own. I have seen many colleagues implement all sorts of techniques for business, data model and data access layers, most of them which revolve around similar concepts described here. I just went ahead, collected bits and pieces of ideas and mashed them up together. There is still more to be done, and better approaches may exist; I hope that what I have done becomes useful to you in one way or another. If you have suggestions to improve upon and extend these classes, do let me know.
Enjoy!
History
- 29th August, 2007: Initial post