ข้อดีของการสร้างที่เก็บทั่วไปเทียบกับที่เก็บเฉพาะสำหรับแต่ละอ็อบเจ็กต์?


132

เรากำลังพัฒนาแอปพลิเคชัน ASP.NET MVC และตอนนี้กำลังสร้างคลาสที่เก็บ / บริการ ฉันสงสัยว่าการสร้างอินเทอร์เฟซ IRepository ทั่วไปมีประโยชน์หรือไม่เมื่อเทียบกับแต่ละ Repository ที่มีอินเทอร์เฟซและชุดวิธีการเฉพาะของตัวเอง

ตัวอย่างเช่นอินเทอร์เฟซ IRepository ทั่วไปอาจมีลักษณะดังนี้ (นำมาจากคำตอบนี้ ):

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

แต่ละ Repository จะใช้อินเทอร์เฟซนี้ตัวอย่างเช่น:

  • CustomerRepository: IRepository
  • ProductRepository: IRepository
  • เป็นต้น

ทางเลือกอื่นที่เราติดตามในโครงการก่อนหน้าคือ:

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

ในกรณีที่สองนิพจน์ (LINQ หรืออื่น ๆ ) จะมีอยู่ทั้งหมดในการนำ Repository ไปใช้งานใครก็ตามที่ใช้บริการเพียงแค่ต้องรู้ว่าจะเรียกใช้ฟังก์ชันที่เก็บใด

ฉันเดาว่าฉันไม่เห็นข้อดีของการเขียนไวยากรณ์นิพจน์ทั้งหมดในคลาสบริการและส่งไปยังที่เก็บ นี่จะไม่หมายความว่ารหัส LINQ ที่ง่ายต่อการทำให้สับสนในหลาย ๆ กรณีหรือไม่?

ตัวอย่างเช่นในระบบการออกใบแจ้งหนี้เดิมเราเรียก

InvoiceRepository.GetSingleInvoice(DateTime invoiceDate, int accountId)

จากบริการที่แตกต่างกันเล็กน้อย (ลูกค้าใบแจ้งหนี้บัญชี ฯลฯ ) ดูเหมือนว่าจะสะอาดกว่าการเขียนสิ่งต่อไปนี้ในหลาย ๆ ที่:

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

ข้อเสียเดียวที่ฉันเห็นในการใช้วิธีการเฉพาะคือเราสามารถจบลงด้วยการเรียงสับเปลี่ยนของฟังก์ชัน Get * มากมาย แต่ดูเหมือนว่าจะดีกว่าในการผลักตรรกะนิพจน์เข้าสู่คลาส Service

ฉันขาดอะไรไป?


การใช้ที่เก็บข้อมูลทั่วไปที่มี ORM แบบเต็มดูไม่มีประโยชน์ ฉันได้กล่าวนี้ในรายละเอียดที่นี่
Amit Joshi

คำตอบ:


170

นี่เป็นปัญหาที่เก่าพอ ๆ กับรูปแบบ Repository เอง การเปิดตัวล่าสุดของ LINQ IQueryableซึ่งเป็นตัวแทนของข้อความค้นหาที่สม่ำเสมอทำให้เกิดการสนทนามากมายเกี่ยวกับหัวข้อนี้

ฉันชอบที่เก็บที่เฉพาะเจาะจงด้วยตัวเองหลังจากทำงานหนักมากเพื่อสร้างกรอบพื้นที่เก็บข้อมูลทั่วไป ไม่ว่าฉันจะลองใช้กลไกอะไรที่ชาญฉลาดฉันก็มักจะพบกับปัญหาเดียวกันเสมอ: ที่เก็บเป็นส่วนหนึ่งของโดเมนที่กำลังสร้างแบบจำลองและโดเมนนั้นไม่ใช่แบบทั่วไป ไม่ใช่ทุกเอนทิตีที่สามารถลบได้ไม่ใช่ทุกเอนทิตีที่สามารถเพิ่มได้ไม่ใช่ทุกเอนทิตีจะมีที่เก็บ ข้อความค้นหาแตกต่างกันไปอย่างมาก API ที่เก็บจะมีลักษณะเฉพาะเช่นเดียวกับเอนทิตีเอง

รูปแบบที่ฉันมักใช้คือการมีอินเทอร์เฟซที่เก็บเฉพาะ แต่เป็นคลาสพื้นฐานสำหรับการนำไปใช้งาน ตัวอย่างเช่นการใช้ LINQ เป็น SQL คุณสามารถทำได้:

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

แทนที่DataContextด้วยหน่วยการทำงานที่คุณเลือก ตัวอย่างการใช้งานอาจเป็น:

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

โปรดสังเกตว่า API สาธารณะของที่เก็บไม่อนุญาตให้ลบผู้ใช้ นอกจากนี้การเปิดเผยIQueryableยังเป็นอีกหนึ่งกระป๋องของเวิร์ม - มีความคิดเห็นมากมายพอ ๆ กับปุ่มท้องในหัวข้อนั้น


