SOA, TDD, DI & DDD - игра-оболочка? - PullRequest
1 голос
/ 31 марта 2011

Хорошо, я попытаюсь пойти коротко и прямо к сути.Я пытаюсь разработать слабосвязанное многоуровневое сервисное приложение, которое можно тестировать и поддерживать внедрение зависимостей.Вот что у меня есть:

На уровне службы у меня есть метод StartSession, который принимает некоторые ключевые данные, необходимые для начала сеанса.Мой класс обслуживания является фасадом и делегируется экземпляру интерфейса ISessionManager, который внедряется в конструктор класса обслуживания.

Я использую шаблон Repository на уровне доступа к данным.Итак, у меня есть ISessionRepository, с которым будут работать мои доменные объекты, и который я реализую, используя технологию доступа к данным du jour.В ISessionRepository есть методы для GetById, Add и Update.

Поскольку мой класс обслуживания - это просто фасад, я думаю, можно с уверенностью сказать, что моя реализация ISessionManager является реальным классом обслуживания в моей архитектуре.Этот класс координирует операции с моим доменом / бизнес-объектом Session.И вот тут возникает проблема с игрой и проблемой оболочки.

В моем классе SessionManager (конкретный ISessionManager) вот как я реализовал StartSession:

public ISession StartSession(object sessionStartInfo)
{
    var session = Session.GetSession(sessionStartInfo);

    if (session == null)
        session = Session.NewSession(sessionStartInfo);

    return session;
}

У меня три проблемы с этим кодом:

  • Во-первых, очевидно, что я мог бы переместить эту логику в метод StartSession в моем классе Session, но я думаю, что это противоречило бы цели класса SessionManager, который затем просто становится вторым фасадом (или это все ещесчитать координатором?).Увы, игра в оболочку.
  • Во-вторых, SessionManager тесно связан с классом Session.Я подумал о создании ISessionFactory / SessionFactory, который можно было бы внедрить в SessionManager, но тогда у меня была бы такая же тесная связь внутри фабрики.Но, может, все в порядке?
  • Наконец, мне кажется, что истинные DI и фабричные методы не смешиваются.В конце концов, мы хотим избежать «нового» экземпляра объекта и позволить контейнеру возвращать его нам.И истинный DI говорит, что мы не должны ссылаться на контейнер напрямую.Итак, как же мне получить конкретный класс ISessionRepository, вставленный в мой объект домена Session?Нужно ли вводить его в фабричный класс, а затем вручную передавать его в Session при создании нового экземпляра (используя «new»)?

Имейте в виду, что это также только одна операция, и мне также нужно выполнять другие задачи, такие как сохранение сеанса, вывод списка сеансов на основе различных критериев плюс работа с другими объектами домена в моем решении.Кроме того, объект Session также инкапсулирует бизнес-логику для авторизации, проверки и т. Д., Поэтому (я думаю) он должен быть там.

Ключ к тому, чего я хочу достичь, не только функциональный, но и тестируемый.Я использую DI для разрушения зависимостей, чтобы мы могли легко выполнять модульные тесты, используя макеты, а также дать нам возможность вносить изменения в конкретные реализации, не требуя изменений во многих областях.

Можете ли вы помочь мне обернуть головувокруг лучших практик для такого дизайна и как я могу наилучшим образом достичь своих целей для твердого решения SOA, DDD и TDD?

ОБНОВЛЕНИЕ

Меня попросили предоставитьнекоторый дополнительный код, настолько краткий, насколько это возможно:

[ServiceContract()]
public class SessionService : ISessionService
{
    public SessionService(ISessionManager manager) { Manager = manager; }

    public ISessionManager Manager { get; private set; }

    [OperationContract()]
    public SessionContract StartSession(SessionCriteriaContract criteria)
    {
        var session = Manager.StartSession(Mapper.Map<SessionCriteria>(criteria));

        return Mapper.Map<SessionContract>(session);
    }
}

