Версия 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.
Последнее что хотелось бы упомянуть, это расположение файлов в проекте, я обычно делаю примерно так:
О том как тестировать приложения с таким дизайном я напишу позже.