На главную Наши проекты:
Журнал   ·   Discuz!ML   ·   Wiki   ·   DRKB   ·   Помощь проекту
ПРАВИЛА FAQ Помощь Участники Календарь Избранное RSS
msm.ru
Модераторы: maxim84_
  
> ASP.NET MVC , Размышления об архитектуре типичного приложения
    Сегодня мы попробуем вместе с Вами заложить надежный архитектурный фундамент под приложение, разрабатываемое с использованием технологии ASP.NET MVC.
    Конечно же, предложенный ниже подход не является истинно правильным в последней инстанции, тем не менее, многое из того, что будет описано, является приличной отправной точкой для старта собственных изысканий и модификаций и может быть использовано в реальных приложениях, которые пишите Вы сами.
    В рамках статейного примера, мне приходится рассматривать предельно простую бизнес-модель приложения, но уверяю Вас, что для рассмотрения архитектурных моментов это нам не помешает.

    Начнем?

    Приложения, которые мы пишем, оперируют данными и слой доступа к данным необходимый минимум из того, что нам необходимо реализовать при работе с ними. Встает вопрос: Как его правильно реализовать? Существует огромное число умных книжек, которые описывают огромное число паттернов и возможных подходов к решению. Мой личный опыт подсказывает, что требования зачастую индивидуальны и единого универсального решения не существует. Как всегда нужно рассматривать огромное число факторов и пытаться найти золотую середину. Стоит рассматривать вопросы производительности, безопасности, качества получаемого решения, удобства сопровождения, рентабельности и многие другие индивидуальные факторы. Все это влияет как на выбор технологии для реализации, так и на выбор соответствующей архитектуры.
    Мне кажется, нам следует оттолкнуться от чего более конкретного. Предположим, что перед нами стоит задача реализовать приложение для заказчика средней руки в максимально короткие сроки с перспективой сопровождения нами продукта в будущем. Пусть это будет гипотетическая Front-End система генерации и доставки рекламного контента для вино-водочной компании X. Это приложение нацелено на активную работу с данными, предполагает наличие подсистемы отчетности и отдельного приложения в виде административного интерфейса по управлению этими самыми данными, с другой стороны приложение не предполагает сколь либо сложной бизнес логики присущей многим финансовым системам. Наличие активной работы с данными и отсутствие сколь либо сложной логики наталкивает на мысль использовать тонкую модель работы с данными, где данные, по сути, выступают доменом приложения. Многие апологеты толстой модели уже взвыли, к этому моменту, по крайней мере, я надеюсь на это, - не люблю крайности. Реальность такова, что Microsoft ориентируется именно на тонкие модели при разработке приложений любой сложности, а потому оставим споры философам и продолжим ход своих рассуждений. Могу лишь заметить, что сам придержусь центристских позиций в этом вопросе. Подсистема отчетности, наверняка потребует приличного числа обращений к БД, более того ряд запросов наверняка будут иметь солидную сложность вовлекая в выборки большое число таблиц. Требования к производительности для данной подсистемы являются критичными. Что же мы можем предложить? Сжатые сроки, а это нормально для бизнеса, подталкивают нас смотреть на объектно-реляционные преобразователи, которые де-факто уже являются стандартом при разработке для малых и средних систем приложений, но подсистема отчетности требует производительности не характерной для ОРМ. Есть ли выход? Есть. Мы будем использовать промежуточный абстрактный слой между ОРМ и нашим доменом, который скроет от нас работу с ОРМ и предоставит общий программный интерфейс для доступа к данным. В случае необходимости, мы сможем переписать (или написать изначально) нужные места без применения ОРМ библиотеки используя чистый ADO.NET и хранимые процедуры, которые смогут обеспечить приемлемый уровень производительности в приложении. Параллельно мы сохраним единую модель доступа к любым данным в приложении, через абстрактный слой. Это в перспективе может позволить нам практически безболезненно изменять реализацию слоя доступа к данным, а также позволит иметь единый слой контроля обращения к данным как с использованием ОРМ, так и без нее, что может быть использовано в различных сценариях имеющих отношение к безопасности.
    Я предлагаю определить простой интерфейс подобный нижеприведенному:

    ExpandedWrap disabled
          public interface IRepository<T> where T : class
          {
              T Load(int id);
       
              IList<T> ListAll();
              IList<T> Find(Func<T, bool> whereClause);
              IList<T> Load(int pageNumber, int pageSize, out int total);
              IList<T> Load(Func<T, bool> whereClause, int pageNumber, int pageSize, out int total);
       
              void Insert(T obj);
              void Save(T obj);
              void Update(T obj);
              void Delete(T obj);
          }


    Этот интерфейс только стартовая точка, в действительности он приведен просто для демонстрации общей концепции, но суть должна быть ясна уже сейчас, если клиент будет использовать похожий интерфейс для доступа к данным он будет оставаться в неведении о том, что происходит за «фасадом», а следовательно код клиента не будет завязан на конкретную технологию работы с данными. Тут стоит обратить внимание, что реализация подобного интерфейса не есть классической реализацией паттерна Repository, однако преследует схожие с ним цели.
    Часто многие .NET разработчики определяют более универсальные методы, по указанию запросов используя методы вроде:

    ExpandedWrap disabled
      IQueryable<T> Query<Y>(Expression<Func<T, bool>> where);


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

    Так же иногда в репозитории объявляются методы, нацеленные исключительно на выборку данных, а CRUD операции помещаются в некий глобальный контекст, который ассоциируется с сеансом работы с БД. Такой подход неплохо смотрится в двухзвенных приложениях, хотя и имеет порядочное число критиков, однако смею вас заверить, что для многих приложений часто бывает просто удобно поступать именно так, особенно это касается упомянутых уже двухзвенных приложений.

    С большой долей вероятности, в обсуждаемом приложении нам придется реализовывать IRepository<T> в двух вариациях для ORM и для прямых обращений к базе данных, а потому мне кажется логичным использовать абстрактные базовые классы. Они помогут инкапсулировать общие моменты реализации конкретных репоситориев. Для примера, но позже, я приведу реализацию такого класса, который работает с Linq to Sql, как наиболее простое решение.
    Однако перед этим я позволю себе пофилософствовать о некоторых особенностях реализации такого класса. Как я упоминал, мне бы хотелось скрыть от клиента детали реализации слоя обращения к данным. В частности, мне бы не хотелось, что бы клиент взаимодействовал с контекстом данных напрямую, было бы логично предположить, что контекст может быть агрегирован в такой класс, но мой опыт подсказывает, что это не лучшее решении. Как известно контекст в Linq to Sql связан с физическим соединением с базой данных, а следовательно было бы полезно иметь контекст внешним по отношению к репозиторию особенно это бывает очевидно тогда когда нам необходимо работать с несколькими репозиториями в рамках единой транзакции. Другой аргумент это не желание создавать зависимости между репозиторием и контекстом. Более того локальный контекст лично мне кажется не уместным т.к. конкретный контекст после построения модели будет наследником стандартного DataContext, а значит, что реализация абстрактного класса будет зависеть от модели построенной по конкретной базе данных, что кажется нонсенсом. Я просто уверен, что контекст должен быть внешним, но при этом постоянное написание кода в стиле:

    ExpandedWrap disabled
                  using (var context = new BlogDataContext())
                  {
                      var messageProvider = new MessagesProvider(context);
                      int total = 0;
                      IList<Message> messages = messageProvider.Load(0, 20, out total);
                  }


    Удручает и убивает во мне программиста … обещаю к концу статьи мы избавимся от использования подобного кода в ASP.NET MVC приложениях.

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

    ExpandedWrap disabled
          public class DataRepository
          {
              protected DataContext context = …;
       
              protected static DataRepository _repository = new DataRepository();
              protected static DataRepository Instance { get { return _repository; } }
       
              protected IRepository<Message> messageProvider = new MessagesProvider(Instance.context);
        protected IRepository<User> userProvider = new MessagesProvider(Instance.context);
       
              public static IRepository<Message> MessageProvider
              {
                  get { return Instance.messageProvider; }
              }
       
              public IRepository<User> UserProvider
              {
                  get { return Instance.userProvider; }
              }
       
              …
          }


    За использование подобного подхода говорит удобство использования класса клиентом.

    ExpandedWrap disabled
      IList<Message> messages = DataRepository.MessageProvider.ListAll();
      User current = DataRepository.UserProvider.LoadById(...);


    Клиентский код максимально просто писать и код оперирующий данными максимально унифицирован – «мечта клиента». Более того, можно легко реализовать фиктивную версию реестра, что позволит писать Unit тесты не обращающиеся на прямую к данным. Легко организовать контроль транзакций в приложении, для этого необходимо в DataRepository добавить методы BeginTransaction, Commit, Rollback, а также обеспечить возможность использования единственного контекста в рамках работы каждого из репозиториев.

    Тут же стоит обратить внимание на то, что время жизни объекта DataRepository обычно совпадает с временем сеанса работы клиента с ним. Следовательно нам будет необходимо обеспечить методы по корректному освобождению ресурсов, что в результате так или иначе заставит нас реализовывать IDisposable и использовать using или операции на подобии Open(), Close().

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

    Размышление о фабриках и локаторах было уместным, оно же позволит нам найти альтернативное решение (или же использовать схожий подход, но несколько в другом ключе). Подход к тому же максимально удобный для ASP.NET MVC приложения. Кто не догадался речь пойдет об использовании Dependency Injection и Inversion of control.
    С помощью библиотек-контейнеров на подобие StructureMap или Spring удобно разрешать зависимости, более того, многие из этих библиотек имеют удобные средства для управления жизненным циклом объектов, ну и в довесок код написанный с их использованием является слабосвязанным, а следовательно легко поддается тестированию. (Вариант с модификацией реестра с их использованием выходит за рамки рассматриваемых мной концепции, но основываясь на примере ниже, Вы без труда реализуете подобное и для реестра.)

    Я отдаю предпочтение библиотеке Unity. Unity, с моей точки зрения, солидная библиотека, обладающая всем необходимым и практически лишенная слабых мест. Если же приводить причины, почему ею не стоит пользоваться, то, наверное, единственная это та, что нет некоторых вещей из «коробки» доступных в паре других библиотек. Что служит оправданием для мужчин слабых духом :)

    И так вернемся к предмету. Я хочу «впрыснуть» контекст в провайдер посредством Unity, более того я собираюсь переложить на ее плечи и всю ответственность за контролем времени жизни этого самого контекста, что позволит программисту сложить с себя обязанность освобождать ресурсы явно. Все это будет делаться автоматически. Звучит многообещающе.

    Вернемся же к базовому репозиторию (и когда мы о нем говорили в последний раз?:)) ниже приведена возможная реализация для Linq to Sql:

    ExpandedWrap disabled
         public abstract class L2SRepository<T> : IRepository<T> where T : class
          {
              protected DataContext _context;
       
              public L2SRepository(DataContext context)
              {
                  _context = context;
              }
       
              #region IDataRepository<T> Members
       
              public abstract T Load(int id);
       
              public virtual IList<T> ListAll()
              {
                  return _context.GetTable<T>().ToList();
              }
       
              public virtual IList<T> Find(Func<T, bool> whereClause)
              {
                  return _context.GetTable<T>().Where(whereClause).ToList();
              }
       
              public virtual IList<T> Load(int pageNumber, int pageSize, out int total)
              {
                  var result = _context.GetTable<T>().Skip((pageNumber * pageSize) - 1).Take(pageSize).ToList();
                  total = result.Count();
                  return result;
              }
       
              public virtual IList<T> Load(Func<T, bool> whereClause, int pageNumber, int pageSize, out int total)
              {
                  var result = _context.GetTable<T>().Where(whereClause).Skip((pageNumber * pageSize) - 1).Take(pageSize).ToList();
                  total = result.Count();
                  return result;
              }
       
              public virtual void Save(T obj)
              {
                  _context.GetTable<T>().InsertOnSubmit(obj);
       
              }
       
              public virtual void Update(T obj)
              {
                  _context.GetTable<T>().Attach(obj, true);
       
              }
       
              public virtual void Insert(T obj)
              {
                  _context.GetTable<T>().InsertOnSubmit(obj);
       
              }
       
              public virtual void Delete(T obj)
              {
                  _context.GetTable<T>().DeleteOnSubmit(obj);
       
              }
       
              protected DataContext Context { get { return _context; } }
       
              #endregion
          }


    Зависимость впрыскивается в класс, через конструктор. Методы умышленно сделаны виртуальными, я предполагаю (я просто уверен в этом :)), что конкретные реализации зачастую будут иметь необходимость изменять реализацию работы методов базового класса. Реализация «в лоб» некторые методы, полагаются на особенности реализации контроля обнаружения конфликтов, к примеру Update отработает исключительно для сущностей замапленых на таблицы с timestamp или version, но для демонстрации основной идеи больше и не нужно.
    Дополнительно также стоит заметить, что реализация не затрагивает некоторых моментов, которые очевидно будут востребованы в нашем приложении, но я считаю себя просто обязанным об этом упомянуть. Часто вам потребуется фетчинг данных и было бы неплохо иметь поддержку его настройки и использования его, а наследник мог бы переопределять стратегии фетчинга в своей реализации. Другим важным аспектом является управление разрешением конфликтов и базовые аспекты могут быть реализованы здесь по месту, с правом предоставления стратегии разрешения потомкам, что конечно не мешает реализовать стратегию «по умолчанию».

    Осталось показать тривиальную реализацию типичного репозитория:

    ExpandedWrap disabled
          public class MessagesProvider : L2SRepository<Message>
          {
              public MessagesProvider(DataContext context)
                  : base(context)
              {
              }
       
              public override Message Load(int id)
              {
                  return Context.GetTable<Message>().Where(item => item.Id == id).SingleOrDefault();
              }
          }


    В него же можно добавлять специфические методы по получению данных.

    Вот код программной регистрации объектов в контейнере:

    ExpandedWrap disabled
      IUnityContainer container = new UnityContainer();
                  container.Configure<InjectedMembers>().ConfigureInjectionFor<BlogDataContext>(new InjectionConstructor(connectionString));
                  container.RegisterType<DataContext, BlogDataContext>();
      container.RegisterType<IRepository<Message>, MessagesProvider>();


    Я указываю контейнеру использовать конструктор принимающий connectionString при конструировании экземпляра контекста.
    Теперь репозиторием можно пользоваться так:

    ExpandedWrap disabled
      IRepository<Message> provider = Container.Resolve<IRepository<Message>>();
      Message message = provider.Load(1);


    На заметку: регистрацию можно осуществить и декларативно посредством конфигурационного файла.

    Отлично движемся дальше. На очереди ASP.NET MVC, а именно контроллеры. Я предлагаю написать собственную фабрику для инстанцирования контроллеров или воспользоваться бесчисленными примерами из сети интернет.

    ExpandedWrap disabled
          public class UnityControllerFactory : DefaultControllerFactory
          {
              IUnityContainer container;
       
              public UnityControllerFactory(IUnityContainer container)
              {
                  this.container = container;
              }
       
              protected override IController GetControllerInstance(Type controllerType)
              {
                  try
                  {
                      if (!typeof(IController).IsAssignableFrom(controllerType))
                          throw new ArgumentException(
                              "Type is not a controller”,
                              "controllerType");
       
                      return container.Resolve(controllerType) as IController;
                  }
                  catch
                  {
                      return null;
                  }
              }
          }


    Суть этой фабрики, заменить стандартную фабрику ASP.NET MVC, это позволит нам истанцировать контроллеры посредством Unity, что даст нам возможность впрыскивать в них нужные зависимости, например нужные репозитории :)

    Теперь контроллер в приложении может выглядеть вот так расчитывая на то, что вышепреведенная фабрика без труда инстанцирует котроллер:

    ExpandedWrap disabled
          public class MessageController : Controller
          {
                      IRepository<Message> _repository;
       
              public MessageController(IRepository<Message> repository)
              {
                  _repository = repository;
              }
              …
          }


    А зависимость, будет разрешена контейнером на основе конфигурации.

    Вот пример настройки установки нужной фабрики в файле global.asax:

    ExpandedWrap disabled
                  ControllerBuilder.Current.SetControllerFactory(new UnityControllerFactory(container));


    Вероятно, в том сценарии в котором я собираюсь использовать фабрику, контейнер можно было бы устанавливать через соответствующие свойство в UnityControllerFactory.

    ExpandedWrap disabled
              IUnityContainer container;
       
              public IUnityContainer Container
              {
                  set { container = value; }
              }


    А саму фабрику объявить статической переменной в Application:

    ExpandedWrap disabled
              private static UnityControllerFactory factory = new UnityControllerFactory();
              protected void Application_Start()
              {
                  RegisterRoutes(RouteTable.Routes);
                  ControllerBuilder.Current.SetControllerFactory(factory);
              }



    На десерт я выполню свое обещание и покажу, как можно переложить на плечи Unity заботу об освобождении ресурсов.
    Мы воспользуемся возможностью Unity создавать дочерние контейнеры. Суть идеи проста в начале запроса мы создаем дочерний контейнер, настраиваем его специальным образом, заставляя контейнер создавать единственный экземпляр объекта, а в конце реквеста будем уничтожать дочерний контейнер. Это позволит нам без малейших усилий реализовать логику в которой будет создаваться единственный контекст на HttpRequest, что на мой взгляд является идеальным решением для веб-приложений, как с точки зрения использования ресурсов, так и с точки зрения удобства использования. Для этого добавим в global.asax следующий код:

    ExpandedWrap disabled
              private static IUnityContainer main = new UnityContainer();
              [ThreadStatic]
              private static IUnityContainer perRequestContainer = null;
              protected void Application_BeginRequest()
              {
                  perRequestContainer = main.CreateChildContainer();
                  perRequestContainer.Setup(ConnectionString);
                  factory.Container = perRequestContainer;
              }
       
              protected void Application_EndRequest()
              {
                  perRequestContainer.Dispose();
              }
              protected string ConnectionString
              {
                  get { return ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString; }
              }


    Секрет конфигурации дочернего контейнера кроется в следующей строке кода:

    ExpandedWrap disabled
      perRequestContainer.Setup(ConnectionString);


    Как Вы наверное догадались Setup является не чем иным как утилитным extention методом написанным мной для облегчения программной конфигурации зависимостей в нашем приложении:

    ExpandedWrap disabled
          public static class UnityRegistrator
          {
              public static void Setup(this IUnityContainer container, string connectionString)
              {
                  container.Configure<InjectedMembers>().ConfigureInjectionFor<BlogDataContext>(new InjectionConstructor(connectionString));
       
                  container.RegisterType<DataContext, BlogDataContext>(new ContainerControlledLifetimeManager())
                           .RegisterType<IRepository<Message>, MessagesProvider>(new ContainerControlledLifetimeManager());
       
              }
          }


    ContainerControlledLifetimeManager является имеено тем Lifetime менеджером, который позволит получать единственный экземпляр объекта как в случае инстанцирования, так и в случае впрыскивания зависимости.
    Если говорить об альтернативных реализациях, то можно написать собственный Lifetime менеджер который будет сохранять объекты в HttpRequest или же воспользоваться библиотекой StructureMap, которая способность ассоциировать объекты с HttpRequest реализует путем тривиальной настройки. В этих вариантах не понадобятся дочерние контейнеры.

    Мне кажется мы добились основных целей. Наметили вариант развития приложения. Максимально упростили себе его тестирование и сопровождение. Код является слабосвязанным. Обеспечили возможность использовать прозрачно для клиента альтернативные реализации репоситориев оставляя себе открытой дверь для прямого взаимодействия с БД. Ну и самое главное, реализовали возможность иметь единственный контекст на реквест с автоматическим освобождением неуправляемых ресурсов.

    До новых встреч ;)
      Хорошая статья, держи +1 :)
      Немного мыслей - конкретные реализации IRepository<T> если в терминологии Фаулера, это по сути Шлюзы таблицы данных. Твоя имплементация держать в репозитории предзаготовленные шлюзы довольно симпотичная, но есть как по мне несколько недостатков - синглтоны вечно живут в памяти и жрут ее :) плюс реализовывать шлюзы должен более-менее толковый разработчик с учетом разнопоточного и многопользовательского доступа.
        Цитата PIL @
        Твоя имплементация держать в репозитории предзаготовленные шлюзы довольно симпотичная, но есть как по мне несколько недостатков - синглтоны вечно живут в памяти и жрут ее плюс реализовывать шлюзы должен более-менее толковый разработчик с учетом разнопоточного и многопользовательского доступа.

        Согласен. Но в вебе используя Unity, мы легко устраним такую проблему автоматически освобождая ресурсы. Реестр проживет не более одного реквеста. В обычных многопользовательских приложениях мы можем реализовать хранение реестра по стратегии Per thread, через TLS память. К тому же ниже предлагается другое решение. ;) ориентированное исключительно на ASP.NET MVC. Если бы мне нужен был реестр шлюзов в ASP.NET MVC приложении я бы предпочел наверное иметь реестр где репозитории просто впрыскиваются через свойства зависимостей.

        Цитата PIL @
        Хорошая статья, держи +1

        Спасибо.
          juice, Истину глаголишь :)
          Единственное что хотелось бы добавить, так это то что по-хорошему, контроллеры не должны ввобще ничего "знать" о репозиториях. В нашем проеке (тоже ASP.NET MVC) контроллеры работают со слоем служб, а службы уже непосредственно используют репозитории. Например:

          Репозиторий:
          ExpandedWrap disabled
                public class SqlEmailRepository : SqlRepositoryBase, IEmailRepository
                {
                    public SqlEmailRepository(SqlRepositoryBase rep){
                        db = rep.db;
                    }
             
                    /// <summary>
                    /// Initializes a new instance of the <see cref="SqlEmailRepository"/> class.
                    /// </summary>
                    public SqlEmailRepository(){
                        db = new SomeOurDataContext();
                    }
                    
                    [ExceptionCallHandler(RepositoryPoliciesConfiguration.repositoryCommonPolicyName)]
                    public void SaveMessage(MailMessage message)
                    {
                        if (message.ID <= 0){
                            db.MailMessages.InsertOnSubmit(message);
                        }
                        foreach (MailAttachment attachment in message.MailAttachments){
                            if (attachment.ID <= 0){
                                db.MailAttachments.InsertOnSubmit(attachment);
                            }
                        }
                        SubmitChanges();
                    }
            ... и т.д.
          Разруливание конфликтов параллельного доступа реализовано в базовом репозитории SqlRepositoryBase
          Служба:
          ExpandedWrap disabled
                public class SomeOurEmailService : OurServiceBase, IEmailService
                {
                    private IEmailRepository repository;
             
                    
                    public SomeOurEmailService(IEmailRepository repository){
                        this.repository = repository;
                    }
             
                    public SomeOurEmailService(){
                        this.repository = new SqlEmailRepository();
                    }
             
                    // Этот метод вызывает контроллер
                    [ExceptionCallHandler(ServicePoliciesConfiguration.eMailServiceFoldersExceptionPolicy)]
                    public void SaveMailFolder(MailFolder folder){
                        if(folder == null){
                            throw new ArgumentNullException("EmailService.SaveMailFolder: Mail Folder must be not null");
                        }
                        EasyMailHelper helper = new EasyMailHelper(folder.MailAccount);
                        helper.CreateOrSelectMailbox(folder.FullName);
                        // А вот обращение к репозиторию
                        repository.SaveFolder(folder);
                    }
            и т.д.

          Контроллер:
          ExpandedWrap disabled
                public class EmailController : OurControllerBase
                {
                    IEmailService service = PolicyInjection.Create<EmailService>();
                    ...
                    // Например, посылка сообщения
                    [Authorize]
                    public ActionResult SendMessage(){
                        // Юзаем сервис...
                        service.SendMailMessage(message, account, true);
                    }

          Такой код позволяет совсем абстрагировать контроллер, а соответственно максимально отделить логику от представления, ну конечно же позволяет удобно тестировать логику, которая расположена в сервисах, подменяя репозиторий на мок-объект.

          Хотя, для примера наверное правильно что juice не стал вводить еще один уровень абстракции - дабы не усложнять :)

          Спасибо за статею! Бессонная ночь не прошла зря :)
          +1!
          Сообщение отредактировано: Miha_Dnepr -
            Цитата Miha_Dnepr @
            Такой код позволяет совсем абстрагировать контроллер, а соответственно максимально отделить логику от представления, ну конечно же позволяет удобно тестировать логику, которая расположена в сервисах, подменяя репозиторий на мок-объект.

            Хотя, для примера наверное правильно что juice не стал вводить еще один уровень абстракции - дабы не усложнять

            Миша, ты не совсем прав. В моем примере контроллеры ASP.NET MVC приложения, вообще получают зависимость через конструктор. В твоем примере вы обходитесь без DependencyIjection просто используя PolicyInjection как обычный локатор (InversionOfControl), посути перенося зависимость в него. Локаторы я могу писать и сам. Используя DependencyInjection, ты исбавляешься и от связи с локатором который делает код еще более приспособленным для тестирования. Сила контейнеров именно в умении самостоятельно разрешать зависимости впрыскивая их в объекты, а не в умении искать их по реестру.

            Сравни:

            ExpandedWrap disabled
              public class EmailController : OurControllerBase
                  {
                      IEmailService service = PolicyInjection.Create<GConnectEmailService>();
               
              }


            Налицо завимость от локатора, я считаю, что контейнеры нужно использовать по назначению, раз сказал A надо говорить и B.

            Мой контроллер ничего незнает о PolicyInjection или Unity, Unity сама в фабрике разрешит все зависимости инстанцировав контроллер.

            ExpandedWrap disabled
                  public class MessageController : Controller
                  {
                              IRepository<Message> _repository;
               
                      public MessageController(IRepository<Message> repository)
                      {
                          _repository = repository;
                      }
                      …
                  }


            Теперь, что касается слоя служб. В этом я придерживаюсь классического подхода, они нужны там где нужны и не более того и чем тоньше они тем лучше. В частности удобно их использовать для обозначения контекста доменов. Вводить дополнительный чисто инфраструктурный уровень в ориентированной на работу с данными двухуровневке я не стану. Нельзя сказать, что Ваш подход это плохо или неправильно, кажый архитектор решит сам нужно ли это ему. Просто это не мой выбор.
              :) Да, но в моем случае, тоже есть возможность получить зависимость через конструктор:
              ExpandedWrap disabled
                        public SqlEmailRepository(SqlRepositoryBase rep){
                            db = rep.db;
                        }

              Где "db" и есть дата-контекст, котроый мы получем в конструкторе. Таким образом, репозиторию все равно каким образом создан контекст - либо через юнити (как в твоем случае), либо напрямую.
              Теперь по поводу PolicyInjection - в моем примере, нужно было его просто удалить, на самом деле. PolicyInjection в моем случае используется для работоспособности методов типа:
              ExpandedWrap disabled
                    [ExceptionCallHandler(ServicePoliciesConfiguration.eMailServiceFoldersExceptionPolicy)]
                    public void SomeServiceMethod(){...}

              Это делается для того что бы похендлить исключения. Политика отлова/логгирования задается в конфигурации, а затем при помощи PolicyInjection внедряется. Таким образом, мне вообще можно забыть про try/catch - все типы исключений и политика реагирования на них задаются в конфиге. К репозиториям и данным, PolicyInjection в моем случае вообще не имеет отношения.
              Единственное что пока остается для меня загадкой, так это совместная работа PolicyInjection и юнити контейнера.
                Цитата Miha_Dnepr @
                Таким образом, репозиторию все равно каким образом создан контекст - либо через юнити (как в твоем случае), либо напрямую.

                Именно, это и демонстрирует не только репозитории в моем обзоре, но и сами контроллеры. ;)

                Вообще жду больше от тебя инфы с "фронта", где ASP.NET MVC обкатывается в боевом проекте.
                  Цитата juice @
                  Именно, это и демонстрирует не только репозитории в моем обзоре, но и сами контроллеры.
                  Ну да, я же и не спорил. Посто нужно было выкорчевать полиси нжекшн, что бы вопросов не возникало. Ну, и по-поводу слоя служб: действительно, они нужны там где нужны :) допустим, в ситуации с мэйлом, это не просто работа с данными (я имею ввиду, например кеширование ссобщеня в БД) но и работа с e-mail сервером. В моем случае логика кеширования и реальной посылки сообщения, создания/удаления папок на imap сервере и т.д находится в сервисе, который это все разруливает. Логика там довольно непростая, т.к. исользуются доп. почтовые библиотеки, которые упрощают работу с imap/smtp - и мне совсем не хочется выносить это все в контроллер. Я стараюсь просто как можно четчте обозначить границы. Представь, если придется все это дело потом, например, на сильверлайт переность? Контроллеры придется "прибить", а сервисы останутся со всей логикой, уже обкатанной и оттестированной. Вот собственно истинная причина разграничения и введения дополнительного слоя. Естесственно, там где идет работа только с данными, там доп. служба не всегда нужна - можно обращаться к репозиториям из контроллера, но для общности архитектуры, я все равно выношу работу с репозиториями в службу. Дело в том, что у меня есть четкие границы, благодаря которым, я могу предоставить общую архитектуру для всего проекта. Например, я принял решение логировать исключения на уровне служб. контроллер лиишь получает эксепшн от уровня службы (заметь, лог уже произведен причем, не важно куда - в БД, мэйл или WMI - все задается в конфиге на уровне политик), локализует его, делает юзер-френдли и посылает в виде Json клиенту. Опять же, при необходимости, можно убрать контроллер в данном виде из архитектуры приложения и поменять его на что-угодно, хоть на вин. форму, и т.д.
                  По-поводу инфы с фронта: к началу апреля уже будет что показать, сейчас дорабатываем UI - это полный капец, самая сложная часть получается (ну, помимо движка отчетов, генерилки Pdf и некоторых задач с мэйлом). Зато, я немерянно тащусь от того что получается. Работает быстро и красиво. Опять-же, ни одного серверного контрола, WebParts и иже с ними мы не используем.
                  З.Ы. Поздравляю всех с днем рождения релиза MVC! (он правда был несколько дней назад)
                  http://www.microsoft.com/downloads/details.aspx?FamilyID=53289097-73ce-43bf-b6a6-35e00103cb4b&displaylang=en
                    Miha_Dnepr, ты абсолютно прав мой друг. В таком случае я тоже использую слой служб, но исключительно для делегирования работы домену. Так удобно работать в распределенных приложениях с использованием Application Server или в приложениях ориентрированных на службы. Вобщем мораль простая, нужно использовать именно то, что лучше всего подходит для каждого конкретного проекта. В приложении которое имеет несколько предметных контекстов, можно использовать либо модели напрямую либо использовать слой служб для доступа к доменам. В рассматриваемом выше примере сущности есть не чем иным как моделью ориентированной на сохраняемость, а потому я решил не использовать слой служб.
                      Цитата Miha_Dnepr @
                      Посто нужно было выкорчевать полиси нжекшн, что бы вопросов не возникало.

                      По этому поводу если контейнер разрешает зависимости, то это не мешает использовать PolicyInjectiton
                      Цитата Miha_Dnepr @
                      Это делается для того что бы похендлить исключения. Политика отлова/логгирования задается в конфигурации, а затем при помощи PolicyInjection внедряется. Таким образом, мне вообще можно забыть про try/catch - все типы исключений и политика реагирования на них задаются в конфиге. К репозиториям и данным, PolicyInjection в моем случае вообще не имеет отношения.
                      Единственное что пока остается для меня загадкой, так это совместная работа PolicyInjection и юнити контейнера.


                      Это не загадка :) Unity предаставляет подмножесто AOP - Interception, которое позволяет точно также обрабатывать сквозной код. Поддерживается три типа прокси классов и правила матчинга, как программные так и конфигурационные.

                      Вобщем если переписать твой код в моем стиле и инстанцировать контроллеры в собственной фабрике посредством PolicyInjection, думаю все будет работать на ура.
                      Я думаю, что напишу следующую статью именно об этом, - о работе механизма Interception в Unity.
                        Цитата juice @
                        Я думаю, что напишу следующую статью именно об этом, - о работе механизма Interception в Unity.
                        - было бы очень неплохо :)
                          Хорошая статья+
                          juice, выложи по возможности готовые сорсы к статье, хотелось бы запустить и более подробно изучить
                          0 пользователей читают эту тему (0 гостей и 0 скрытых пользователей)
                          0 пользователей:


                          Рейтинг@Mail.ru
                          [ Script execution time: 0,0952 ]   [ 16 queries used ]   [ Generated: 23.04.24, 12:28 GMT ]