Keuntungan membuat repositori generik vs. repositori spesifik untuk setiap objek?

132

Kami sedang mengembangkan aplikasi ASP.NET MVC, dan sekarang sedang membangun repositori / kelas layanan. Saya bertanya-tanya apakah ada keuntungan utama untuk membuat antarmuka IRepository generik yang diterapkan semua repositori, dibandingkan setiap Repositori yang memiliki antarmuka unik dan serangkaian metode sendiri.

Sebagai contoh: antarmuka IRepository generik mungkin terlihat seperti (diambil dari jawaban ini ):

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();
}

Setiap Repositori akan mengimplementasikan antarmuka ini, misalnya:

  • Gudang Pelanggan: Gudang IR
  • ProductRepository: IRepository
  • dll.

Alternatif yang telah kami ikuti dalam proyek sebelumnya adalah:

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

Dalam kasus kedua, ekspresi (LINQ atau lainnya) akan sepenuhnya terkandung dalam implementasi Repositori, siapa pun yang mengimplementasikan layanan hanya perlu tahu fungsi repositori mana yang harus dipanggil.

Saya kira saya tidak melihat keuntungan dari menulis semua sintaks ekspresi di kelas layanan dan meneruskan ke repositori. Bukankah ini berarti kode LINQ yang mudah diacak digandakan dalam banyak kasus?

Misalnya, dalam sistem faktur lama kami, kami menelepon

InvoiceRepository.GetSingleInvoice(DateTime invoiceDate, int accountId)

dari beberapa layanan yang berbeda (Pelanggan, Faktur, Akun, dll). Tampaknya jauh lebih bersih daripada menulis yang berikut di banyak tempat:

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

Satu-satunya kelemahan saya melihat untuk menggunakan pendekatan spesifik adalah bahwa kita bisa berakhir dengan banyak permutasi fungsi Get *, tetapi ini tampaknya masih lebih baik untuk mendorong logika ekspresi ke dalam kelas-kelas Layanan.

Apa yang saya lewatkan?

Bip bip
sumber
Menggunakan repositori generik dengan ORM penuh tampak tidak berguna. Saya telah membahas ini secara rinci di sini .
Amit Joshi

Jawaban:

169

Ini adalah masalah setua pola Repositori itu sendiri. Pengenalan terbaru LINQIQueryable , sebuah representasi seragam dari suatu query, telah menyebabkan banyak diskusi tentang topik ini.

Saya lebih suka repositori spesifik sendiri, setelah bekerja sangat keras untuk membangun kerangka repositori generik. Tidak peduli apa pun mekanisme pintar yang saya coba, saya selalu berakhir pada masalah yang sama: repositori adalah bagian dari domain yang dimodelkan, dan domain itu tidak generik. Tidak setiap entitas dapat dihapus, tidak setiap entitas dapat ditambahkan, tidak setiap entitas memiliki repositori. Pertanyaan sangat bervariasi; API repositori menjadi seunik entitas itu sendiri.

Pola yang sering saya gunakan adalah memiliki antarmuka repositori tertentu, tetapi kelas dasar untuk implementasinya. Misalnya, menggunakan LINQ ke SQL, Anda bisa melakukan:

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

Ganti DataContextdengan unit kerja pilihan Anda. Contoh implementasi mungkin:

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

Perhatikan API publik dari repositori tidak memungkinkan pengguna untuk dihapus. Selain itu, mengekspos IQueryableadalah kaleng cacing lainnya - ada banyak pendapat seperti pusar pada topik itu.

Bryan Watts
sumber
9
Serius, jawaban yang bagus. Terima kasih!
Bip bip
5
Jadi bagaimana Anda menggunakan IoC / DI dengan ini? (Saya seorang pemula di IoC) Pertanyaan saya mengenai pola Anda secara lengkap: stackoverflow.com/questions/4312388/…
dan
36
"repositori adalah bagian dari domain yang dimodelkan, dan domain itu tidak generik. Tidak setiap entitas dapat dihapus, tidak setiap entitas dapat ditambahkan, tidak setiap entitas memiliki repositori" sempurna!
adamwtiko
Saya tahu ini adalah jawaban lama, tapi saya ingin tahu jika meninggalkan metode Pembaruan dari kelas Repositori dengan sengaja. Saya kesulitan menemukan cara yang bersih untuk melakukan ini.
rtf
1
Oldie tapi goodie. Posting ini sangat bijak dan harus dibaca dan dibaca kembali tetapi semua pengembang kaya. Terima kasih @BryanWatts. Implementasi saya biasanya berbasis necis, tetapi premisnya sama. Repositori dasar, dengan repositori khusus untuk mewakili domain yang memilih untuk ikut serta dalam fitur.
pimbrouwers
27

Saya sebenarnya sedikit tidak setuju dengan jabatan Bryan. Saya pikir dia benar, yang pada akhirnya semuanya sangat unik dan sebagainya. Tetapi pada saat yang sama, sebagian besar keluar saat Anda mendesain, dan saya menemukan bahwa mendapatkan repositori generik dan menggunakannya saat mengembangkan model saya, saya bisa mendapatkan aplikasi dengan sangat cepat, kemudian refactor ke spesifisitas yang lebih besar ketika saya menemukan perlu melakukannya.

