¿Ventaja de crear un repositorio genérico versus un repositorio específico para cada objeto?

132

Estamos desarrollando una aplicación ASP.NET MVC y ahora estamos creando las clases de repositorio / servicio. Me pregunto si existen ventajas importantes para crear una interfaz genérica de IRepository que implementen todos los repositorios, en comparación con cada Repository que tenga su propia interfaz única y un conjunto de métodos.

Por ejemplo: una interfaz genérica de IRepository podría verse así (tomada de esta respuesta ):

public interface IRepository : IDisposable
{
    T[] GetAll<T>();
    T[] GetAll<T>(Expression<Func<T, bool>> filter);
    T GetSingle<T>(Expression<Func<T, bool>> filter);
    T GetSingle<T>(Expression<Func<T, bool>> filter, List<Expression<Func<T, object>>> subSelectors);
    void Delete<T>(T entity);
    void Add<T>(T entity);
    int SaveChanges();
    DbTransaction BeginTransaction();
}

Cada repositorio implementaría esta interfaz, por ejemplo:

  • CustomerRepository: IRepository
  • ProductRepository: IRepository
  • etc.

La alternativa que hemos seguido en proyectos anteriores sería:

public interface IInvoiceRepository : IDisposable
{
    EntityCollection<InvoiceEntity> GetAllInvoices(int accountId);
    EntityCollection<InvoiceEntity> GetAllInvoices(DateTime theDate);
    InvoiceEntity GetSingleInvoice(int id, bool doFetchRelated);
    InvoiceEntity GetSingleInvoice(DateTime invoiceDate, int accountId); //unique
    InvoiceEntity CreateInvoice();
    InvoiceLineEntity CreateInvoiceLine();
    void SaveChanges(InvoiceEntity); //handles inserts or updates
    void DeleteInvoice(InvoiceEntity);
    void DeleteInvoiceLine(InvoiceLineEntity);
}

En el segundo caso, las expresiones (LINQ o de otro tipo) estarían completamente incluidas en la implementación del Repositorio, quien implementa el servicio solo necesita saber a qué función del repositorio llamar.

Supongo que no veo la ventaja de escribir toda la sintaxis de expresión en la clase de servicio y pasarla al repositorio. ¿No significa esto que el código LINQ fácil de enredar se está duplicando en muchos casos?

Por ejemplo, en nuestro antiguo sistema de facturación, llamamos

InvoiceRepository.GetSingleInvoice(DateTime invoiceDate, int accountId)

de algunos servicios diferentes (Cliente, Factura, Cuenta, etc.). Eso parece mucho más limpio que escribir lo siguiente en varios lugares:

rep.GetSingle(x => x.AccountId = someId && x.InvoiceDate = someDate.Date);

La única desventaja que veo al usar el enfoque específico es que podríamos terminar con muchas permutaciones de las funciones Get *, pero esto todavía parece preferible a empujar la lógica de expresión hacia las clases de Servicio.

¿Qué me estoy perdiendo?

Bip bip
fuente
Usar repositorios genéricos con ORM completos parece inútil. He discutido esto en detalles aquí .
Amit Joshi

Respuestas:

169

Este es un problema tan antiguo como el patrón del repositorio en sí. La reciente introducción de LINQ IQueryable, una representación uniforme de una consulta, ha provocado mucha discusión sobre este mismo tema.

Prefiero repositorios específicos, después de haber trabajado muy duro para construir un marco genérico de repositorios. No importa qué mecanismo inteligente probé, siempre terminé con el mismo problema: un repositorio es una parte del dominio que se está modelando, y ese dominio no es genérico. No se pueden eliminar todas las entidades, no se pueden agregar todas las entidades, no todas las entidades tienen un repositorio. Las consultas varían enormemente; la API del repositorio se vuelve tan única como la entidad misma.

Un patrón que uso a menudo es tener interfaces de repositorio específicas, pero una clase base para las implementaciones. Por ejemplo, usando LINQ to SQL, podría hacer:

