کنید دنبال

آشنایی با الگوی Generic Repository

در این مقاله به بررسی الگویی بسیار مهم و جذاب با عنوان Generic Repository میپردازیم. این الگو را میتوان برای استفاده های مختلف پیاده سازی نمود، اما به طور عمده برای ارتباط نرم افزار و پایگاه داده کاربرد دارد. از مزایای این الگو میتوان به موارد زیر اشاره کرد: (ترجیحا مفاهیم مختصر زیر را مطالعه کنید تا به چرایی استفاده از این الگو پی ببرید.)

  • تسهیل و الگوپذیر بودن پیاده سازی ارتباط با دیتابیس و عملیات CRUD
  • پیاده سازی دقیق مفهوم Encapsulation

همانطور که میدانید، مفهوم کپسوله سازی، یکی از مفاهیم بسیار مهم در کدنویسی شیء‎‌گراست. (البته امروزه تقریبا تمام زبانهای غیر شیء گرا نیز اهمیت بسیار زیاد این موضوع را انکار نمیکنند.) به طور معمول یک پروژه برنامه نویسی از ماژول های مختلفی تشکیل شده است. اگر مفهوم کپسوله سازی در پیاده سازی ماژول ها رعایت شود، ماژول A صرفا میداند که ماژول B چه کاری میکند و از اینکه چگونه کار میکند اطلاعی ندارد. بنابراین اگر در نحوه پیاده سازی یک ماژول تغییری ایجاد شود، سایر ماژول ها دست خوش تغییر نخواهند شد(چون به چگونه کار کردن یا همان نحوه پیاده سازی هیچ وابستگی ندارند).

 اگر بخواهیم مفهوم کپسوله سازی را در سطح این مقاله شرح دهیم، میتوان گفت که ماژولهای پروژه ما نیاز دارند تا عملیات Crud را انجام دهند، اما معمولا نیازی به اینکه بدانند ما از چه پایگاه داده ای استفاده کرده ایم و یا برای ارتباط با پایگاه داده از چه فریم ورک هایی استفاده کرده ایم ندارند.(مثلا ماژول ثبت نام نیاز دارد تا یک کاربر را در دیتابیس درج کند، اما اینکه دیتابیس پروژه چیست و با چه فریم ورکی اینزرت انجام میشود اصلا اهمیتی ندارد.) به زبان دیگر، شاید نیاز باشد تا ما از یک پایگاه داده(مثلا Sql Server) به یک پایگاه داده دیگر(مثلا Mongo DB) کوچ کنیم. قاعدتا ماژولهای پروژه ما بر اساس این تغییر نباید نیاز به اصلاح پیدا کنند و صرفا باید نحوه پیاده سازی ماژول ارتباط با پایگاه داده اصلاح شود. در یک سناریوی دیگر، شاید نیاز داشته باشیم تا در تمام قسمتهای پروژه Entity Framework را حذف و Dapper را جایگزین کنیم. اگر این تغییر باعث شود که تمام ماژولهای پروژه نیاز به اصلاح پیدا کنند قطعا کپسوله سازی رعایت نشده است. با استفاده از الگوی جنریک، این مورد به شکل بسیار دقیقی قابل پیاده سازی است.

در این مقاله

  •  معماری پروژه را در نظر نمیگیریم و صرفا به نحوه پیاده سازی جنریک میپردازیم. این الگو را میتوان در معماری های مختلفی استفاده نمود.
  •   از پروژه و پایگاه داده ایجاد شده در مقاله «طراحی پایگاه داده codefirst» استفاده میکنیم. در صورت نیاز ابتدا به مطالعه مقاله مذکور بپردازید.
  •   پروژه از نوع Asp.net WebApi و Core 6 است. در Core 6 پیکربندی های پروژه از طریق کلاس Program انجام میشود، در حالی که در نسخه های پایین تر Core، به جای کلاس Program از کلاس Startup استفاده میشود، اما شکل کلی پیکربندی ها مشابه و با کمی اصلاح قابل پیاده سازی است.
  • از EntityFramework استفاده شده است و در مقالات بعدی Dapper را نیز بررسی خواهیم کرد.

(خب توضیحات تئوری کافیه، بریم سراغ پیاده سازی و کد زدن:)

Generic Repository از چه اجزایی تشکیل میشود.

  • به طور کلی ما در این الگو یک قسمت پایه داریم که شامل عملیات مشترک میان تمام انتیتی ها میشود. در این قسمت متدهای Insert، Update، Delete و هر آنچه که مشترکا برای کار با تمام انتیتی ها نیاز داریم را جهت جلوگیری از کدنویسی اضافه و تکراری پیاده سازی میکنیم.این قسمت شامل اینترفیس IBaseRepository و کلاس BaseRepository است.
  • یک قسمت اختصاصی برای انتیتی ها داریم که در حالت عادی شامل تمام متدهای قسمت پایه به علاوه متدهای ویژه و خاص هر انتیتی است.این قسمت به ازای هر انتیتی شامل یک اینترفیس و یک کلاس است.

