عنوان:

‫آشنایی با الگوی مخزن نشتی‌دار در برنامه‌نویسی #C و روش‌های رفع آن


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۱/۲۱ ۰۷:۱۸
آدرس: www.dntips.ir
مقدمه
الگوی مخزن (Repository Pattern) یکی از الگوهای پرکاربرد در توسعه نرم‌افزارهای مبتنی بر #C و فریم‌ورک .NET است که به منظور جداسازی منطق کسب‌وکار از لایه دسترسی به داده‌ها استفاده می‌شود. این الگو با هدف افزایش قابلیت تست‌پذیری، نگهداری آسان‌تر کد و ایجاد انعطاف‌پذیری در معماری برنامه‌ها طراحی شده است. با این حال، پیاده‌سازی نادرست این الگو می‌تواند به مشکلات متعددی از جمله نشت جزئیات پیاده‌سازی (Leaky Abstraction) منجر شود که وعده‌های اولیه آن را زیر سؤال می‌برد. در این مقاله آموزشی، ابتدا به بررسی مشکلات رایج در پیاده‌سازی الگوی مخزن می‌پردازیم و سپس راهکارهایی عملی و حرفه‌ای را برای بهینه‌سازی آن ارائه می‌دهیم. این مطلب برای توسعه‌دهندگانی که با فریم‌ورک .NET کار می‌کنند و به دنبال طراحی کد تمیز و قابل اطمینان هستند، تهیه شده است.

مشکلات رایج در پیاده‌سازی الگوی مخزن

نشت IQueryable و انعطاف‌پذیری بیش از حد
یکی از شایع‌ترین اشتباهات در طراحی مخزن، بازگرداندن نوع IQueryable<T> از متدهای مخزن است. این کار به ظاهر انعطاف‌پذیری بالایی به مصرف‌کنندگان می‌دهد، اما در عمل جزئیات پیاده‌سازی را افشا می‌کند. به عنوان مثال:
public interface IProductRepository
{
    IQueryable<Product> GetProducts();
}

public class ProductRepository : IProductRepository
{
    private readonly DbContext _context;
    
    public ProductRepository(DbContext context)
    {
        _context = context;
    }
    
    public IQueryable<Product> GetProducts()
    {
        return _context.Products;
    }
}
در این کد، مصرف‌کننده می‌تواند کوئری‌هایی مانند زیر بنویسد:
var discountedProducts = repository.GetProducts()
    .Where(p => p.Category == "EBooks")
    .Where(p => p.Price > 100)
    .OrderBy(p => p.Name)
    .Skip(100)
    .Take(10)
    .ToList();
مشکل اینجاست که مخزن هیچ کنترلی بر نحوه اجرای کوئری ندارد و ممکن است کوئری‌های ناکارآمد یا وابسته به ارائه‌دهنده داده (مانند Entity Framework) ایجاد شود. این نشت، جداسازی لایه‌ها را نقض می‌کند.

وابستگی به مفاهیم خاص فریم‌ورک
گاهی اوقات رابط‌های مخزن به مفاهیم خاص یک ORM (مانند Entity Framework) وابسته می‌شوند. برای مثال:
public interface IRepository<T> where T : class
{
    DbSet<T> Entities { get; }
    void Attach(T entity);
    void DetachEntity(T entity);
    EntityEntry<T> GetEntityEntry(T entity);
}
این رابط به وضوح به Entity Framework وابسته است و تغییر ارائه‌دهنده داده بدون بازنویسی کد مصرف‌کننده غیرممکن می‌شود.

نشت استثناها
عدم مدیریت صحیح استثناهای لایه داده نیز یکی دیگر از مشکلات رایج است. به عنوان مثال:
public Customer GetById(int id)
{
    try
    {
        return _context.Customers.Find(id);
    }
    catch (SqlException ex)
    {
        throw;
    }
}
در اینجا، استثنای SqlException که خاص SQL Server است، به لایه بالاتر نشت می‌کند و مصرف‌کننده را مجبور به وابستگی به جزئیات پایگاه داده می‌کند.

رفتارهای ناخواسته مانند بارگذاری تنبل (Lazy Loading)
وقتی مخزن اشیاء را با ویژگی‌های ناوبری (Navigation Properties) بازمی‌گرداند، مصرف‌کننده ممکن است ناخواسته با مشکلاتی مانند "N+1 Query Problem" مواجه شود:
public class OrderRepository : IOrderRepository
{
    private readonly DbContext _context;
    
    public Order GetById(int id)
    {
        return _context.Orders.Find(id);
    }
}