public class SessionManager : ISessionManager
{
    public SessionManager() { }

    public ISession StartSession(SessionCriteria criteria)
    {
        var session = Session.GetSession(criteria);

        if (session == null)
            session = Session.NewSession(criteria);

        return session;
    }
}

public class Session : ISession
{
    public Session(ISessionRepository repository, IValidator<ISession> validator)
    {
        Repository = repository;
        Validator = validator;
    }

    // ISession Properties

    public static ISession GetSession(SessionCriteria criteria)
    {
        return Repository.FindOne(criteria);
    }

    public static ISession NewSession(SessionCriteria criteria)
    {
        var session = ????;

        // Set properties based on criteria object

        return session;
    }

    public Boolean Save()
    {
        if (!Validator.IsValid(this))
            return false;

        return Repository.Save(this);
    }
}

И, очевидно, есть интерфейс ISessionRepository и конкретный класс XyzSessionRepository, который, я думаю, не нужно показывать.

2-е ОБНОВЛЕНИЕ

Я добавил зависимость IValidator в объект домена Session, чтобы проиллюстрировать, что используются другие компоненты.

Ответы [ 3 ]

2 голосов
/ 01 апреля 2011

Просто некоторые комментарии ...

В конце концов, мы хотим избежать «нового» экземпляра объекта и позволить контейнеру возвращать этот экземпляр нам.

это не правда на 100%. Вы хотите избежать «нового» только через так называемые швы , которые в основном являются линиями между слоями. если вы пытаетесь абстрагировать постоянство с помощью репозиториев - это шов , если вы пытаетесь отделить модель домена от пользовательского интерфейса (классическая ссылка - system.web), то есть шов если вы находитесь на одном уровне, то отсоединение одной реализации от другой иногда не имеет смысла и просто добавляет дополнительную сложность (бесполезная абстракция, конфигурация контейнера ioc и т. д.). Другая (очевидная) причина, по которой Вы хотите что-то абстрагировать, - это когда вам уже сейчас нужен полиморфизм.

И истинный DI говорит, что мы не должны ссылаться на контейнер напрямую.

это правда. но другая концепция, которую вы могли бы упустить, это так называемый составной корень (хорошо, когда у вещей есть имя :). эта концепция разрешает путаницу с «когда использовать сервисный локатор». Идея проста - Вы должны составить свой график зависимостей как можно быстрее. должно быть только 1 место, где вы на самом деле ссылаетесь на контейнер ioc.

например. в приложении asp.net mvc общая точка компоновки: ControllerFactory.

Нужно ли вводить его в фабричный класс, а затем вручную передавать его в Session при создании нового экземпляра

Как я пока вижу, фабрики, как правило, хороши для 2 вещей:

1.Для создания сложных объектов ( Шаблон Builder значительно помогает)
2. Устранение нарушений открыто закрыто и единая ответственность принципы

public void PurchaseProduct(Product product){
  if(product.HasSomething) order.Apply(new FirstDiscountPolicy());
  if(product.HasSomethingElse) order.Apply(new SecondDiscountPolicy());
}

становится как:

public void PurchaseProduct(Product product){
  order.Apply(DiscountPolicyFactory.Create(product));
}

Таким образом, ваш класс с PurchaseProduct не нужно будет изменять, если появится новая политика скидок, и PurchaseProduct будет нести ответственность только за покупку продукта, а не знать, какую скидку применить.

P.s. если вас интересует DI, прочитайте «Внедрение зависимостей в .NET» от Mark Seemann .

2 голосов
/ 31 марта 2011

Размещенный код многое проясняет. Мне кажется, что класс сеанса содержит состояние (с поведением), а классы service и manager строго выполняют действия / поведение.