مراحل کدنویسی

1)    ابتدا فولدر Infrastructure را که خود شامل دو فولدر Interface و Repository است در سورس پروژه میکنیم.

اینترفیس های مربوطه را در فولدر Interface و کلاس ها را در فولدر Repository پیاده سازی میکنیم.

2)    ابتدا اینترفیس IBaseRepository را که شامل تعریف متدهای پایه و مشترک است ایجاد میکنیم.

public interface IBaseRepository where TEtity : EntityBase

    {

        Task>
GetAllAsync();

        Task>
GetAsync(Expression> predicate);

        Task>
GetAsync(Expression> predicate = null,

                                       
Func, IOrderedQueryable>
orderBy = null,

                                        string includeString = null,

                                        bool disableTracking = true);

        Task>
GetAsync(Expression> predicate = null,

                                       Func,
IOrderedQueryable> orderBy = null,

                                      
List>> includes = null,

                                       bool disableTracking = true);

        Task GetByIdAsync(int id);

        Task AddAsync(TEtity
entity);

        Task UpdateAsync(TEtity entity);

        Task DeleteAsync(TEtity entity);

    }

3)    کلاس BaseRepository که وظیفه پیاده سازی متدهای موجود در IBaseRepository را دارد پیاده سازی میکنیم. دقت کنید که TEntity در این نمونه، مشخص کننده نوع کلاس انتیتی است و در ادامه مراحل کاربرد آن را خواهید دید.

public class BaseRepository :IBaseRepository where TEntity : EntityBase

    {

        protected readonly Context _dbContext;

        public BaseRepository(Context dbContext)

        {

            _dbContext = dbContext ?? throw new
ArgumentNullException(nameof(dbContext));

        }

        public async Task> GetAllAsync()

        {

            return await _dbContext.Set().ToListAsync();

        }

        public async Task>
GetAsync(Expression> predicate)

        {

            return await _dbContext.Set().Where(predicate).ToListAsync();

        }

        public async Task>
GetAsync(Expression> predicate = null, Func,
IOrderedQueryable> orderBy = null, string includeString = null, bool disableTracking = true)

        {

            IQueryable query =
_dbContext.Set();

            if (disableTracking) query = query.AsNoTracking();

            if (!string.IsNullOrWhiteSpace(includeString)) query =
query.Include(includeString);

            if (predicate != null) query = query.Where(predicate);

            if (orderBy != null)

                return await orderBy(query).ToListAsync();

            return await query.ToListAsync();

        }

        public async Task>
GetAsync(Expression> predicate = null, Func,
IOrderedQueryable> orderBy = null, List>>
includes = null, bool disableTracking = true)

        {

            IQueryable query =
_dbContext.Set();

            if (disableTracking) query = query.AsNoTracking();

            if (includes != null) query = includes.Aggregate(query, (current, include) =>
current.Include(include));

            if (predicate != null) query = query.Where(predicate);

            if (orderBy != null)

                return await
orderBy(query).ToListAsync();

            return await query.ToListAsync();

        }

        public virtual async Task GetByIdAsync(int id)

        {

            return await _dbContext.Set().FindAsync(id);

        }

        public async Task AddAsync(TEntity entity)

        {

           
_dbContext.Set().Add(entity);

            await _dbContext.SaveChangesAsync();

            return entity;

        }

        public async Task UpdateAsync(TEntity entity)

        {

            _dbContext.Entry(entity).State =
EntityState.Modified;

            await _dbContext.SaveChangesAsync();

        }

        public async Task DeleteAsync(TEntity entity)

        {

           
_dbContext.Set().Remove(entity);

            await _dbContext.SaveChangesAsync();

        }

    }

در مراحل فوق، قسمت مشترک میان تمام انتیتی ها پیاده سازی شد.

4)    به ازای هر انتیتی یک اینترفیس و یک کلاس خواهیم ساخت. این اینترفیس ها و کلاسها محل اتصال ماژول های ما و انتیتی جهت عملیات Crud خواهند بود که در انتهای مقاله خواهید دید.

public interface IUserRepository:IBaseRepository

    {

 }

public class BaseRepository : GenericRepository, IBaseRepository

    {

        public UserRepository(Context dbContext) : base(dbContext)

        {

        }

    }

