Может ли этот код MVC быть реорганизован с использованием шаблона проектирования? - PullRequest
8 голосов
/ 07 марта 2012

На моем сайте ASP.NET MVC 3 есть такой код контроллера:

[HttpPost]
public ActionResult Save(PostViewModel viewModel)
{
   // VM -> Domain Mapping. Definetely belongs here. Happy with this.
   var post = Mapper.Map<PostViewModel, Post>(viewModel);

   // Saving. Again, fine. Controllers job to update model.
   _postRepository.Save(post);

   // No. Noooo..caching, thread spawning, something about a user?? Why....
   Task.Factory.StartNew(() => {
       _cache.RefreshSomeCache(post);
       _cache2.RefreshSomeOtherCache(post2);
       _userRepository.GiveUserPoints(post.User);
       _someotherRepo.AuditThisHappened();
   });

   // This should be the 3rd line in this method.
   return RedirectToAction("Index");
}

По сути, я имею в виду код в блоке потоков. Все вещи должны происходить, но пользователю не нужно ждать их (хороший пример для фонового потока, верно?).

Просто чтобы прояснить ситуацию, я использую кеширование (обычный кеш данных ASP.NET) по всему сайту, и у большинства из них есть политика кеширования "не истекает", поэтому я вручную выталкиваю ее, когда требуется (как описано выше).

И пользовательская часть, в основном, дает пользователю репутацию за что-то (например, стек).

Итак, подведем итоги: у нас есть кэширование, обработка репутации пользователя, аудит - все в одном. На самом деле не принадлежит ни к одному месту, делает это. Отсюда проблема с текущим кодом и проблема с попыткой выяснить, как его убрать.

Причина, по которой я хочу провести рефакторинг, по нескольким причинам:

  1. Сложно провести юнит-тест. Многопоточность и модульное тестирование не очень-то хороши.
  2. читаемость. Трудно читать. Грязное.
  3. SRP. Контролер делает / знает слишком много.

Я решил 1), обернув код, порождающий потоки, в интерфейс, и просто высмеял это.

Но я бы хотел сделать какой-нибудь шаблон, где мой код мог бы выглядеть так:

[HttpPost]
public ActionResult Save(PostViewModel viewModel)
{
   // Map.
   var post = Mapper.Map<PostViewModel, Post>(viewModel);

   // Save.
   _postRepository.Save(post);

   // Tell someone about this.
   _eventManager.RaiseEvent(post);

   // Redirect.
   return RedirectToAction("Index");
}

По сути, возлагать бремя на "что-то другое", чтобы реагировать, а не на контроллер.

Я слышал / читал о задачах, командах, событиях и т. Д., Но еще не видел ни одного, реализованного в пространстве ASP.NET MVC.

Первые мысли сказали бы мне создать своего рода "менеджера событий". Но потом я подумал, куда это денется? В домене? Тогда как он обрабатывает взаимодействия с кешем, что является проблемой инфраструктуры. А затем многопоточность, которая также является проблемой инфраструктуры. А что если я хочу сделать синхронно, а не асинхронно? Что делает это решение?

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

Любой совет?

Ответы [ 7 ]

2 голосов
/ 07 марта 2012

Вы можете использовать аспектно-ориентированное программирование для решения этой конкретной проблемы.Обычно используемым продуктом в мире .NET является PostSharp .

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

Вы также можете разделить их на различные атрибуты, чтобы вы могли создавать различные виды комбинаций.

2 голосов
/ 07 марта 2012

возможно, объект post должен обновить себя и передать IRepository (который сам был передан в контроллер.) (Это базовая инъекция зависимостей / IOC, и он делает контроллер более тонким)

//in controller:
var post = Mapper.Map<PostViewModel, Post>(viewModel);
post.Update(_postRepository);

//inside Post.cs:
public Update(IRepository rep){
//update db with the repo    
//give points
}
2 голосов
/ 07 марта 2012

Первые мысли подсказали бы мне создать своего рода "менеджер событий".Но потом я подумал, куда это денется?В домене?

Так я решаю проблему.Я вижу менеджера событий как инфраструктуру.Но фактические события принадлежат домену.

Ну тогда как он обрабатывает взаимодействия с кешем, что является проблемой инфраструктуры.А затем многопоточность, которая также является проблемой инфраструктуры.А что если я хочу сделать синхронно, а не асинхронно?Что делает это решение?