Jadi, dalam kasus seperti itu, saya sering membuat IRepository generik yang memiliki tumpukan CRUD penuh, dan itu memungkinkan saya cepat bermain dengan API dan membiarkan orang bermain dengan UI dan melakukan pengujian integrasi & penerimaan pengguna secara paralel. Kemudian, ketika saya menemukan saya memerlukan pertanyaan spesifik pada repo, dll, saya mulai mengganti ketergantungan itu dengan yang spesifik jika diperlukan dan pergi dari sana. Satu imp yang mendasarinya. mudah untuk dibuat dan digunakan (dan mungkin menghubungkan ke db di memori atau objek statis atau objek mengejek atau apa pun).

Yang mengatakan, apa yang saya mulai lakukan akhir-akhir ini adalah memecah perilaku. Jadi, jika Anda melakukan antarmuka untuk IDataFetcher, IDataUpdater, IDataInserter, dan IDataDeleter (misalnya), Anda dapat mencampur dan mencocokkan untuk menentukan persyaratan Anda melalui antarmuka dan kemudian memiliki implementasi yang menangani beberapa atau semuanya, dan saya bisa masih menyuntikkan penerapan do-it-all untuk digunakan saat saya membangun aplikasi.

paul

Paul
sumber
4
Terima kasih atas balasannya @Paul. Saya sebenarnya mencoba pendekatan itu juga. Saya tidak tahu bagaimana mengekspresikan secara umum metode pertama yang saya coba GetById(),. Apakah saya harus menggunakan IRepository<T, TId>, GetById(object id)atau membuat asumsi dan menggunakan GetById(int id)? Bagaimana cara kerja kunci komposit? Saya bertanya-tanya apakah seleksi generik dengan ID adalah abstraksi yang bermanfaat. Jika tidak, apa lagi yang repositori generik akan dipaksa untuk diungkapkan secara akwardly? Itu adalah garis alasan di balik abstrak implementasi , bukan antarmuka .
Bryan Watts
10
Juga, mekanisme permintaan generik adalah tanggung jawab ORM. Gudang Anda harus menerapkan kueri spesifik untuk entitas proyek Anda dengan menggunakan mekanisme kueri umum. Konsumen repositori Anda tidak boleh dipaksa untuk menulis pertanyaan mereka sendiri kecuali itu adalah bagian dari domain masalah Anda, seperti dengan pelaporan.
Bryan Watts
2
@Bryan - Mengenai GetById Anda (). Saya menggunakan FindById <T, TId> (TId id); Sehingga menghasilkan sesuatu seperti repositori.FindById <Invoice, int> (435);
Joshua Hayes
Saya biasanya tidak menaruh metode kueri tingkat lapangan pada antarmuka umum, terus terang. Seperti yang Anda tunjukkan, tidak semua model harus ditanyakan dengan satu kunci, dan dalam beberapa kasus aplikasi Anda tidak akan pernah mengambil sesuatu dengan ID sama sekali (misalnya jika Anda menggunakan kunci primer yang dihasilkan oleh DB dan Anda hanya mengambil dengan kunci alami, misalnya nama login). Metode kueri berevolusi pada antarmuka spesifik yang saya buat sebagai bagian dari refactoring.
Paul
13

Saya lebih suka repositori spesifik yang berasal dari repositori generik (atau daftar repositori generik untuk menentukan perilaku yang tepat) dengan tanda tangan metode yang dapat ditimpa.

Arnis Lapsa
sumber
Bisakah Anda memberikan contoh snippet-ish kecil?
Johann Gerell
@ Johann Gerell tidak, karena saya tidak menggunakan repositori lagi.
Arnis Lapsa
apa yang Anda gunakan sekarang sehingga Anda tinggal jauh dari repositori?
Chris
@ Chris saya sangat fokus pada memiliki model domain yang kaya. masukan bagian dari aplikasi adalah yang penting. jika semua perubahan status dipantau secara hati-hati, tidak masalah seberapa banyak Anda membaca data selama cukup efisien. jadi saya hanya menggunakan NHibernate ISession secara langsung. tanpa abstraksi lapisan repositori, akan jauh lebih mudah untuk menentukan hal-hal seperti eager loading, multi queries, dll. dan jika Anda benar-benar membutuhkannya, tidak sulit untuk mengabaikan ISession juga.
Arnis Lapsa
3
@JesseWebb Naah ... Dengan logika kueri model domain kaya akan disederhanakan secara signifikan. Misalnya, jika saya ingin mencari pengguna yang telah membeli sesuatu, saya hanya mencari pengguna. Di mana (u => u. Telah dibeli) Apa saja, bukan pengguna. Bergabung (x => Pesanan, sesuatu sesuatu, saya tidak tahu linq). Di mana ( order => order.Status == 1) .Gabung (x => x.produk) .Dimana (x .... dll dll dll .... bla bla bla
Arnis Lapsa
5

Memiliki repositori generik yang dibungkus oleh repositori tertentu. Dengan begitu Anda dapat mengontrol antarmuka publik tetapi masih memiliki keuntungan dari penggunaan kembali kode yang berasal dari memiliki repositori generik.

Peter
sumber
2

UserRepository kelas publik: Repository, IUserRepository

Tidakkah Anda harus menyuntikkan IUserRepository untuk menghindari mengekspos antarmuka. Seperti yang dikatakan orang, Anda mungkin tidak perlu tumpukan CRUD penuh dll.

Ste
sumber
2
Ini lebih mirip komentar daripada jawaban.
Amit Joshi