2012-08-23 18 views
12

Actualmente hemos implementado un patrón de repositorio en el trabajo. Todos nuestros repositorios se sientan detrás de sus propias interfaces y se asignan a través de Ninject. Nuestro proyecto es bastante grande y hay un par de peculiaridades con este patrón que estoy tratando de resolver.Intentando simplificar nuestro patrón de repositorio

Primero, hay algunos controladores donde necesitamos más de 10 a 15 repositorios, todos en el mismo controlador. El constructor se pone bastante feo cuando pregunta por tantos repositorios. La segunda peculiaridad se revela después de llamar a métodos en múltiples repositorios. Después de trabajar con varios repositorios, necesitamos llamar al método SaveChanges, pero ¿a qué repositorio debemos llamarlo? Cada repositorio tiene uno. Todos los repositorios tienen la misma instancia del contexto de datos de Entity Framework inyectado, por lo que seleccionar cualquier repositorio aleatorio para llamar a guardar funcionará. Parece tan complicado.

Busqué el patrón de "Unidad de trabajo" y encontré una solución que creo que resuelve ambos problemas, pero no estoy 100% seguro en esta solución, por lo que cualquier comentario es apreciado. Creé una clase llamada DataBucket (sobre todo porque no me gusta llamarla UnitOfWork. Suena gracioso).

// Slimmed down for readability 
public class DataBucket 
{ 
    private DataContext _dataContext; 

    public IReportsRepository ReportRepository { get; set; } 
    public IEmployeeRepository EmployeeRepository { get; set; } 
    public IDashboardRepository DashboardRepository { get; set; } 

    public DataBucket(DataContext dataContext, 
     IReportsRepository reportsRepository, 
     IEmployeeRepository employeeRepository, 
     IDashboardRepository dashboardRepository) 
    { 
     _dataContext = dataContext; 
     this.ReportRepository = reportsRepository; 
     this.EmployeeRepository = employeeRepository; 
     this.DashboardRepository = dashboardRepository; 
    } 

    public void SaveChanges() 
    { 
     _dataContext.SaveChanges(); 
    } 
} 

Esto parece resolver ambos problemas. Ahora solo hay un método SaveChanges en el contenedor de datos y solo se inyecta un objeto, el contenedor de datos. A continuación, accede a todos los repositorios como propiedades. El cubo de datos sería un poco desordenado ya que estaría aceptando TODO (fácilmente 50 o más) de nuestros repositorios en su constructor.

El proceso de agregar un nuevo repositorio ahora incluiría: crear la interfaz, crear el repositorio, mapear la interfaz y el repositorio en Ninject, y agregar una propiedad al contenedor de datos y rellenarlo.

Pensé en una alternativa a esto que eliminaría un paso de arriba.

public class DataBucket 
{ 
    private DataContext _dataContext; 

    public IReportsRepository ReportRepository { get; set; } 
    public IEmployeeRepository EmployeeRepository { get; set; } 
    public IDashboardRepository DashboardRepository { get; set; } 

    public DataBucket(DataContext dataContext) 
    { 
     _dataContext = dataContext; 
     this.ReportRepository = new ReportsRepository(dataContext); 
     this.EmployeeRepository = new EmployeeRepository(dataContext); 
     this.DashboardRepository = new DashboardRepository(dataContext); 
    } 

    public void SaveChanges() 
    { 
     _dataContext.SaveChanges(); 
    } 
} 

Ésta elimina prácticamente todas las asignaciones del repositorio en Ninject porque todos están instanciados en el cubo de datos. Así que ahora los pasos para agregar un nuevo repositorio incluyen: Crear interfaz, crear repositorio, agregar propiedad al depósito de datos e instanciar.

¿Pueden ver algunos defectos con este modelo? En la superficie, parece mucho más conveniente consumir nuestros repositorios de esta manera. ¿Es este un problema que se ha abordado anteriormente? De ser así, ¿cuál es el enfoque más común y/o más eficiente para este problema?

Gracias :)

+0

¿No necesita una interfaz IDataBucket ya que esta clase usa las clases de repositorio, por lo que esta clase también se inyecta? Aparte de eso, parece una buena solución para mí. Creo que sería mejor hacer que las propiedades del repositorio tengan un setter privado. – Maarten

+1

Puede inyectar clases concretas con Ninject. Si no proporciona ninguna asignación, Ninject buscará una clase con el nombre solicitado e instanciará si es posible. – Chev

+0

Por motivos de unidad aunque @Maarten probablemente tengas razón. Tendré que interconectarlo para poder simularlo para probarlo. – Chev

Respuesta

2

Creo que tiene toda la razón para utilizar el patrón Unidad de trabajo en este caso. Esto no solo le impide necesitar un método SaveChanges en cada repositorio, sino que le proporciona una buena manera de manejar las transacciones desde dentro del código en lugar de en su base de datos. Incluí un método Rollback con mi UOW para que, si hubiera una excepción, pudiera deshacer cualquiera de los cambios que la operación ya había realizado en mi DataContext.