در این نمونه، اینترفیس و کلاس جنریک مربوط به انتیتی User را پیاده سازی کردیم. همانطور که میبینید به واسطه ارث بری کلاس UserRepository از BaseRepository تمامی متدهای کلاس پایه در UserRepository وجود دارد. اگر  بخواهیم متدهای اضافه تری، مخصوص انتیتی یوزر داشته باشیم، میتوانیم ابتدا در اینترفیس IUserRepository تعریف و سپس در کلاس UserRepository پیاده سازی کنیم. همچنین TEntity نیز با تایپ انتیتی(User) مقداردهی شده است.

1)    TEntity در کلاس BaseRepository چه کاربردی دارد؟

از آنجا که کلاسهای پایه به تمامی انتیتی ها اختصاص دارند، نیاز است تا مشخص شود که دقیقا کلاس پایه عملیات خود را روی کدام انتیتی(کدام جدول) انجام دهد. بنابریاین TEntity وظیفه مشخص کردن این موضوع را بر عهده دارد و از طریق کلاسهای جنریک مربوط به هر انتیتی، همانند نمونه فوق مشخص میشوند.

2)    چرا برای هر انتیتی یک اینترفیس استفاده کردیم؟

این موضوع دقیقا به توضیحات ابتدای مقاله، پیرامون موضوع کپسوله سازی دلالت دارد. اینترفیس ها مشخص میکنند که ماژول ما چه کاری میکند و همانطور که میبینید خالی از هرگونه پیاده سازی هستند. کلاسها وظیفه پیاده سازی وظایف را بر عهده دارند. بدون اینترفیس ها در بسیاری از موارد(به طور مثال در معماری کلین) به هیچ وجه قادر نخواهیم بود تا کپسوله سازی را پیاده سازی کنیم. هر چند این موضوع به معماری پروژه ما وابستگی زیادی دارد، اما بهترین شکل پیاده سازی کپسوله سازی، صرفا با وجود اینترفیس ها مقدور است.

5)    به روش مشابه، برای تمام انتیتی ها پیاده سازی را انجام میدهیم.

الگوی ما پیاده سازی شده است، اکنون ماژولهای مختلف میتوانند با استفاده از یکی از دو روش مرسوم زیر عملیات CRUD را به راحتی انجام دهند.

برای اینکه بتوانیم در نهایت از الگوی فوق استفاده کنیم،  یک کنترلر نمونه به نام Home ایجاد و در آن یک اکشن به نام Sample اضافه نموده ایم. اکشن Sample، از الگوی فوق به دو روش زیر جهت دریافت یک User با نام کاربری Rabiee از پایگاه داده استفاده میکند.

روش اول: UnitOfWork

در این روش، یک کلاس به نام UnitOfWork خواهیم داشته که از هر ریپازیتوری، یک آبجکت به عنوان پارامتر دارد. هر زمان که بخواهیم از ریپازیتوری ها استفاده کنیم، یک آبجکت از کلاس UnitOfWork میسازیم و به این واسطه به تمام ریپازیتوری ها دسترسی داریم. پیاده سازی کلاس UnitOfWork در نمونه ما به شکل زیر خواهد بود.

public class UnitOfWork

    {

        Context DBContext = new Context();

        IProductRepository productRepository { get; set; }

        IUserRepository userRepository { get; set; }

        ICategoryRepository categoryRepository
{ get; set; }

        IBasketRepository basketRepository { get; set; }

        IPaymentRepository paymentRepository { get; set; }

        public IProductRepository Products

        {

            get

            {

                if (productRepository == null)

                {

                    productRepository = new
ProductRepository(DBContext);

                }

                return productRepository;

            }

        }

        public IUserRepository users

        {

            get

            {

                if (userRepository == null)

                {

                    userRepository = new UserRepository(DBContext);

                }

                return userRepository;

            }

        }

        public ICategoryRepository Categories

        {

            get

            {

                if (categoryRepository == null)

                {

                    categoryRepository = new
CategoryRepository(DBContext);

                }

                return categoryRepository;

            }

        }

        public IBasketRepository Basket

        {

            get

            {

                if (basketRepository == null)

                {

                    basketRepository = new BasketRepository(DBContext);

                }

                return basketRepository;

            }

        }

        public IPaymentRepository Payment

        {

            get

            {

                if (paymentRepository == null)

                {

                    paymentRepository = new
PaymentRepository(DBContext);

                }

                return paymentRepository;

            }

        }

    }