Async хорош, но усложняет обработку транзакций.Если вы используете контейнер IoC, у вас уже есть четко определенная область действия и транзакция, которую можно использовать во время распространения события.

imho, подписчик может планировать / обрабатывать свою задачу, если он знает, что это обработка событияпотребуется время.

Предлагаемое решение:

Используйте свой контейнер IoC для публикации событий.Я бы позволил хранилищу публиковать события (либо PostUpdated, либо EntityUpdated в зависимости от того, что вы хотите сделать с событием), а не контроллер (чтобы уменьшить дублирование кода).

Я сделал реализацию IoC для автофака, которая позволяет:

DomainEventDispatcher.Current.Dispatch(new EntityUpdated(post));

Подписка:

public class CacheService : IAutoSubscriberOf<EntityUpdated>
{
    public void Handle(EntityUpdated domainEvent) {};
}

https://github.com/sogeti-se/Sogeti.Pattern/wiki/Domain-events

Типичное использование

  1. Реализация IServiceResolver (для вашего контейнера)
  2. Назначьте его: ServiceResolver.Assign(new yourResolver(yourContainer))
  3. Используйте, как описано здесь .
1 голос
/ 07 марта 2012

Возможно, вы захотите внедрить систему служебной шины, такую ​​как NServiceBus , если вы имеете дело с очисткой или аудитом кэша (который может лучше обслуживаться асинхронно).

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

1 голос
/ 07 марта 2012

В MvcContrib есть реализация шины сообщений как часть функции PortableArea.Его основная цель - позволить независимо реализованным функциям запускать и прослушивать события, что звучит почти так, как вы хотите.

Я не уверен, что это лучший вариант, так как состояние MvcContribнесколько недокументированный и отрывочный.Некоторые детали активно обслуживаются, в то время как другие устарели.

Другой вариант, который стоит рассмотреть, это ZeroMQ , но он может быть излишним для ваших нужд.

0 голосов
/ 09 марта 2012

Принял ответ @ jgauffin, но подумал, что добавлю отдельный ответ, поскольку я использую StructureMap, и ему нужно один дополнительный шаг .

Я следовал указаниям @jgauffin, но мой обработчик событий не срабатывал.

Итак, я добавил это в мою конфигурацию StructureMap:

x.Scan(y => 
{
   y.AssembliesFromApplicationBaseDirectory();
   y.AddAllTypesOf(typeof (IAutoSubscriberOf<>));
   y.WithDefaultConventions();
});

Тогда это работает.

Полагаю, альтернативой этому является ручное указание каждого зарегистрированного слушателя, что немного сходит с ума.

Может ли кто-нибудь, кто использует StructureMap, сказать мне, если это правильный подход?

0 голосов
/ 07 марта 2012
  1. Я думаю, вам нужно рассмотреть вопрос транзакции . Если вы попытаетесь обновить кэши в другом потоке и вернуть результат действия сразу после repository.save. Сложно откатиться, если операции с кешем выдают ошибку (большинство кешей являются глобальными, которые обычно сталкиваются с проблемами блокировки / синхронизации).
  2. MVC - шаблон уровня пользовательского интерфейса. Я обычно помещаю некоторую логику на основе контекста http в тело действия и помещаю бизнес-логику в более низкие уровни, например, в сервис, который обрабатывает модель предметной области. В вашем случае я хотел бы добавить IPostService и поместить в него вашу функцию сохранения и кэшированную функцию. Что-то вроде:

    [HttpPost]
    public ActionResult Save(PostViewModel viewModel)
    {
       // Map.
       var post = Mapper.Map<PostViewModel, Post>(viewModel);
    
       // Save.
       _postService.Save(post);
    
       // Redirect.
       return RedirectToAction("Index");
    }
    

    И поскольку кеш не связан с бизнес-логикой, он не должен появляться в интерфейсе сервиса. Это деталь реализации.

  3. Более того, я думаю, что хорошо использовать AOP для запуска события и использовать контейнер IoC для внедрения логики кэша (вместе с логикой транзакции). Что-то вроде:

    //codes in your PostService which implements IPostService
    [CacheEvent("POST")]
    [Transaction]
    public void Save(Post post) //care about domain model instead of view model
    {
       // Save.
       _postReposity.Save(post);
    }
    
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...