|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionObject 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 an RDBMS and the code to access it. A possible way to simulate a data access layer is to use mock objects. This article proposed 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 a complete separation of the data access layer? Dependency InversionThe Dependency Inversion Principle, as Robert C. Martin stated in "Agile Principles, Patterns, and Practices in C#", says that:
Dependency inversion separates concerns, and enables a top-down developing process, building high level modules before low level modules. .NET ImplementationApplied to the .NET world, dependency inversion implies that:
The diagram at the beginning of the article shows the architecture of this solution for layer separation. We have four assemblies:
The code included in this article provides a Test project. EntitiesThe sample provided is about business order management. Here is the class diagram that represents the entities in the sample:
InterfacesThe separation we are looking for can be obtained by applying the 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 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 This is the code for the /// 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);
}
A callback mechanism is used to instantiate objects off the data access layer. A delegate, LogicThe business logic classes shouldn't have a direct reference to the DAL classes. Instead, they will reference the 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 public class InMemoryDALAttribute : System.Attribute { }
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();
}
}
}
Methods that need to retrieve data will instantiate public List<Order> GetByCustomerId(Guid customerId)
{
List<Order> lis;
using (IOrdersSession sess = GBDipendencyInjection.GetSession())
{
lis = sess.DbOrders.Search(Guid.Empty, customerId, null, CreateOrder);
}
return lis;
}
In the 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;
}
In Memory Data AccessA singleton class contains a generic list of 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. A cascading find method is applied to search the list. public sealed class OrderInMemoryDAL : IOrderDAL
{
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 The next step will be removing the In the following article, Building an Agile SQL Data Access Layer, I focus on the development of the code needed for SSL Server data access, with sample code extending the one attached to this article.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||