2012-03-07 11 views
7

Tengo código del controlador como este por todo el sitio de ASP.NET MVC 3:¿Se puede refactorizar este código MVC usando un patrón de diseño?

[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"); 
} 

Básicamente, me refiero al código en el bloque de roscado. Todas las cosas deben suceder, pero el usuario no tiene que esperarlas (es un buen argumento para un hilo de fondo, ¿no?).

Para que quede claro, utilizo el caché (caché de datos ASP.NET regular) en todo el sitio, y la mayoría tiene una política de caché "sin caducidad", así que lo desalojo manualmente cuando sea necesario (como el anterior) .

Y la parte del usuario básicamente le está dando al usuario representante para hacer algo (como Stack).

Recapitulemos: tenemos almacenamiento en memoria caché, manejo de la reputación del usuario, auditoría, todo en uno. Realmente no pertenece en un solo lugar. De ahí el problema con el código actual y el problema con tratar de encontrar la manera de alejarlo.

La razón por la que quiero refactorizar esto es por varias razones:

  1. difícil de probar la unidad. El multihilo y las pruebas unitarias realmente no funcionan bien.
  2. Legibilidad. Es difícil de leer Sucio.
  3. SRP. Controlador haciendo/sabiendo demasiado.

Lo resuelto 1) envolviendo el código de generación de hebras en una interfaz, y solo burlándote/simulándolo.

Pero me gustaría hacer algún tipo de patrón, donde mi código podría tener este aspecto:

[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"); 
} 

Básicamente, poniendo la responsabilidad en "algo más" que reaccionan, no el controlador.

He oído/leído acerca de Tareas, Comandos, Eventos, etc., pero todavía tengo que ver uno implementado en el espacio ASP.NET MVC.

Los primeros pensamientos me dirían crear algún tipo de "gestor de eventos". Pero luego pensé, ¿a dónde va esto? En el dominio? Entonces, ¿cómo maneja las interacciones con el caché, que es una preocupación de infraestructura? Y luego enhebrar, que también es una preocupación de infraestructura. ¿Y qué ocurre si quiero hacerlo sincrónicamente, en lugar de asincrónico? ¿Qué hace esa decisión?

No quiero tener que apilar toda esta lógica en otro lado. Lo ideal es que se vuelva a factorizar en componentes manejables y significativos, sin responsabilidad cambiada, si eso tiene sentido.

¿Algún consejo?

+1

+1 esta es una buena pregunta a un problema común. Espero ver soluciones n + 1. dónde ir después ... hmmm :) –

Respuesta

2

Los primeros pensamientos me dirían crear algún tipo de "gestor de eventos". Pero luego pensé, ¿a dónde va esto? En el dominio?

Es la forma en que resuelvo el problema. Veo el administrador de eventos como infraestructura. Pero los eventos reales pertenecen al dominio.

Bueno, entonces, ¿cómo maneja las interacciones con la memoria caché, que es una preocupación de infraestructura. Y luego enhebrar, que también es una preocupación de infraestructura. ¿Y qué ocurre si quiero hacerlo sincrónicamente, en lugar de asincrónico? ¿Qué hace esa decisión?

Async es agradable, pero hace que el manejo de transacciones sea complejo. Si usa un contenedor de IoC, ya tiene un ámbito bien definido y una transacción que puede usarse durante la propagación del evento.

imho depende del suscriptor programar/enhebrar su tarea si sabe que su manejo de eventos llevará tiempo.

solución propuesta:

Use su contenedor IoC para publicar los eventos. Dejaría que el repositorio publique los eventos (PostUpdated o EntityUpdated según lo que desee hacer con el evento) en lugar del controlador (para reducir la duplicación del código).

que he hecho una implementación COI para autofac cual le permite:

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

suscripción:

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

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

uso típico

  1. Implementar IServiceResolver (para su contenedor)
  2. asignarlo: ServiceResolver.Assign(new yourResolver(yourContainer))
  3. uso como se describe here.
+0

interesante. pero ¿cómo encaja el hilo en la mezcla? ¿Necesitaría el caché disparar el hilo? – RPM1984

+0

@ RPM1984: lee mi actualización. – jgauffin

+0

