Nov 30, 2009

Делаем роли явными

Версия 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 и прочее. И вот к чему я пришел.

Рассмотрим следующую модель:

imageПользователь реализует 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.

Последнее что хотелось бы упомянуть, это расположение файлов в проекте, я обычно делаю примерно так:

Содержимое проекта

О том как тестировать приложения с таким дизайном я напишу позже.

Пример кода

5 comments:

  1. Очень удобно если прикладывать исходники к статье

    ReplyDelete
  2. Во! А то пишешь буквари :) Это хоть читать интересно.

    по сути - а зачем там foreach в Get - в примере у тебя стратегия одна на роль (интерфейс). Ты думаешь, что есть ситуации (и это оправданно), когда их будет много?

    Я бы понял, если бы ты искал конкретное совпадение класс:интерфейс, но у тебя действительно ищутся все классы, которые реализуют фетч-стратегию для роли. хорошо, что у тея роль так по-дурацки называеся (про это вообще будет отдельный камент :), а если б она называлась IBecomesFavourite и была релевантна как для кастомера, так и для продукта?

    ReplyDelete
  3. теперь про названия ролей - я понимаю, что доменная модель приведена чисто ради примера, но зачем же так коряво? ubiquitous language наше всё :)

    когда я вижу IMakeCustomerPreferred, то читаю буквально следующее - тот, кто умеет сделать кастомера избранным, что конечно не так.
    правельнее было бы назвать роли таки ролями ([отглагольными] существительными), а не глаголами:
    IOrderMaker
    IPotentialPreferredCustomer

    ReplyDelete
  4. 1. Исходники добавил
    2. foreach в Get потому что теоретически для роли может быть несколько Feching strategy, соответственно применять надо все. А вот как реализовывать их для нескольких сущностей, возможно стоит расширить интерфейс, и сделать что-то наподобие IFetchingStrategy<TEntiy, TRole>, тогда можно будет явно указывать для какой сущности какая роль.
    3. Да, согласен, завтра поправлю

    ReplyDelete