Una cosa que podría hacer para evitar problemas de dependencia extraños sería agrupar repositorios relacionados en su propia Unidad de trabajo, en lugar de tener un gran DataBucket que contenga todos los repositorios que tenga (si esa fuera su intención). Cada UOW solo necesitaría ser accesible al mismo nivel que los repositorios que contenía, y otros repositorios probablemente no dependan de otros UOWs (sus repositorios no deberían necesitar usar otros repositorios).

Si quieres ser un purista aún mayor del patrón, también puedes estructurar tus UOW para representar solo eso, una sola Unidad de Trabajo. Los define para representar una operación específica en su dominio y proporcionar los repositorios necesarios para completar esa operación. Los repositorios individuales pueden existir en más de un UOW, si tiene sentido que lo use más de una operación en su dominio.

Por ejemplo, un PlaceCustomerOrderUnitOfWork pueden necesitar un CustomerRepository, OrderRepository, BillingRepository, y una ShippingRepository

Un CreateCustomerUnitOfWork pueden necesitar sólo un CustomerRepository. De cualquier manera, puede pasar fácilmente esa dependencia a sus consumidores, las interfaces más finas para su UOW pueden ayudar a dirigir sus pruebas y reducir el esfuerzo para crear un simulacro.

+0

Interesantes ideas. Si voy con mi idea original de un contenedor de datos que contiene todo, ¿ve algún problema al tener instalados todos los repositorios adjuntos? Otra solución sería actualizar el repositorio la primera vez que se accede a la propiedad en el UOW. [Algo como esto] (https://gist.github.com/3440615). De esta forma solo se crearían instancias de los repos que se necesitan durante esa transacción. ¿Qué piensas? – Chev

+0

@AlexFord así es exactamente como implementé el mío, por "carga lenta" de los repositorios 'if (repo == null) then // instanceiate'. Además de crear una interfaz monolítica, mantener todo en un solo DataBucket debería estar bien, siempre que tengas la intención de que todas las implementaciones de tu Repositorio vayan en el mismo ensamblaje que tu implementación de Unidad de trabajo. No es que usted NO PODRÍA tenerlos en otros diferentes, sin embargo, esto le abre la posibilidad de tener dependencias cíclicas extrañas. – mclark1129

+0

Sí, todo lo que estamos discutiendo aquí se hace en nuestro ensamblado de "Dominio", por lo que no deberíamos preocuparnos. Gracias por tu contribución. Has sido de gran ayuda. – Chev

1

La noción de cada repositorio teniendo un SaveChanges es defectuoso porque llamándolo guarda todo. No es posible modificar parte de un DataContext, siempre guarda todo. Entonces, una clase de titular central DataContext es una buena idea.

Alternativamente, podría tener un repositorio con métodos genéricos que puedan operar en cualquier tipo de entidad (GetTable<T>, Query<T>, ...). Eso eliminaría todas las clases y las fusionaría en una sola (básicamente, solo queda DataBucket).

Incluso podría ser que no necesite repositorios: ¡puede inyectar el DataContext en sí mismo! El DataContext en sí mismo es un repositorio y una capa de acceso a datos completa. Sin embargo, no se presta a la burla.

Si puede hacerlo, esto depende de lo que necesite que el "repositorio" proporcione.


El único problema con tener que DataBucket clase sería que esta clase necesita saber acerca de todas las entidades y todos los repositorios. Por lo tanto, ocupa un lugar muy alto en la pila de software (en la parte superior). Al mismo tiempo, prácticamente todo lo usa, por lo que se encuentra en la parte inferior, también. ¡Espere! Ese es un ciclo de dependencia en toda la base de código.

Esto significa que todo lo que lo usa y todo lo que utiliza debe estar en el mismo conjunto.

+0

Correcto, los cambios en el nivel de repositorio no tienen sentido. Es por eso que estoy abordando esto :). Intenté con un repositorio genérico y terminé encontrando que demasiados repositorios necesitaban métodos y funcionalidades especiales para encajar en una plantilla genérica. Tampoco me gusta construir árboles de expresión LINQ por encima del nivel de repositorio porque de alguna manera se derrota el propósito de la abstracción; Básicamente hace que las capas superiores dependan de una capa de datos que siempre comprenderá los árboles de expresión frente a IQueryable. – Chev

+0

Por otro lado, no permitir expresiones arbitrarias fuera del repositorio le obliga a agregar métodos de consulta especializados (y clases de tipos de devolución) para todos los casos de uso no triviales (entidad-obtener). Hablando de experiencia con una gran base de código con más de 300 tablas aquí: El repositorio patern no vale la pena. Nunca cambiamos los de ORM. Nunca cambiamos nuestro RDBMS. Nunca decidimos cambiar a NoSQL. Simplemente nunca sucede. Una abstracción de repositorio no agregaría nada útil, sino que agregaría esfuerzos y complejidad. – usr

