Делаем роли явными при создании коммерческой системы

16 июля 2010

Данный пост появился в результате многократного прослушивания лекции 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 роли, IMakeOrders и IMakeCustomerPreferred. Теперь хотелось бы иметь возможность написать на 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);

    IMakeOrders orderMaker = customerRepository.Get<IMakeOrders>(customerId);
    orderMaker.MakeOrder(product, amount);
}

Чтобы это сделать был реализован метод Get в классе Repository:

public TRole Get<TRole>(int id) where TRole:IRole
{
    return (TRole) (Object) session.Get<TEntity>(id);
}

Но в данном случае никто не мешает использовать напрямую поля и методы класса Customer, которые он реализует через интерфейс IMakeOrders, чтобы скрыть их можно воспользоваться интерфейсами с явной реализацией. Тогда метод MakeOrder будет виден только при приведении типа Customer к IMakeOrders. Если Вам это нужно, можно оставить их простыми методами.

Теперь обратимся к роли IMakeCustomerPreferred. Допустим пользователь может стать избранным, только если он сделал более 5 заказов. В таком случае следует заставить репозиторий получить покупателя со всеми его заказами. Это делается при помощи fetching strategy. Изменять её хотелось бы независимо от реализации MakeCustomerPreferred. Поэтому был задан следующий интерфейс:

/// <summary>
/// Sets fetching strategy for specified role
/// </summary>
/// <typeparam name="T"><see cref="IRole"/></typeparam>
public interface IFetchingStrategy<T>
{
    void AddFetchTo(ICriteria criteria);
}

И реализация:

/// <summary>
/// Loads customer with his orders
/// </summary>
public class MakeCustomerPreferred: IFetchingStrategy<IMakeCustomerPreferred>
{
    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<TRole> fetchingStrategy = (IFetchingStrategy<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<IRole>)).GetTypes()
           where t.GetInterfaces().Contains(typeof (IFetchingStrategy<TRole>))
           select t;
}

Метод GetFetchingStrategies получает все типы, реализующие IFetchingStrategy (все стратегии для передаваемой роли). Метод Get создает экземпляр каждой из стратегий по очереди и применяет к запросу. Естественно метод поиска стратегий можно упростить если использовать Service Locator. Последнее что хотелось бы упомянуть, это расположение файлов в проекте, я обычно делаю примерно так:

Содержимое проекта
расположение файлов в проекте

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

Ваш e-mail не будет опубликован. Обязательные поля помечены *