5
แล้วคุณจะใช้ IoC / DI กับสิ่งนี้อย่างไร? (ฉันเป็นมือใหม่ที่ IoC) คำถามของฉันเกี่ยวกับรูปแบบของคุณแบบเต็ม: stackoverflow.com/questions/4312388/…
dan

36
"ที่เก็บเป็นส่วนหนึ่งของโดเมนที่สร้างแบบจำลองและโดเมนนั้นไม่ใช่โดเมนทั่วไปไม่สามารถลบทุกเอนทิตีไม่สามารถเพิ่มทุกเอนทิตีไม่ใช่ทุกเอนทิตีจะมีที่เก็บ" สมบูรณ์แบบ!
adamwtiko

ฉันรู้ว่านี่เป็นคำตอบเก่า แต่ฉันสงสัยว่าถ้าปล่อยวิธีการอัปเดตออกจากคลาส Repository ด้วยเจตนา ฉันมีปัญหาในการหาวิธีที่สะอาดเพื่อทำสิ่งนี้
rtf

@Tanner: การอัปเดตมีความหมายโดยนัย - เมื่อคุณแก้ไขวัตถุที่ถูกติดตามจากนั้นคอมมิตDataContextLINQ กับ SQL จะออกคำสั่งที่เหมาะสม
Bryan Watts

1
Oldie แต่เป็นคนดี โพสต์นี้ฉลาดอย่างไม่น่าเชื่อและควรอ่านและอ่านซ้ำ แต่นักพัฒนาที่น่าสนใจทุกคน ขอบคุณ @BryanWatts โดยทั่วไปการใช้งานของฉันจะขึ้นอยู่กับ dapper แต่หลักฐานก็เหมือนกัน ที่เก็บฐานพร้อมที่เก็บเฉพาะเพื่อแสดงโดเมนที่เลือกใช้คุณลักษณะ
PIM

27

ฉันไม่เห็นด้วยเล็กน้อยกับโพสต์ของไบรอัน ฉันคิดว่าเขาพูดถูกในที่สุดทุกอย่างก็มีเอกลักษณ์และอื่น ๆ แต่ในขณะเดียวกันสิ่งนั้นส่วนใหญ่จะออกมาในขณะที่คุณออกแบบและฉันพบว่าการได้รับที่เก็บข้อมูลทั่วไปและใช้งานในขณะที่พัฒนาโมเดลของฉันฉันสามารถดาวน์โหลดแอปได้อย่างรวดเร็วจากนั้นปรับโครงสร้างให้มีความเฉพาะเจาะจงมากขึ้นเมื่อฉันพบ จำเป็นต้องทำเช่นนั้น

ดังนั้นในกรณีเช่นนี้ฉันมักจะสร้าง IRepository ทั่วไปที่มีสแต็ก CRUD เต็มรูปแบบและนั่นทำให้ฉันสามารถเล่นกับ API ได้อย่างรวดเร็วและปล่อยให้ผู้คนเล่นกับ UI และทำการทดสอบการรวมและการยอมรับของผู้ใช้ควบคู่กันไป จากนั้นเมื่อฉันพบว่าฉันต้องการคำถามเฉพาะเกี่ยวกับ repo ฯลฯ ฉันเริ่มแทนที่การอ้างอิงนั้นด้วยสิ่งที่เจาะจงหากจำเป็นและไปจากที่นั่น นัยหนึ่งที่แฝงอยู่ สร้างและใช้งานได้ง่าย (และอาจเชื่อมต่อกับฐานข้อมูลในหน่วยความจำหรือวัตถุคงที่หรือวัตถุจำลองหรืออะไรก็ตาม)

ที่กล่าวว่าสิ่งที่ฉันได้เริ่มทำเมื่อเร็ว ๆ นี้คือการทำลายพฤติกรรม ดังนั้นหากคุณทำอินเทอร์เฟซสำหรับ IDataFetcher, IDataUpdater, IDataInserter และ IDataDeleter (เช่น) คุณสามารถผสมและจับคู่เพื่อกำหนดความต้องการของคุณผ่านอินเทอร์เฟซจากนั้นจึงมีการใช้งานที่ดูแลบางส่วนหรือทั้งหมดและฉันสามารถ ยังคงอัดฉีดการนำไปใช้งานทั้งหมดเพื่อใช้ในขณะที่ฉันกำลังสร้างแอป

พอล


4
ขอบคุณสำหรับการตอบกลับ @Paul ฉันก็ลองใช้วิธีนั้นเช่นกัน ฉันคิดไม่ออกว่าจะแสดงวิธีการแรกที่ฉันลองโดยGetById()ทั่วไปได้อย่างไร ฉันควรใช้IRepository<T, TId>, GetById(object id)หรือทำให้สมมติฐานและการใช้งานGetById(int id)? คีย์คอมโพสิตทำงานอย่างไร? ฉันสงสัยว่าการเลือกทั่วไปโดย ID เป็นนามธรรมที่คุ้มค่าหรือไม่ ถ้าไม่เช่นนั้นที่เก็บข้อมูลทั่วไปจะถูกบังคับให้แสดงความอึดอัดใจอะไรอีก? นั่นคือสายของเหตุผลที่อยู่เบื้องหลังการสรุปการดำเนินงานไม่ได้อินเตอร์เฟซ
Bryan Watts