+0

En su situación, probablemente estaría de acuerdo. Desafortunadamente, tengo la tarea de mantener la separación de las preocupaciones de tal manera que podríamos cambiar a NoSQL si quisiéramos. – Chev

0

Lo que he hecho en el pasado era crear contenedores de inyección para niños (yo estaba usando Unity) y registrar un contexto de datos con ContainerControlledLifetime. De modo que cuando los repositorios se crean instancias, siempre tienen el mismo contexto de datos inyectado en ellos. Luego me aferro a ese contexto de datos y cuando mi "Unidad de trabajo" está completa, llamo al DataContext.SaveChanges() vaciando todos los cambios a la base de datos.

Esto tiene algunas otras ventajas, como (con EF) algo de caché local, de modo que si más de un repositorio necesita obtener la misma entidad, solo el primer repositorio causa una ida y vuelta a la base de datos.

También es una buena forma de "completar" los cambios y asegurarse de que se ejecutan como una única transacción atómica.

5

Primero, hay algunos controladores donde necesitamos más de 10 a 15 repositorios, todos en el mismo controlador.

Di la bienvenida a Abstract factory pattern. En lugar de registrar todos los repositorios en Ninject e inyectarlos a los controladores, registre una sola implementación de la fábrica que podrá proporcionar cualquier repositorio que necesite, incluso puede crearlos de forma perezosa solo si el controlador realmente los necesita. Que inyectar la fábrica al controlador.

Sí, también tiene algunas desventajas: le está dando permiso al controlador para obtener cualquier repositorio. ¿Eso es un problema para ti? Siempre puede crear múltiples fábricas para algunos subsistemas si lo necesita o simplemente exponer múltiples interfaces de fábrica en una sola implementación. Todavía no cubre todos los casos, pero es mejor que pasar 15 parámetros al constructor. Por cierto. ¿Estás seguro de que esos controladores no deberían estar divididos?

Nota: Esto no es un antipatrón del proveedor de servicios.

Después de trabajar con varios repositorios, necesitamos llamar al método SaveChanges, pero ¿a qué repositorio debemos llamarlo?

Di la bienvenida al patrón Unit of Work. La unidad de trabajo es una transacción lógica en su aplicación. Persiste todos los cambios de la transacción lógica juntos. El repositorio no debería ser responsable de los cambios persistentes, la unidad de trabajo debería ser. Alguien mencionó que DbContext es la implementación del patrón Repositorio. It is not. Es la implementación del patrón de Unidad de trabajo y DbSet es la implementación del patrón Repositorio.

Lo que necesita es una clase central que contenga la instancia del contexto. El contexto también se pasará a los repositorios porque lo necesitan para recuperar datos, pero solo la clase central (unidad de trabajo) ofrecerá cambios de guardado. También puede manejar la transacción de base de datos si, por ejemplo, necesita cambiar el nivel de aislamiento.

¿Dónde debe manejarse la unidad de trabajo? That depends donde se orquesta su operación lógica. Si la operación se orquesta directamente en las acciones del controlador, también debe tener una unidad de trabajo en la acción y llamar al SaveChanges una vez que se hayan realizado todas las modificaciones.

Si no le preocupa demasiado la separación de preocupaciones, puede incluso combine unit of work and factory en una sola clase. Eso nos lleva a su DataBucket.

+0

Tu idea suena similar a mi clase de depósito de datos revisados, donde solo instalo repositorios cuando se necesitan. https://gist.github.com/3440615 ¿Qué piensas de eso? ¿O tienes una idea diferente de cómo esta fábrica debería "crear" repositorios? ¿Esta fábrica tiene un método 'CreateReportsRepository'? Si es así, tendría tantos métodos como mi cubo tiene propiedades. ¿O estás pensando en abstraerlo en un método que instanciaría algo así como un 'IRepository' y luego lo lanzaría donde se necesita? – Chev

+0

Sí, necesitaría tener tantos métodos como repositorios. Puede crecer a una cantidad fea, ese es un momento para pensar en fábricas separadas. A menos que tenga un repositorio genérico que implementa la misma interfaz genérica (que considero mala), no lo hará mejor. Con la misma interfaz genérica puede intentar ocultar la complejidad detrás de un único método, pero aún necesita una lógica para crear instancias de un depósito correcto. –

+1

Tomemos este paso más por el agujero del conejo. Digamos que ahora creo dos fábricas que contienen repositorios relacionados. En alguna parte de mi aplicación necesito usar ambas fábricas para hacer cambios. ¿A qué fábrica llamo guardar cambios? Parece que he creado el mismo problema que los métodos de guardado a nivel de repositorio. – Chev

Cuestiones relacionadas