Вы можете посмотреть на удаление зависимости Repository от Session и добавление ее в SessionManager. Таким образом, вместо вызова сеанса Repository.Save (this) ваш класс Manager будет иметь метод Save (сеанс ISession), который затем будет вызывать Repository.Save (сеанс). Это будет означать, что контейнером не нужно будет управлять самим сеансом, и было бы совершенно разумно создать его с помощью «new Session ()» (или с использованием фабрики, которая делает то же самое). Я думаю, что тот факт, что методы Get и New в Session являются статическими, является признаком того, что они могут не принадлежать этому классу (этот код компилируется? Похоже, вы используете свойство экземпляра в статическом методе).

Наконец, мне кажется, что настоящий ДИ и фабричные методы не смешиваются. После все, мы хотим избежать "нового" экземпляр объекта и пусть контейнер вернуть экземпляр нам. И правда DI говорит, что мы не должны ссылаться на контейнер напрямую. Так, как же тогда получить бетон Класс ISessionRepository внедрен в мой объект домена сеанса? Есть ли у меня это впрыскивается в заводской класс, то вручную передать его в сессию, когда создание нового экземпляра (используя "Новый")

Этот вопрос задают много, когда речь идет об управлении классами, которые смешивают состояние и обслуживание через контейнер IOC. Как только вы используете абстрактную фабрику, которая использует «new», вы теряете преимущества инфраструктуры DI от этого класса вниз в графе объектов. Вы можете избавиться от этого, полностью разделив состояние и службу, и имея только ваши классы, которые обеспечивают службу / поведение, управляемое контейнером. Это приводит к передаче всех данных через вызовы методов (или функциональное программирование). Некоторые контейнеры (Windsor для одного) также обеспечивают решение этой самой проблемы (в Windsor это называется Factory Facility).

Редактировать: хотел добавить, что функциональное программирование также приводит к тому, что Фаулер назвал бы "моделями анемичной области". Это обычно считается плохой вещью в DDD, поэтому вам, возможно, придется взвесить это против совета, который я опубликовал выше.

1 голос
/ 12 апреля 2011

Я думал, что опубликую подход, которому я в итоге следовал, отдавая должное выше.

Прочитав некоторые дополнительные статьи по DDD, я наконец-то натолкнулся на замечание о том, что наши доменные объекты не должны отвечать за их создание или сохранение, а также на мысль, что "новый" экземпляр объекта домена можно использовать с в доменном слое (как ускользнул Арнис).

Итак, я сохранил свой класс SessionManager, но переименовал его в SessionService, чтобы было понятнее, что это доменная служба (не путать с SessionService на уровне фасада). Теперь оно реализовано так:

public class SessionService : ISessionService
{
    public SessionService(ISessionFactory factory, ISessionRepository repository)
    {
        Factory = factory;
        Repository = repository;
    }

    public ISessionFactory Factory { get; private set; }
    public ISessionRepository Repository { get; private set; }

    public ISession StartSession(SessionCriteria criteria)
    {
        var session = Repository.GetSession(criteria);

        if (session == null)
            session = Factory.CreateSession(criteria);
        else if (!session.CanResume)
            thrown new InvalidOperationException("Cannot resume the session.");

        return session;
    }
}

Класс Session теперь больше относится к истинному объекту домена, связанному только с состоянием и логикой, которые требуются при работе с Session, такими как свойство CanResume, показанное выше, и логикой проверки.

Класс SessionFactory отвечает за создание новых экземпляров и позволяет мне по-прежнему внедрять экземпляр ISessionValidator, предоставленный контейнером, без прямой ссылки на сам контейнер:

public class SessionFactory : ISessionFactory
{
    public SessionFactory(ISessionValidator validator)
    {
        Validator = validator;
    }

    public ISessionValidator Validator { get; private set; }

    public Session CreateSession(SessionCriteria criteria)
    {
        var session = new Session(Validator);

        // Map properties

        return session;
    }
}

Если кто-то не укажет на недостаток в моем подходе, мне очень приятно, что это согласуется с DDD и дает мне полную поддержку модульного тестирования и т. Д. - все, что я делал после.

...