Робимо ролі явними при створенні комерційної системи

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 адреса не оприлюднюватиметься. Обов’язкові поля позначені *