public abstract class Repository<TEntity>
{
    private DataContext _dataContext;

    protected Repository(DataContext dataContext)
    {
        _dataContext = dataContext;
    }

    protected IQueryable<TEntity> Query
    {
        get { return _dataContext.GetTable<TEntity>(); }
    }

    protected void InsertOnCommit(TEntity entity)
    {
        _dataContext.GetTable<TEntity>().InsertOnCommit(entity);
    }

    protected void DeleteOnCommit(TEntity entity)
    {
        _dataContext.GetTable<TEntity>().DeleteOnCommit(entity);
    }
}

Reemplace DataContextcon su unidad de trabajo de elección. Un ejemplo de implementación podría ser:

public interface IUserRepository
{
    User GetById(int id);

    IQueryable<User> GetLockedOutUsers();

    void Insert(User user);
}

public class UserRepository : Repository<User>, IUserRepository
{
    public UserRepository(DataContext dataContext) : base(dataContext)
    {}

    public User GetById(int id)
    {
        return Query.Where(user => user.Id == id).SingleOrDefault();
    }

    public IQueryable<User> GetLockedOutUsers()
    {
        return Query.Where(user => user.IsLockedOut);
    }

    public void Insert(User user)
    {
        InsertOnCommit(user);
    }
}

Observe que la API pública del repositorio no permite que los usuarios sean eliminados. Además, exponer IQueryablees otra lata de gusanos: hay tantas opiniones como ombligos sobre ese tema.

Bryan Watts
fuente
9
En serio, gran respuesta. ¡Gracias!
Beep beep
55
Entonces, ¿cómo usarías IoC / DI con esto? (Soy un novato en IoC) Mi pregunta con respecto a su patrón completo: stackoverflow.com/questions/4312388/…
dan
36
"un repositorio es una parte del dominio que se está modelando, y ese dominio no es genérico. No todas las entidades se pueden eliminar, no se pueden agregar todas las entidades, no todas las entidades tienen un repositorio" ¡perfecto!
adamwtiko
Sé que esta es una respuesta anterior, pero tengo curiosidad por dejar de lado un método de actualización de la clase Repository con intencionalidad. Tengo problemas para encontrar una manera limpia de hacer esto.
rtf
1
Oldie pero un regalo. Esta publicación es increíblemente sabia y debe leerse y releerse, pero todos los desarrolladores acomodados. Gracias @BryanWatts. Mi implementación generalmente se basa en aptitudes, pero la premisa es la misma. Repositorio base, con repositorios específicos para representar el dominio que opta por las características.
pimbrouwers
27

De hecho, estoy un poco en desacuerdo con la publicación de Bryan. Creo que tiene razón, que en última instancia todo es muy único, etc. Pero al mismo tiempo, la mayor parte de eso surge a medida que diseña, y descubro que obtener un repositorio genérico y usarlo mientras desarrollo mi modelo, puedo obtener una aplicación muy rápidamente, luego refactorizar a una mayor especificidad a medida que encuentro el Necesito hacerlo.

Entonces, en casos como ese, a menudo he creado un IRepository genérico que tiene la pila CRUD completa, y que me permite jugar rápidamente con la API y dejar que la gente juegue con la interfaz de usuario y realice pruebas de integración y aceptación del usuario en paralelo. Luego, cuando descubro que necesito consultas específicas sobre el repositorio, etc., comienzo a reemplazar esa dependencia con la específica si es necesario y a partir de ahí. Una impl subyacente. es fácil de crear y usar (y posiblemente conectar a una base de datos en memoria u objetos estáticos u objetos simulados o lo que sea).

Dicho esto, lo que comencé a hacer últimamente es romper el comportamiento. Entonces, si hace interfaces para IDataFetcher, IDataUpdater, IDataInserter e IDataDeleter (por ejemplo), puede mezclar y combinar para definir sus requisitos a través de la interfaz y luego tener implementaciones que se encarguen de algunos o todos ellos, y yo puedo todavía inyecto la implementación de todo para usar mientras estoy construyendo la aplicación.

