La ventaja de la creación de un repositorio genérico vs repositorio específico para cada objeto?


Estamos desarrollando un ASP.NET MVC, y ahora están construyendo las clases repository / service. Me pregunto si hay alguna ventaja importante en la creación de una interfaz IRepository genérica que todos los repositorios implementan, frente a que cada Repositorio tiene su propia interfaz única y un conjunto de métodos.

Por ejemplo: una interfaz IRepository genérica podría verse como (tomado 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, para ejemplo:

  • CustomerRepository: IRepository{[14]]}
  • ProductRepository: IRepository{[14]]}
  • 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 no) estarían completamente contenidas en la implementación del Repositorio, quien esté implementando el servicio solo necesita saber a qué función de repositorio llamar.

Supongo que no veo la ventaja de escribir todos los sintaxis de expresión en la clase service y pasando al repositorio. ¿No significaría esto que el código LINQ fácil de confundir se está duplicando en muchos casos?

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

InvoiceRepository.GetSingleInvoice(DateTime invoiceDate, int accountId)

De unos pocos 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 Get * functions, pero esto todavía parece preferible a empujar la lógica de expresión hacia arriba en las clases de Servicio.

¿Qué me estoy perdiendo?

Author: Community, 2009-08-05

5 answers

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

Yo prefiero repositorios específicos, después de haber trabajado muy duro para construir un framework de repositorio genérico. 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 todas las entidades puede ser eliminado, no todas las entidades pueden ser agregadas, no todas las entidades tienen un repositorio. Las consultas varían enormemente; la API del repositorio se vuelve tan única como la propia entidad.

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 a SQL, usted 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 DataContext con 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 permitir que los usuarios sean eliminados. Además, exponer IQueryable es otra lata de gusanos - hay tantas opiniones como ombligos sobre ese tema.

 158
Author: Bryan Watts,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-08-23 15:00:51

En realidad estoy ligeramente en desacuerdo con el post de Bryan. Creo que tiene razón, que en última instancia todo es muy único y así sucesivamente. Pero al mismo tiempo, la mayor parte de eso sale a medida que diseñas, y encuentro que al 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 la necesidad de hacerlo.

Así que, 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 haga pruebas de integración y aceptación de usuarios en paralelo. Luego, a medida que encuentro que necesito consultas específicas sobre el repositorio, etc., empiezo a reemplazar esa dependencia con la específica si es necesario y a partir de ahí. Un impl subyacente. es fácil de crear y usar (y posiblemente enganchar a una base de datos en memoria u objetos estáticos u objetos burlados o lo que sea).

Dicho esto, lo que he empezado a hacer últimamente es romper el comportamiento. Por lo tanto, si haces interfaces para IDataFetcher, IDataUpdater, IDataInserter e IDataDeleter (por ejemplo), puedes mezclar y combinar para definir tus requisitos a través de la interfaz y luego tener implementaciones que se ocupen de algunos o todos ellos, y todavía puedo inyectar la implementación does-it-all para usar mientras estoy construyendo la aplicación.

Paul

 26
Author: Paul,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-03-20 13:56:07

Prefiero repositorios específicos que se deriven de repositorios genéricos (o lista de repositorios genéricos para especificar el comportamiento exacto) con firmas de métodos anulables.

 11
Author: Arnis Lapsa,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-08-05 08:06:10

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

 5
Author: Peter,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-14 11:45:25

Public class UserRepository: Repositorio, IUserRepository

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

 1
Author: Ste,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-15 09:52:45