Object oriented design concepts and agile development principles state that data access is a detail and not part of the domain of the application model. This means that the architecture of applications should ensure that the implementation of object persistence and retrieval is hidden from the domain of the application model.
With the complete decoupling of the data access layer comes the possibility to test the business model in isolation by providing a data access layer that mimics a real one but doesn't carry the entire heavy infrastructure needed by a RDBMS and the code to access it.
A possible way to simulate a data access layer is to use mock objects. This blog post proposes a slightly different approach that consists of implementing a simple "In memory" data access and persistence mechanism.
This approach allows deferring the development of the database details, typically involving implementing table structures, sql generation scripts, sql queries and other configurations.
But how do we obtain complete separation of the data access layer?
Dependency Inversion
The Dependency Inversion Principle, as Robert C. Martin stated in "Agile Principles, Patterns and Practices in C#" says that:
- High level modules should not depend upon low level modules. Both should depend upon abstractions.
- Abstractions should not depend upon details. Details should depend upon abstractions.
.NET implementation
Applied to the .net world, dependency inversion implies that:
- The DAL (data access layer) should be in a separate assembly.
- The business logic layer and the DAL should depend upon common interfaces defined in a third assembly.
- The DAL shouldn't have the responsibility of instantiating objects, the business logic layer should.
- Entities: holds definition for the entities in the application model. It doesn't reference any of the other assemblies.
- Logic: contains the application business rules (object creation included). It references Entities and Interfaces.
- Interfaces: contains contracts for low level modules. Data access, in our case. It references Entities.
- Data Access: provides services for persisting and retrieving objects. It references Entities and Interfaces.
Entities
Here is the class diagram that represents the entities:
Interfaces
The separation we are looking for can be obtained by applying abstract factory and facade patterns through the definition of a group of interfaces, one for each entity class, wrapped together by an interface adding connection and transaction management. As in the following class diagram.
The IOrdersSession interface will orchestrate data access functionality provided by all the single entity data access classes.
public interface IOrdersSession: IDisposable
{
IDbTransaction BeginTransaction();
ICustomerDAL DbCustomer { get; }
void Save(Customer customer);
void Delete(Customer customer);
IOrderDAL DbOrder { get; }
void Save(Order order);
void Delete(Order order);
IOrderItemDAL DbOrderItem { get; }
void Save(OrderItem orderItem);
void Delete(OrderItem orderItem);
IProductDAL DbProduct { get; }
void Save(Product product);
void Delete(Product product);
}
This interface derives from IDisposable to ensure that any database connection is closed (and transaction rolled back if still present) at object disposal and to enable the use of the using statement.
This is the code for the Order entity data access interface:
A callback mechanism is used to instantiate objects off the data access layer. A delegate, OrderDataAdapterHandler, is passed as a parameter in the methods that retrieve objects (the search method in our example). A blog post by Scott Stewart is available for detail info about this alternative to using DTOs to retrieve data from the DAL.
/// method to call to instantiate objects
public delegate Order OrderDataAdapterHandler(
Guid id, Guid idCustomer, DateTime orderDate);
/// <summary>
/// Order data access interface
/// </summary>
public interface IOrderDAL
{
List<Order> Search(Guid id, Guid idCustomer,
DateTime? orderDate,
OrderDataAdapterHandler orderDataAdapter);
}
Logic
The business logic classes shouldn't have a direct reference to the DAL classes. Instead, they will reference the IOrdersSession interface and a Dependency injection (DI) mechanism will provide the actual object implementing it.
Dependency injection can be provided by a DI framework (such as NInject or Unity) or, as in the sample code here quoted, by a simple method that instantiates an object implementing IOrdersSession. In this case this is done in a conditional way: business logic classes decorated with the [InMemoryDAL] attribute will be injected with the InMemory access classes.
public class InMemoryDALAttribute : System.Attribute { }Methods that need to retrieve data will instantiate IOrderSession in a using statement and pass as a parameter a factory method with a signature corresponding to the one defined in the entity DAL interface.
public class GBDipendencyInjection
{
public static IOrderdSession GetSession()
{
// get call stack
StackTrace stackTrace = new StackTrace();
// get calling class type
Type tipo = stackTrace.GetFrame(1).GetMethod().DeclaringType;
object[] attributes;
attributes = tipo.GetCustomAttributes(
typeof(InMemoryDALAttribute), false);
if (attributes.Length == 1)
{
return new InMemoryOrderSession();
}
else
{
return new SqlServerOrderSession();
}
}
}
public List<Order> GetByCustomerId(Guid customerId)In the OrderLogic class, CreateOrder is the factory method that instantiates objects of type Order.
{
List<Order> lis;
using (IOrdersSession sess
= GBDipendencyInjection.GetSession())
{
lis = sess.DbOrders.Search(Guid.Empty, customerId, null,
CreateOrder);
}
return lis;
}
In Memory Data Access
public Order CreateOrder(Guid id,
Guid idCustomer,
DateTime orderDate)
{
Order order = new Order();
CustomerLogic costumerLogic = new CustomerLogic();
Customer customer = costumerLogic.GetById(idCustomer);
if (customer == null)
{
throw new ArgumentException("Customer not found.");
}
order.Customer = customer;
order.OrderDate = orderDate;
return order;
}
A singleton class contains a generic list of the objects whose storage is simulated. Since it is a singleton, it maintains the list across threads, thus it can be still used when developing the interface (even a web interface) and defer database development in the last stages of development. Cascading Find method is applied to search the list.
public sealed class OrderInMemoryDAL : IOrderDALInMemoryOrderSession implements IOrdersSession managing saving and deleting simply by adding and removing from the generic lists stored in the singleton objects.
{
internal List<Order> OrderList;
static readonly OrderInMemoryDAL _istance
= new OrderInMemoryDAL();
private OrderInMemoryDAL()
{
OrderList = new List<Order>();
}
internal static OrderInMemoryDAL Istance
{
get { return _istance; }
}
#region IOrderDAL Members
public List<Order> Search(Guid id, Guid idCustomer,
DateTime? orderDate, OrderDataAdapterHandler orderDataAdapter)
{
List<Order> lis = OrderList;
if (id != Guid.Empty)
{ lis = lis.FindAll(
delegate(Order entity) {
return entity.Id == id; });
}
if (idCustomer != Guid.Empty)
{ lis = lis.FindAll(
delegate(Order entity) {
return entity.Costumer.Id == idCustomer; });
}
if (orderDate.HasValue)
{ lis = lis.FindAll(
delegate(Order entity) {
return entity.OrderDate == orderDate; });
}
return lis;
}
#endregion
}
public class InMemoryOrderSession: IOrdersSession
{
...
public IOrderDAL DbOrders
{
get { return OrderInMemoryDAL.Istance; }
}
public void Save(Order order)
{
Delete(order);
OrderInMemoryDAL.Istance.OrderList.Add(order);
}
public void Delete(Order order)
{
OrderInMemoryDAL.Istance.OrderList.RemoveAll(
delegate(Order _entity)
{ return _entity.Id == order.Id; }
);
}
...
}
This approach enables to develop the model and even the user interface without worrying about the database details. This lowers the cost of changes to the model since it is not required to change database structures and sql scripts and queries.
Another advantage is being able to test the business logic in isolation from the database access code.
The last stage of the development of our application will be writing a class implementing IOrdersSession that makes use of a RDBMS through an access library, like ADO.NET, providing connection and transaction services.This class will be a facade for all the single classes that manage CRUD operations on the RDBMS for model entities persistence.
The next step will be removing the [InMemoryDAL] attribute from the business logic class. Now the simple DI mechanism will inject the "real" DAL in the application. Running unit tests on the business logic classes again, the DAL will be tested.
If you're interested to see the sample application you can download the file containing the complete Visual Studio solution.
In next posts I'll focus on the developing of these classes, extending the current sample code and on the CodeSmith templates that can generate much of the glue code necessary for implementing this architecture.
Bookmark this on Delicious
Very good article, I often read articles on topics like this but never this good. Don't forget to read our article which is certainly no less interesting.
ReplyDeletepiala eropa 2021
casino indonesia
ReplyDeleteThe article is very interesting. Thanks for sharing this article. I enjoyed reading your article.
python internship | web development internship |internship for mechanical engineering students |mechanical engineering internships |java training in chennai |internship for 1st year engineering students |online internships for cse students |online internship for engineering students |internship for ece students|data science internships |