برای اینکه بتوانیم از UnitOfWork استفاده کنیم، یک پارامتر از این نوع با نام _Context در کنترلر قرار داده ایم. اکشن Sample، از _Context جهت دریافت یک User با نام کاربری Rabiee استفاده میکند.

  [Route("Home")]

    [ApiController]

    public class HomeController : ControllerBase

    {

        UnitOfWork _Context;

        public HomeController()

        {

            _Context = new UnitOfWork();

        }

        [HttpGet]

        [Route("Sample")]

        public async Task Sample()

        {

            var users =(await _Context.users.GetAsync(a=>a.Username=="Rabiee")).FirstOrDefault();

            return Ok();

        }

    }

همانطور که میبینید،در سازنده کنترلر یک آبجکت از نوع UnitOfWork به نام _Context ایجاد نموده و در اکشن Sample با استفاده از آن کاربری را که نام کاربری او Rabiee است خارج کردیم.

در نهایت، در صورتی که قصد دارید با استفاده از UnitOfWork پیاده سازی را انجام دهید، قطعه کد زیر را به کلاس Context اضافه کنید.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
           if (!optionsBuilder.IsConfigured)
           {
                   optionsBuilder.UseSqlServer("Server=.;Database=Seller;User Id=sa;Password=123456;");
            }
     }

روش دوم: استفاده از تزریق وابستگی(Dependency Injection)

این روش مرسوم تر و بسیار کاربردی تری نسبت به UnitOfWork است، چرا که در معماری های سطح بالا نمیتوان از UnitOfWork استفاده کرد اما این روش در هر معماری به شکل دقیقی و بسیار کارآمد قابل استفاده است.

در پروژه های Asp.net Core، میتوان از تزریق وابستگی به صورت Built In استفاده نمود. ابتدا باید در کلاس Program یا Startup، مشخص کنیم که قرار است چه کلاسهایی Inject شود. قاعدتا در این مثال باید بتوانیم اینترفیس های مربوط به انتیتی ها را در تمامی ماژول ها Inject کنیم تا در صورت نیاز استفاده کنند. بنابراین از قطعه کد زیر استفاده میکنیم.

builder.Services.AddScoped(typeof(IBaseRepository<>), typeof(BaseRepository<>));

builder.Services.AddScoped(typeof(IUserRepository), typeof(UserRepository));

builder.Services.AddScoped(typeof(ICategoryRepository), typeof(CategoryRepository));

builder.Services.AddScoped(typeof(IProductRepository), typeof(ProductRepository));

builder.Services.AddScoped(typeof(IPaymentRepository), typeof(PaymentRepository));

builder.Services.AddScoped(typeof(IBasketRepository),
typeof(BasketRepository));

به واسطه کد فوق، میتوانیم ریپازیتوری مد نظر را در سازنده کلاسهای مختلف به عنوان ورودی درج و از آن استفاده کنیم.(اینجکت کنیم)

در کنترلر Home، ریپازیتوری مربوط به User را اینجکت میکنیم. سپس از آن در اکشن Sample جهت استخراج کاربری با نام Rabiee استفاده میکنیم. پیاده سازی به شکل زیر است.

[Route("Home")]

    [ApiController]

    public class HomeController : ControllerBase

    {

        IUserRepository _userRepository;

        public HomeController(IUserRepository userRepository)

        {

            _userRepository = userRepository;

        }

        [HttpGet]

        [Route("Sample")]

        public async Task Sample()

        {

            var users =(await _userRepository.GetAsync(a=>a.Username=="Rabiee")).FirstOrDefault();

            return Ok();

        }

    }

در نهایت با فراخوانی سرویس  Sample(http://localhost:5232/home/sample) خواهید دید که ارتباط با پایگاه داده به درستی برقرار خواهد شد.

  •  این مقاله صرف نظر از پیاده سازی معماری است. به طور کلی الگوی جنریک شامل همین مفاهیم است و معماری صرفا محل قرار گیری کلاسها و اینترفیس ها را در پروژه تغییر میدهد.
  • در این مقاله جهت پیاده سازی کلاسهای پایه از EntityFramework استفاده شد، اما این اجبار نیست و شما میتوانید در هر یک از متدها، از framework مد نظر خود استفاده کنید.
  • همانطور که در ابتدای مقاله اشاره شد، اگر بخواهیم پایگاه داده پروژه را تغییر دهیم صرفا پیاده سازی های داخل کلاسهای ریپازیتوری را تغییر میدهیم و سایر ماژولها که صرفا اینترفیس ها را میبینند با تغییری مواجه نخواهند شد.

سورس کد پروژه در گیت هاب(کلیک کنید)

در مقالات بعد جهت آشنایی با فریم ورک پرکاربرد Dapper، سناریوی نمونه ای را پیاده سازی خواهیم کرد.

مشاهده مقاله در ویرگول 

نظر خود را بنویسید.

Theme Skin
Select your color