var order = orderRepository.GetById(1111);
foreach (var item in order.OrderItems)
{
    // ممکن است کوئری‌های اضافی اجرا شود
}
این رفتار به دلیل بارگذاری تنبل در Entity Framework رخ می‌دهد و عملکرد برنامه را تحت تأثیر قرار می‌دهد.

راهکارهای بهینه‌سازی الگوی مخزن

بازگرداندن مجموعه‌های مشخص به جای IQueryable
برای جلوگیری از نشت، بهتر است به جای IQueryable از مجموعه‌های مشخص مانند IReadOnlyCollection استفاده شود:
public interface IProductRepository
{
    IReadOnlyCollection<Product> GetProductsByCategory(string category);
    IReadOnlyCollection<Product> GetDiscountedProducts(decimal minPrice);
    PagedResult<Product> GetProductsPaged(int page, int pageSize, string sortField);
}
این روش، مخزن را مجبور می‌کند تا متدهای مشخص و هدفمندی تعریف کند که نیازهای کسب‌وکار را برآورده کند.

استفاده از الگوی مشخصات (Specification Pattern)
برای کوئری‌های پیچیده‌تر، الگوی مشخصات می‌تواند جایگزین مناسبی باشد:
public interface ISpecification<T>
{
    Expression<Func<T, bool>> Criteria { get; }
    List<Expression<Func<T, object>>> Includes { get; }
    Expression<Func<T, object>> OrderBy { get; }
    int Take { get; }
    int Skip { get; }
}

public interface IRepository<T>
{
    IReadOnlyList<T> FindAll(ISpecification<T> spec);
}

public class DiscountedEbooksSpecification : BaseSpecification<Product>
{
    public DiscountedEbooksSpecification(decimal minPrice)
        : base(p => p.Category == "Ebooks" && p.Price > minPrice)
    {
        AddOrderBy(p => p.Name);
        ApplyPaging(10, 10);
    }
}

var spec = new DiscountedEbooksSpecification(69);
var products = repository.FindAll(spec);
این الگو به مصرف‌کننده اجازه می‌دهد کوئری‌ها را بدون افشای جزئیات پیاده‌سازی تعریف کند.

مدیریت استثناها و انتزاع آنها
به جای نشت استثناهای خاص پایگاه داده، باید آنها را به استثناهای دامنه‌محور ترجمه کرد:
public class EntityNotFoundException : Exception
{
    public EntityNotFoundException(string entityType, object key)
        : base($"{entityType} با شناسه {key} یافت نشد.")
    {
    }
}

public Customer GetById(int id)
{
    try
    {
        var customer = _context.Customers.Find(id);
        if (customer == null)
            throw new EntityNotFoundException("Customer", id);
        return customer;
    }
    catch (SqlException ex)
    {
        throw new Exception("خطا در دسترسی به داده‌ها", ex);
    }
}
این رویکرد، وابستگی به فناوری خاص را از بین می‌برد.

بارگذاری مشتاقانه (Eager Loading) و استفاده از پروجکشن
برای جلوگیری از مشکلات بارگذاری تنبل، باید داده‌ها به صورت صریح بارگذاری شوند:
public Order GetOrderWithDetails(int id)
{
    return _context.Orders
        .Include(o => o.OrderItems)
        .ThenInclude(i => i.Product)
        .FirstOrDefault(o => o.Id == id);
}
یا از پروجکشن به DTOها استفاده شود:
public OrderDetailsDto GetOrderDetails(int id)
{
    return _context.Orders
        .Where(o => o.Id == id)
        .Select(o => new OrderDetailsDto
        {
            OrderId = o.Id,
            CustomerName = o.Customer.Name,
            Items = o.OrderItems.Select(i => new OrderItemDto
            {
                ProductName = i.Product.Name,
                Quantity = i.Quantity
            }).ToList()
        })
        .FirstOrDefault();
}
این روش کنترل بیشتری بر داده‌های بارگذاری‌شده فراهم می‌کند.

نتیجه‌گیری
الگوی مخزن اگرچه ابزاری قدرتمند برای جداسازی لایه‌ها در برنامه‌های .NET است، اما پیاده‌سازی نادرست آن می‌تواند به نشت جزئیات پیاده‌سازی و کاهش کارایی منجر شود. با اجتناب از بازگرداندن IQueryable renormalization، استفاده از الگوی مشخصات برای کوئری‌های پیچیده، مدیریت صحیح استثناها و به‌کارگیری بارگذاری مشتاق یا پروجکشن، می‌توان مخزن‌هایی طراحی کرد که واقعاً به وعده‌های خود عمل کنند. این اصول به توسعه‌دهندگان کمک می‌کند تا کدهایی تمیز، قابل نگهداری و مستقل از فناوری بنویسند که در بلندمدت ارزش خود را حفظ کنند.


مشاهده مطلب اصلی