10
นอกจากนี้กลไกการสืบค้นทั่วไปยังเป็นความรับผิดชอบของ ORM ที่เก็บของคุณควรใช้การสืบค้นเฉพาะสำหรับเอนทิตีของโครงการของคุณโดยใช้กลไกการสืบค้นทั่วไป ผู้บริโภคของที่เก็บของคุณไม่ควรถูกบังคับให้เขียนคำค้นหาของตนเองเว้นแต่จะเป็นส่วนหนึ่งของโดเมนปัญหาของคุณเช่นการรายงาน
Bryan Watts

2
@ ไบรอัน - เกี่ยวกับ GetById ของคุณ () ฉันใช้ FindById <T, TId> (TId id); จึงทำให้เกิดบางอย่างเช่น repository.FindById <Invoice, int> (435);
Joshua Hayes

ฉันมักจะไม่ใส่วิธีการสอบถามระดับฟิลด์ในอินเทอร์เฟซทั่วไปอย่างตรงไปตรงมา ดังที่คุณได้ชี้ให้เห็นไม่ใช่ทุกรุ่นที่ควรถูกค้นหาด้วยคีย์เดียวและในบางกรณีแอปของคุณจะไม่ดึงข้อมูลบางอย่างด้วย ID เลย (เช่นหากคุณใช้คีย์หลักที่สร้างจากฐานข้อมูลและคุณจะดึงข้อมูลโดย คีย์ธรรมชาติเช่นชื่อล็อกอิน) วิธีการสืบค้นมีวิวัฒนาการบนอินเทอร์เฟซเฉพาะที่ฉันสร้างขึ้นเพื่อเป็นส่วนหนึ่งของการปรับโครงสร้างใหม่
พอล

13

ฉันชอบที่เก็บเฉพาะที่มาจากที่เก็บทั่วไป (หรือรายการของที่เก็บทั่วไปเพื่อระบุพฤติกรรมที่แน่นอน) พร้อมลายเซ็นเมธอดที่เขียนทับได้


คุณช่วยให้ตัวอย่างข้อมูลสั้น ๆ ได้ไหม
Johann Gerell

@Johann Gerell ไม่เพราะฉันไม่ใช้ที่เก็บอีกต่อไป
Arnis Lapsa

คุณใช้อะไรตอนนี้ที่คุณอยู่ห่างจากที่เก็บ?
คริส

@ คริสฉันมุ่งเน้นไปที่การมีโมเดลโดเมนที่หลากหลาย ส่วนอินพุตของแอปพลิเคชันคือสิ่งที่สำคัญ หากการเปลี่ยนแปลงสถานะทั้งหมดได้รับการตรวจสอบอย่างรอบคอบก็ไม่สำคัญว่าคุณจะอ่านข้อมูลมากเพียงใดตราบเท่าที่มีประสิทธิภาพเพียงพอ ดังนั้นฉันจึงใช้ NHibernate ISession โดยตรง หากไม่มีนามธรรมของเลเยอร์ที่เก็บจะง่ายกว่ามากในการระบุสิ่งต่างๆเช่นการโหลดอย่างกระตือรือร้นการสืบค้นหลายรายการ ฯลฯ และถ้าคุณต้องการจริงๆก็ไม่ยากที่จะล้อเลียน ISession
Arnis Lapsa

3
@JesseWebb Naah ... ด้วยตรรกะการสืบค้นแบบจำลองโดเมนที่หลากหลายทำให้ง่ายขึ้น เช่นถ้าฉันต้องการค้นหาผู้ใช้ที่ซื้อบางอย่างฉันก็แค่ค้นหาผู้ใช้โดยที่ (u => u.HasPurchasedAnything) แทนผู้ใช้เข้าร่วม (x => คำสั่งซื้อบางอย่างฉันไม่รู้ linq) ที่ไหน ( order => order.Status == 1) .Join (x => x.products) Where (x .... etc etc etc .... blah blah blah
Arnis Lapsa

5

มีที่เก็บทั่วไปที่ถูกรวมไว้โดยที่เก็บเฉพาะ ด้วยวิธีนี้คุณสามารถควบคุมอินเทอร์เฟซสาธารณะ แต่ยังมีข้อได้เปรียบของการใช้โค้ดซ้ำที่มาจากมีที่เก็บทั่วไป


3

UserRepository คลาสสาธารณะ: Repository, IUserRepository

คุณไม่ควรฉีด IUserRepository เพื่อหลีกเลี่ยงการเปิดเผยอินเทอร์เฟซ ดังที่ผู้คนกล่าวไว้คุณอาจไม่จำเป็นต้องมีสแต็ค CRUD แบบเต็มเป็นต้น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.