Версия 2. Исправления с учетом комментариев.
Данный пост появился в результате многократного прослушивания лекции Udi Dahanan, которая называлась Intentions and Interfaces - Making Patterns Complete (смотреть тут, если не посмотрите, то дальше в общем то читать будет не интересно).
Главная идея лекции заключается в том, чтобы сделать роли в приложении явными. Т.е. если создается система для электронной коммерции, где можно делать заказы, то явно выделить такие интерфейсы (роли) как IMakeOrders, IPayment и т.д. Это позволит четко разделить части приложения, отвечающие за разную функциональность.
Была затронута небезынтересная тема ORM средств, в частности NHibernate, и приведен следующий пример кода:
public class ServiceLayer
{
public void MakePreferred(Id customerId)
{
IMakeCustomerPreferred c = ORM.Get<IMakeCustomerPreferred>(customerId);
c.MakePreferred();
}
}
Это мне понравилось больше всего, т.е. работая с таким кодом, просто невозможно явно не разделять различные части приложения.
Первая попытка реализовать данный подход это использовать схемы наследования, доступные в NHibernate. Не получилось это сделать по причине отсутствия таблицы для интерфейсов (IMakeOrders, IPayment и т.д.), так же нельзя вводить и колонку descriminator, поскольку один объект может реализовывать более одного интерфейса. В общем чистыми маппингами такого дизайна приложения не достичь.
Udi Dahanan запостил у себя на блоге реализацию своего подхода. Честно сказать – не понравилось. Да, возможно такая реализация как то ближе к DDD или к SOA или еще к куче умных слов, но выделять интерфейс для для каждого бизнес объекта мне кажется излишним.
Так же в примере изменены исходные коды NHibernate, что станет проблемой при выходе новой версии библиотеки.
Я бы предпочел построить эту абстракцию выше, без изменения исходного кода класса Session и прочее. И вот к чему я пришел.
Рассмотрим следующую модель:
Пользователь реализует 2 роли, IOrderMaker и IPreferredMaker. Теперь хотелось бы иметь возможность написать на aspx странице следующий код:
private void MakeOrder(int productId, int customerId, int amount)
{
Repository<Product> productRepository = new Repository<Product>(Session);
Repository<Customer> customerRepository = new Repository<Customer>(Session);
Product product = productRepository.Get(productId);
IOrderMaker orderMaker = customerRepository.Get<IOrderMaker>(customerId);
orderMaker.MakeOrder(product, amount);
}
Чтобы это сделать был реализован метод Get в классе Repository:
public TRole Get<TRole>(int id) where TRole:IRole
{
return (TRole) (Object) session.Get<TEntity>(id);
}
Но в данном случае никто не мешает использовать напрямую поля и методы класса Customer, которые он реализует через интерфейс IOrderMaker, чтобы скрыть их можно воспользоваться интерфейсами с явной реализацией. Тогда метод MakeOrder будет виден только при приведении типа Customer к IOrderMaker. Если Вам это не нужно, можно оставить их простыми методами.
Теперь обратимся к роли IPreferredMaker. Допустим пользователь может стать избранным, только если он сделал более 5 заказов. В таком случае следует заставить репозиторий получить покупателя со всеми его заказами. Это делается при помощи fetching strategy. Изменять её хотелось бы независимо от реализации MakePreferred. Поэтому был задан следующий интерфейс:
/// <summary>
/// Sets fetching strategy for specified role
/// </summary>
/// <typeparam name="TEntity"><see cref="Entity"/></typeparam>
/// <typeparam name="TRole"><see cref="IRole"/></typeparam>
public interface IFetchingStrategy<TEntity, TRole>
{
void AddFetchTo(ICriteria criteria);
}
И реализация:
/// <summary>
/// Loads customer with his orders
/// </summary>
public class MakeCustomerPreferred: IFetchingStrategy<Customer, IPreferredMaker>
{
public void AddFetchTo(ICriteria criteria)
{
criteria.SetFetchMode<Customer>(x => x.Orders, FetchMode.Join);
}
}
Теперь надо заставить репозиторий использовать эту стратегию, сделать это можно следующим образом (изменяем метод Get):
public TRole Get<TRole>(int id) where TRole:IRole
{
ICriteria criteria = session.CreateCriteria(typeof(TEntity));
criteria.Add<TEntity>(x => x.Id == id);
IEnumerable<Type> fetchingStrategies = GetFetchingStrategies<TRole>();
foreach (Type fetchingStrategyType in fetchingStrategies)
{
IFetchingStrategy<TEntity, TRole> fetchingStrategy = (IFetchingStrategy<TEntity, TRole>)Activator.CreateInstance(fetchingStrategyType);
fetchingStrategy.AddFetchTo(criteria);
}
TRole result = (TRole) criteria.UniqueResult();
return result;
}
private static IEnumerable<Type> GetFetchingStrategies<TRole>()
{
return from t in Assembly.GetAssembly(typeof (IFetchingStrategy<TEntity, IRole>)).GetTypes()
where t.GetInterfaces().Contains(typeof (IFetchingStrategy<TEntity, TRole>))
select t;
}
Метод GetFetchingStrategies получает все типы, реализующие IFetchingStrategy<TEntity, IRole> (все стратегии для передаваемой роли и сущности). Метод Get создает экземпляр каждой из стратегий по очереди и применяет к запросу. Естественно метод поиска стратегий можно упростить если использовать Service Locator.
Последнее что хотелось бы упомянуть, это расположение файлов в проекте, я обычно делаю примерно так:
О том как тестировать приложения с таким дизайном я напишу позже.