Pablo

Pablo
fuente
44
Gracias por la respuesta @Paul. De hecho, también probé ese enfoque. No podía encontrar la manera de expresar de forma genérica el primer método Probé, GetById(). ¿Debo usar IRepository<T, TId>, GetById(object id)o hacer suposiciones y uso GetById(int id)? ¿Cómo funcionarían las teclas compuestas? Me preguntaba si una selección genérica por ID era una abstracción que valía la pena. Si no es así, ¿qué otra cosa se verían obligados a expresar repositorios genéricos? Esa fue la línea de razonamiento detrás de abstraer la implementación , no la interfaz .
Bryan Watts
10
Además, un mecanismo de consulta genérico es responsabilidad de un ORM. Sus repositorios deberían implementar las consultas específicas para las entidades de su proyecto utilizando el mecanismo de consulta genérico. Los consumidores de su repositorio no deberían verse obligados a escribir sus propias consultas a menos que sea parte de su dominio problemático, como en el caso de los informes.
Bryan Watts el
2
@Bryan: con respecto a su GetById (). Yo uso un FindById <T, TId> (ID de TId); Por lo tanto, resulta en algo así como repositorio.FindById <Factura, int> (435);
Joshua Hayes
No suelo poner métodos de consulta de nivel de campo en las interfaces genéricas, francamente. Como señaló, no todos los modelos deben consultarse con una sola clave y, en algunos casos, su aplicación nunca recuperará algo mediante una ID (por ejemplo, si está utilizando claves primarias generadas por DB y solo obtiene una clave natural, por ejemplo, nombre de usuario). Los métodos de consulta evolucionan en las interfaces específicas que construyo como parte de la refactorización.
Paul
13

Prefiero repositorios específicos que se derivan de un repositorio genérico (o una lista de repositorios genéricos para especificar el comportamiento exacto) con firmas de métodos reemplazables.

Arnis Lapsa
fuente
¿Podría proporcionar un pequeño ejemplo de fragmento de código?
Johann Gerell
@Johann Gerell no, porque ya no uso repositorios.
Arnis Lapsa
¿Qué utilizas ahora que te mantienes alejado de los repositorios?
Chris
@ Chris me enfoco mucho en tener un modelo de dominio rico. La parte de entrada de la aplicación es lo importante. Si todos los cambios de estado se monitorean cuidadosamente, no importa mucho cómo se lean los datos, siempre que sean lo suficientemente eficientes. así que solo uso NHibernate ISession directamente. sin la abstracción de la capa del repositorio, es mucho más fácil especificar cosas como carga ansiosa, consultas múltiples, etc. y si realmente lo necesita, no es difícil burlarse de ISession también.
Arnis Lapsa
3
@JesseWebb Naah ... Con el modelo de dominio rico, la lógica de consulta se simplifica significativamente. Por ejemplo, si quiero buscar usuarios que hayan comprado algo, solo busco usuarios. Dónde (u => u.HasPurchasedAnything) en lugar de usuarios. Únete (x => Pedidos, algo, no sé linq). order => order.Status == 1) .Join (x => x.products) .Where (x .... etc etc etc .... bla bla bla bla
Arnis Lapsa
5

Tener un repositorio genérico envuelto por un repositorio específico. De esa manera, puede controlar la interfaz pública, pero aún tiene la ventaja de la reutilización de código que proviene de tener un repositorio genérico.

Peter
fuente
2

public class UserRepository: Repository, IUserRepository

¿No debería inyectar IUserRepository para evitar exponer la interfaz? Como la gente ha dicho, es posible que no necesite la pila CRUD completa, etc.

Ste
fuente
2
Esto parece más un comentario que una respuesta.
Amit Joshi