@ RPM1984: ¿Mi actualización lo dejó más claro? – jgauffin

1

Hay una implementación de bus de mensajes en MvcContrib, como parte de la función PortableArea. Su objetivo principal es permitir que las funciones implementadas de forma independiente activen y escuchen eventos, lo que suena bastante parecido a lo que deseas.

No estoy seguro si es la mejor opción, ya que el estado de MvcContrib es poco documentado e incompleto. Algunas partes se mantienen activamente mientras que otras están obsoletas.

Otra opción a considerar es ZeroMQ, pero puede ser exagerado para sus necesidades.

2

quizás el objeto poste debería actualizar en sí, y se aprobó una IRepository (que en sí se hizo pasar al controlador.) (Esto es básico de inyección de dependencia/COI, y mantiene el skinnier controlador)

//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 
} 
+1

a) Entonces el Post (dominio) tiene una dependencia en el repositorio, que aunque podría decirse que el repositorio es parte del dominio, me gusta mantener en ridículo a mi POCO. b) realmente no resuelve el problema de caché. – RPM1984

+0

@ RPM1984, es DDD donde la lógica de dominio está dentro del modelo. –

+0

@DarinDimitrov - de acuerdo, bueno, no estoy haciendo DDD entonces. :) Pero quiero mis POCOs con solo propiedades y lógica de negocios (interna), pero que no requieren dependencias externas. – RPM1984

1

Puede usar aspect oriented programming en este problema particular. El producto comúnmente usado en el mundo .NET es PostSharp.

La idea sería que agregue un atributo sobre el método. El atributo le dirá qué acciones particulares se deben realizar (en su caso refresco de caché, puntos para aumentar, etc.) y cuándo debería suceder (en su caso, al salir del método, por ejemplo).

También podría separar estos atributos diferentes, por lo que podría hacer diferentes tipos de combinaciones.

1

Es posible que desee considerar la implementación de un sistema de bus de servicios tales como NServiceBus si se trata de almacenamiento en caché o auditoría (que puede ser mejor servido de forma asíncrona).

Con un bus de mensajes, los mensajes de evento pueden publicarse en cualquier cantidad de controladores de eventos (por ejemplo, un manejador de caché) de forma asincrónica, para que su aplicación pueda lanzar un mensaje y continuar sirviendo páginas síncronas rápidamente.

0
  1. Creo que necesita considerar transacción problema. Si intenta actualizar cachés en otro hilo y devuelve el resultado de la acción inmediatamente después de repository.save. Es difícil retrotraer si las operaciones de caché arrojan un error (la mayoría de las cachés son globales, que generalmente se enfrentan con problemas de bloqueo/sincronización).
  2. MVC es un patrón de capa UI. Normalmente pongo un poco de lógica basada en el contexto http en el cuerpo de acción y coloco la lógica de negocios en capas más bajas, por ejemplo, un servicio que maneja el modelo de dominio. En su caso, me gustaría agregar un IPostService y poner su función de guardar y la función en caché en él. Que es algo así como:

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

    Y debido a que la memoria caché no está relacionado con la lógica de negocio, que does't necesario que aparezca en la interfaz de servicio. Es un detalle de implementación.

  3. Por otra parte, creo que es bueno usar AOP para desencadenar evento y usar un contenedor de IoC para inyectar la lógica de caché (junto con la lógica de transacción). Algo así como:

    //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); 
    } 
    
+0

solía tener servicios, pero los arranqué porque me di cuenta de que simplemente estaban envolviendo mi repositorio y no añadiendo ningún valor. no creo que el hecho de mover el caché exija traerlo de vuelta. Sin embargo, gracias por la sugerencia. todos los consejos útiles. :) – RPM1984

0

Aceptado @ respuesta de jgauffin, pero que me gustaría añadir una respuesta separada desde que uso StructureMap, y es necesario un paso adicional .

Seguí lo que @jgauffin especificó, pero no estaba disparando mi controlador de eventos.

por lo que añade esto a mi config StructureMap:

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

entonces funciona.

Supongo que la alternativa a esto es especificar manualmente cada escucha registrada, lo que se vuelve un poco loco.

¿Alguien que use StructureMap me puede decir si este es el enfoque correcto?

Cuestiones relacionadas