از LINQ To Entities و Entity Framework استفاده کنید. بهینهتر هست.
نظرات مطالب
5 دلیل برای استفاده از یک ابزار ORM
strongType ها و entity ها را می توان هم پای فناوری های مطرح ORM قرار داد؟
در ادامهی طراحی مبتنی بر مینیمالیسم C# 10.0، پس از پیشفرض شدن «top level programs» و همچنین «کاهش تعداد بار تعاریف usingها»، تغییر سوم صورت گرفتهی در قالبهای پروژههای مبتنی بر دات نت 6، ساده سازی تعاریف فضاهای نام است. برای مثال یک کنترلر، به این صورت تعریف شدهاست:
که به آن «File-Scoped Namespaces» هم گفته میشود.
بررسی مفهوم «File-Scoped Namespaces»
یکی از اهداف مهم C# 10.0، کاهش نویز موجود در فایلهای cs. است. اگر قرار است صدها بار در فایلهای مختلف برنامه، using System نوشته شود، چرا یکبار آنرا به صورت عمومی تعریف نکنیم و یا اگر در 99 درصد موارد، توسعه دهندهها به ازای یک فایل، تنها یک فضای نام را تعریف میکنند، چرا باید یک فضای اضافی خالی، برای تعریف آن اختصاص داده شود و تمام فایلها به همراه یک «tab فاصلهی» اضافی مختص به این فضای نام باشند؟
تعریف فعلی فضاهای نام در #C به صورت زیر است:
در این حالت هر شیءای که داخل {} این فضای نام قرار گیرد، متعلق به آن است.
در C# 10.0، میتوان این تعریف را ساده کرد؛ از آنجائیکه به ندرت چند فضای نام در یک تک فایل تعریف میشوند، میتوان تعریف فضای نام را در یک سطر، در ابتدای فایل ذکر کرد، تا به صورت خودکار به کل فایل و اشیاء موجود در آن اعمال شود:
در این حالت، روش استفادهی از یک چنین اشیایی هیچ تغییری نخواهد کرد؛ فقط یک tab space و فاصله از کنار صفحه، صرفهجویی میشود!
محدویتهای «File-Scoped Namespaces»
- بدیهی است در این حالت دیگر نمیتوان چندین فضای نام را همانند قبل در یک فایل cs. تعریف کرد:
و البته این موردی است که جزو best practices توسعهی برنامههای #C به هیچ عنوان توصیه نمیشود.
- همچنین امکان ترکیب روش قبلی تعریف فضاهای نام، با روش جدید، در یک فایل وجود ندارد.
- به علاوه امکان تعریف فضاهای نام تو در تو که با روش قدیمی وجود دارد:
در این حالت جدید پشتیبانی نمیشود.
namespace mvc.Controllers; public class HomeController : Controller { }
بررسی مفهوم «File-Scoped Namespaces»
یکی از اهداف مهم C# 10.0، کاهش نویز موجود در فایلهای cs. است. اگر قرار است صدها بار در فایلهای مختلف برنامه، using System نوشته شود، چرا یکبار آنرا به صورت عمومی تعریف نکنیم و یا اگر در 99 درصد موارد، توسعه دهندهها به ازای یک فایل، تنها یک فضای نام را تعریف میکنند، چرا باید یک فضای اضافی خالی، برای تعریف آن اختصاص داده شود و تمام فایلها به همراه یک «tab فاصلهی» اضافی مختص به این فضای نام باشند؟
تعریف فعلی فضاهای نام در #C به صورت زیر است:
namespace MyNamespace { public class MyClass { public void MyMethod() { //...Method implementation } } }
در C# 10.0، میتوان این تعریف را ساده کرد؛ از آنجائیکه به ندرت چند فضای نام در یک تک فایل تعریف میشوند، میتوان تعریف فضای نام را در یک سطر، در ابتدای فایل ذکر کرد، تا به صورت خودکار به کل فایل و اشیاء موجود در آن اعمال شود:
namespace MyNamespace; public class MyClass { public void MyMethod() { //...Method implementation } }
محدویتهای «File-Scoped Namespaces»
- بدیهی است در این حالت دیگر نمیتوان چندین فضای نام را همانند قبل در یک فایل cs. تعریف کرد:
namespace Name1 { public class Class1 { } } namespace Name1.Name2 { public class Class2 { } }
- همچنین امکان ترکیب روش قبلی تعریف فضاهای نام، با روش جدید، در یک فایل وجود ندارد.
- به علاوه امکان تعریف فضاهای نام تو در تو که با روش قدیمی وجود دارد:
namespace Name1 { public class Class1 { } namespace Name1.Name2 { public class Class2 { } } }
یکی دیگر از روشهایی که جهت بهبود کیفیت کدها مورد استفاده قرار میگیرد، «طراحی با قراردادها» است؛ به این معنا که «بهتر است» متدهای تعریف شده پیش از استفاده از آرگومانهای خود، آنها را دقیقا بررسی کنند و به این نوع پیش شرطها، قرارداد هم گفته میشود.
نمونهای از آنرا در قسمت 9 مشاهده کردید که در آن اگر آرگومانهای متد AddRole، خالی یا نال باشند، یک استثناء صادر میشود. این نوع پیغامهای واضح و دقیق در مورد عدم اعتبار ورودیهای دریافتی، بهتر است از پیغامهای کلی و نامفهوم null reference exception که بدون بررسی stack trace و سایر ملاحظات، علت بروز آنها مشخص نمیشوند.
در دات نت 4، جهت سهولت این نوع بررسیها، مفهوم Code Contracts ارائه شده است. (این نام هم از این جهت بکارگرفته شده که Design by Contract نام تجاری شرکت ثبت شدهای در آمریکا است!)
یک مثال:
متد زیر را در نظر بگیرید. اگر divisor مساوی صفر باشد، استثنای کلی DivideByZeroException صادر میشود:
namespace Refactoring.Day10.DesignByContract.Before
{
public class MathMehods
{
public double Divide(int dividend, int divisor)
{
return dividend / divisor;
}
}
}
روش متداول «طراحی با قراردادها» جهت بهبود کیفیت کد فوق پیش از دات نت 4 به صورت زیر است:
using System;
namespace Refactoring.Day10.DesignByContract.After
{
public class MathMehods
{
public double Divide(int dividend, int divisor)
{
if (divisor == 0)
throw new ArgumentException("divisor cannot be zero", "divisor");
return dividend / divisor;
}
}
}
در اینجا پس از بررسی آرگومان divisor، قرارداد خود را به آن اعمال خواهیم کرد. همچنین در استثنای تعریف شده، پیغام واضحتری به همراه نام آرگومان مورد نظر، ذکر شده است که از هر لحاظ نسبت به استثنای استاندارد و کلی DivideByZeroException مفهومتر است.
در دات نت 4 ، به کمک امکانات مهیای در فضای نام System.Diagnostics.Contracts، این نوع بررسیها نام و امکانات درخور خود را یافتهاند:
using System.Diagnostics.Contracts;
namespace Refactoring.Day10.DesignByContract.After
{
public class MathMehods
{
public double Divide(int dividend, int divisor)
{
Contract.Requires(divisor != 0, "divisor cannot be zero");
return dividend / divisor;
}
}
}
البته اگر قطعه کد فوق را به همراه divisor=0 اجرا کنید، هیچ پیغام خاصی را مشاهده نخواهید کرد؛ از این لحاظ که نیاز است تا فایلهای مرتبط با آنرا از این آدرس دریافت و نصب کنید. این کتابخانه با VS2008 و VS2010 سازگار است. پس از آن، برگهی Code contracts به عنوان یکی از برگههای خواص پروژه در دسترس خواهد بود و به کمک آن میتوان مشخص کرد که برنامه حین رسیدن به این نوع بررسیها چه عکس العملی را باید بروز دهد.
برای مطالعه بیشتر:
مسیرراهها
Entity framework code-first
شروع به کار با EF Code first
برای تکمیل بحث نیاز است تغییرات انجام شده از نگارش 4 به 6 را نیز مد نظر داشته باشید:
آشنایی با مباحث Migrations
آشنایی با تنظیمات نگاشتها به دو روش استفاده از ویژگیها و Fluent API
اعتبارسنجی و بررسی استثناءها
ردیابی تغییرات
استفاده از SQL خام و بانکهای اطلاعاتی متفاوت
نکات مهم کوئری نویسی در EF
استفاده از EF در WPF
لایه بندی پروژههای EF Code first
پروژههای انجام شده با EF Code first
برای تکمیل بحث نیاز است تغییرات انجام شده از نگارش 4 به 6 را نیز مد نظر داشته باشید:
- ارتقاء به Entity framework 6 و استفاده از بانکهای اطلاعاتی غیر از SQL Server
- بروز رسانی استفاده از SqlServer Compact در Entityframework 6.0
آشنایی با مباحث Migrations
- آشنایی با Code first migrations : قسمت اول و دوم
- وادار کردن EF Code first به ساخت بانک اطلاعاتی پیش از شروع به کار برنامه
- بررسی خطاهای متداول عملیات Migration در حین به روز رسانی پروژههای EF Code First
- بازسازی جدول MigrationHistory با کد نویسی در EF Code first
- استفاده از چندین Context در EF 6 Code first
آشنایی با تنظیمات نگاشتها به دو روش استفاده از ویژگیها و Fluent API
- امکانات مهیا جهت تنظیم ویژگیهای مدلهای برنامه در EF Code first
- تعیین شمای جداول بانکی در EF Code First
- بررسی تعاریف نگاشتها به کمک متادیتا در EF Code first
- بررسی Fluent API جهت تعریف نگاشت کلاسها به بانک اطلاعاتی
- مدیریت روابط بین جداول در EF Code first به کمک Fluent API
- ادامه بحث بررسی جزئیات نحوه نگاشت کلاسها به جداول، توسط EF Code firs
- SQL Server CE و ثبت متون طولانی در EF Code first
- بازنویسی سادهتر پیش فرضهای EF Code first در نگارش 6 آن
- مباحث تکمیلی مدلهای خود ارجاع دهنده در EF Code first
- بررسی تفصیلی رابطه Many-to-Many در EF Code first
- تقسیم جدول در Entity Framework Code First
- کار با کلیدهای اصلی و خارجی در EF Code first
- تنظیمات ارث بری کلاسها در EF Code first
- ایندکس منحصر به فرد با استفاده از Data Annotation در EF Code First
- ایجاد ایندکس منحصربفرد در EF Code first به صورت Fluent API
- افزودن خودکار کلاسهای تنظیمات نگاشتها در EF Code first
- خودکار کردن تعاریف DbSetها در EF Code first
- بررسی خطای cycles or multiple cascade paths و یا cyclical reference در EF Code first
- بالا بردن سرعت بارگذاری اولیه EF Code first با تعداد مدلهای زیاد
اعتبارسنجی و بررسی استثناءها
- Entity Framework و InnerException
- استثناهایی که باید حین استفاده از EF Code first بررسی شوند
- اعتبارسنجی در Entity framework Code first
ردیابی تغییرات
- ردیابی تغییرات در EF Code first
- ردیابی تغییرات در Entity Framework، بخش اول
- چند نکته کاربردی درباره Entity Framework
- نمایش خروجی SQL کدهای Entity framework 6 در کنسول دیباگ ویژوال استودیو
- بالا بردن سرعت DbContext هنگام ثبت دادههای زیاد
- استفاده از EF در اپلیکیشنهای N-Tier : قسمت اول
- نحوه کاهش مصرف حافظه EF Code first حین گزارشگیری از اطلاعات
استفاده از SQL خام و بانکهای اطلاعاتی متفاوت
نکات مهم کوئری نویسی در EF
- نوشتن پرس و جو در Entity Framework با استفاده از LINQ To Entity ، قسمت اول، دوم و سوم
- تفاوت بین IQueryable و IEnumerable در حین کار با ORMs
- Lazy loading و eager loading
- روشهایی برای بهبود سرعت برنامههای مبتنی بر Entity framework
- نکاتی در مورد استفاده از توابع تجمعی در Entity framework
- کوئریهایی با قابلیت استفادهی مجدد
- شبیه سازی outer Join در entity framework
- اعمال توابع تجمعی بر روی چند ستون در Entity framework
- متد LastOrDefault در EF
- Func یا Expression Func در EF
- درج یک باره چندین رکورد بصورت همزمان هنگام استفاده از ORMها
- استفاده از خواص راهبری در Entity framework بجای Join نویسی
- مرتب سازی رکوردها به صورت اتفاقی در Entity framework
- استفاده از خواص راهبری در EF Code first جهت ساده سازی کوئریها
- یافتن مقادیر نال در Entity framework
- واکشی اطلاعات به صورت chunk chunk (تکه تکه) و نمایش در ListView
- امنیت در LINQ to SQL
- کارهایی جهت بالابردن کارآیی - قسمت اول
- کارهایی جهت بالابردن کارآیی - قسمت دوم
- کارهایی جهت بالابردن کارآیی - قسمت سوم
استفاده از EF در WPF
لایه بندی پروژههای EF Code first
- پیشنیاز: مطالعه کامل دوره تزریق وابستگیها
- استفاده از الگوی Repository اضافی در EF Code first؛ آری یا خیر؟!
- Repositoryها روی UnitOfWork ایده خوبی نیستند
- پیاده سازی الگوی Context Per Request در برنامههای مبتنی بر EF Code first
- پیاده سازی UnitOfWork به وسیله MEF
پروژههای انجام شده با EF Code first
نظرات مطالب
EF Code First #12
با سلام، من کد شما را به صورت زیر تغییر دادم
ابتدا یک اینترفیس به صورت زیر ایجاد کردم
و تمام اینترفیسهای دیگر از این به صورت زیر به ارث برده شده اند
و در قسمت Servise یک کلاس ایجاد کردم که اینترفیس IGenericService را پیاده سازی میکند که کدهای آن به صورت زیر است
و بقیه کلاسها از کلاس بالا به ارث میبرند.
حال سوال من اینه که این پیاده سازی از لحاظ پیاده سازی مشکلی ندارد؟ و میتوانم در پروژه هام از این روش استفاده کنم یا خیر؟
ممنونم
ابتدا یک اینترفیس به صورت زیر ایجاد کردم
namespace Service.Interfaces { public interface IGenericService<T> { void AddOrUpdate(T entity); void Delete(T entity); T Find(Func<T, bool> predicate); T GetLast(Func<T, bool> predicate); IList<T> GetAll(); IList<T> GetAll(Func<T, bool> predicate); IList<T> GetAll(Expression<Func<T, object>> orderby); IList<T> GetAll(Func<T, bool> predicate, Expression<Func<T, object>> orderby); Task<List<T>> GetAllAsync(); Task<List<T>> GetAllAsync(Func<T, bool> predicate); Task<List<T>> GetAllAsync(Expression<Func<T, object>> orderby); Task<List<T>> GetAllAsync(Func<T, bool> predicate, Expression<Func<T, object>> orderby); int Count(); int Count(Func<T, bool> predicate); } }
و تمام اینترفیسهای دیگر از این به صورت زیر به ارث برده شده اند
public interface IBookGroupService:IGenericService<BookGroup> { }
و در قسمت Servise یک کلاس ایجاد کردم که اینترفیس IGenericService را پیاده سازی میکند که کدهای آن به صورت زیر است
public class EFGenericService<TEntity> : IGenericService<TEntity> where TEntity : class { protected IUnitOfWork _uow; protected IDbSet<TEntity> _tEntities; public EFGenericService(IUnitOfWork uow) { _uow = uow; _tEntities = _uow.Set<TEntity>(); } public void AddOrUpdate(TEntity entity) { _tEntities.AddOrUpdate(entity); } public virtual void Delete(TEntity entity) { _tEntities.Remove(entity); } public virtual TEntity Find(Func<TEntity, bool> predicate) { return _tEntities.Where(predicate).FirstOrDefault(); } public virtual TEntity GetLast(Func<TEntity, bool> predicate) { return _tEntities.Where(predicate).Last(); } public virtual IList<TEntity> GetAll() { return _tEntities.ToList(); } public virtual IList<TEntity> GetAll(Func<TEntity, bool> predicate) { return _tEntities.Where(predicate).ToList(); } public virtual IList<TEntity> GetAll(Expression<Func<TEntity, object>> @orderby) { return _tEntities.OrderBy(@orderby).ToList(); } public virtual IList<TEntity> GetAll(Func<TEntity, bool> predicate, Expression<Func<TEntity, object>> @orderby) { return _tEntities.OrderBy(@orderby).Where(predicate).ToList(); } public async Task<List<TEntity>> GetAllAsync() { return await Task.Run(() => _tEntities.ToList()); } public async Task<List<TEntity>> GetAllAsync(Func<TEntity, bool> predicate) { return await Task.Run(() => _tEntities.Where(predicate).ToList()); } public async Task<List<TEntity>> GetAllAsync(Expression<Func<TEntity, object>> @orderby) { return await Task.Run(() => _tEntities.OrderBy(@orderby).ToList()); } public async Task<List<TEntity>> GetAllAsync(Func<TEntity, bool> predicate, Expression<Func<TEntity, object>> @orderby) { return await Task.Run(()=> _tEntities.OrderBy(@orderby).Where(predicate).ToList()); } public virtual int Count() { return _tEntities.Count(); } public virtual int Count(Func<TEntity, bool> predicate) { return _tEntities.Count(predicate); } }
و بقیه کلاسها از کلاس بالا به ارث میبرند.
public class EFBorrowService:EFGenericService<Borrow>,IBorrowService { public EFBorrowService(IUnitOfWork uow) : base(uow) { } }
حال سوال من اینه که این پیاده سازی از لحاظ پیاده سازی مشکلی ندارد؟ و میتوانم در پروژه هام از این روش استفاده کنم یا خیر؟
ممنونم
مشکل:
و فراخوانی این فایل را در انتهای قسمت body فایل index.html یا Host.cshtml_ بصورت زیر قرار میدهیم:
و حالا که پروژه را اجرا کنید، رویداد کلیک بر روی دکمهی toggle کار نمیکند!
در مثال بالا من دستورات را داخل یک تابع به نام initilizeNiceAdminJs قرار دادم. سپس در فایل index.razor این تابع را در رویداد OnAfterRenderAsync فراخوانی مینماییم:
ممکن است بخواهید در برنامههای Blazor از یک قطعه کد آماده استفاده نمایید که در آن از دستورات Javascript استفاده شده باشد و تعدادی رویداد برای المانهای صفحه تعریف کرده باشند؛ به عنوان مثال من از قالب آماده Nice Admin استفاده میکنم که در آن برای تمام قالب، از یک فایل به نام main.js استفاده شدهاست و در آن برای مخفی و ظاهر نمودن منو، از یک دکمه toggle استفاده کردهاست. برای این عملیات، یک رویداد کلیک در این فایل تعریف شده:
/** * Template Name: NiceAdmin - v2.1.0 * Template URL: https://bootstrapmade.com/nice-admin-bootstrap-admin-html-template/ * Author: BootstrapMade.com * License: https://bootstrapmade.com/license/ */ (function() { "use strict"; /** * Easy selector helper function */ const select = (el, all = false) => { el = el.trim() if (all) { return [...document.querySelectorAll(el)] } else { return document.querySelector(el) } } /** * Easy event listener function */ const on = (type, el, listener, all = false) => { if (all) { select(el, all).forEach(e => e.addEventListener(type, listener)) } else { select(el, all).addEventListener(type, listener) } } /** * Easy on scroll event listener */ const onscroll = (el, listener) => { el.addEventListener('scroll', listener) } /** * Sidebar toggle */ if (select('.toggle-sidebar-btn')) { on('click', '.toggle-sidebar-btn', function(e) { select('body').classList.toggle('toggle-sidebar') }) } /** * Search bar toggle */ if (select('.search-bar-toggle')) { on('click', '.search-bar-toggle', function(e) { select('.search-bar').classList.toggle('search-bar-show') }) } . . . })();
<!DOCTYPE html> <html dir="rtl"> . . . <body> <div id="app">Loading...</div> <div id="blazor-error-ui"> An unhandled error has occurred. <a href="">Reload</a> <a>🗙</a> </div> <script src="_framework/blazor.webassembly.js"></script> <!-- Vendor JS Files --> <script src="assets/vendor/bootstrap/js/bootstrap.bundle.js"></script> <script src="assets/vendor/php-email-form/validate.js"></script> <script src="assets/vendor/quill/quill.min.js"></script> <script src="assets/vendor/tinymce/tinymce.min.js"></script> <script src="assets/vendor/simple-datatables/simple-datatables.js"></script> <script src="assets/vendor/chart.js/chart.min.js"></script> <script src="assets/vendor/apexcharts/apexcharts.min.js"></script> <script src="assets/vendor/echarts/echarts.min.js"></script> <!-- Template Main JS File --> <script src="assets/js/main.js"></script> </body> </html>
دلیل:
مشکل به این دلیل میباشد که کدهای جاوا اسکریپتی، بلافاصله با دانلود فایل اجرا میشوند؛ در حالیکه بارگذاری صفحه هنوز توسط blazor به اتمام نرسیدهاست. در نتیجه المانهایی که در این فایل به آنها اشاره شدهاست، هنوز قابل دسترسی نیستند و رویدادهای تعریف شدهی برای آنها، اجرا نمیشوند.
راه حل:
باید اجرای کدهای جاوا اسکریپتی را تا بارگذاری کامل صفحه به تعویق بیاندازیم. برای همین منظور ابتدا کدهای تعریف شدهی در فایل main.js را بجای اینکه مستقیما اجرا شوند، در یک تابع قرار میدهیم:
/** * Template Name: NiceAdmin - v2.1.0 * Template URL: https://bootstrapmade.com/nice-admin-bootstrap-admin-html-template/ * Author: BootstrapMade.com * License: https://bootstrapmade.com/license/ */ function initilizeNiceAdminJs() { "use strict"; /** * Easy selector helper function */ const select = (el, all = false) => { el = el.trim() if (all) { return [...document.querySelectorAll(el)] } else { return document.querySelector(el) } } /** * Easy event listener function */ const on = (type, el, listener, all = false) => { if (all) { select(el, all).forEach(e => e.addEventListener(type, listener)) } else { select(el, all).addEventListener(type, listener) } } /** * Easy on scroll event listener */ const onscroll = (el, listener) => { el.addEventListener('scroll', listener) } /** * Sidebar toggle */ if (select('.toggle-sidebar-btn')) { on('click', '.toggle-sidebar-btn', function(e) { select('body').classList.toggle('toggle-sidebar') }) } /** * Search bar toggle */ if (select('.search-bar-toggle')) { on('click', '.search-bar-toggle', function(e) { select('.search-bar').classList.toggle('search-bar-show') }) } . . . }
@page "/" @inject IJSRuntime JSRuntime <div> this is index page </div> @code { protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await JSRuntime.InvokeVoidAsync("initilizeNiceAdminJs");// Initialize main.js after site completely loaded } } }
بازخوردهای دوره
تزریق وابستگیها
تزریق وابستگی رو تا چه سطحی باید انجام داد؟ یعنی رعایت کردن اون تو تمام سطوح نرم افزار باید انجام بشه؟ برای مثال کلاس زیر رو در نظر بگیرید که در لایه Entity وجود داره
آیا با اینکه کلاس پدر و فرزند در یک لایه مشترک هستند ، در اینجا ارزش داره که تزریق وابستگی رو انجام بدیم ؟حجم کد و کار بالا بالا نمیره؟ یا اینکه پیچیدگی زیاد نمیشه؟
class Parent { public IChild child {get;set;} public Parent(Ichild child) { this.child =child; } }
نظرات مطالب
Soft Delete در Entity Framework 6
برای مطلب فوق 2 متد به شکل زیر پیاده سازی کردم
و در Context هم برای تعریف فیلتر یه صورت زیر عمل کردم
public void SetEnableActiveFilter(bool isActive) { this.EnableFilter("IsActive").SetParameter("isActive", isActive); } public void SetDisableActiveFilter() { this.DisableFilter("IsActive"); }
modelBuilder.Conventions.Add(FilterConvention.Create<BaseEntity, bool>("IsActive", (entity, isActive) => entity.IsActive == isActive));
مشکل اینجاست که فعال سازی به صورت کامل انجام میشود اما غیر فعال سازی آن انجام نمیگیرد و در تمام فرایندهای بعدی نیز وجود دارد.
مشکل از نحوه تعریف هست؟
در ادامه سری مقالات مرتبط با برنامه نویسی تابعی ، قصد دارم به استفاده کردن یا نکردن از نوعهای داده اولیه (Primitive Types) را بررسی کنیم. پیشنهاد میکنم در صورتی که قسمتهای قبلی را مطالعه نکرده اید ابتدا قسمتهای قبل را بخوانید.
در طراحی مدل دامین، بیشتر مواقع از نوعهای اولیه مانند int , string,… استفاده میکنیم و به عبارتی میتوانیم بگوییم در استفاده از این نوع داده وسواس داریم. قطعه کد زیر را در نظر بگیرید:
کلاس UserFactory، یک متد به نام CreateUser دارد که یک رشته را به عنوان ورودی میگیرد و یک شیء از کلاس User را بر میگرداند. خوب مشکل این متد کجاست؟
اگر به خاطر داشته باشید، در قسمتهای قبلی در مورد مفهومی به نام Honesty صحبت کردیم. به طور ساده باید بتوانیم از روی امضای تابع، کاری را که تابع انجام میدهد و خروجی آن را ببینیم. این تابع Honest نیست؛ شرایطی که string میتواند درست نباشد، خالی باشد، طول غیر مجاز داشته باشد و ... را نمیتوانیم از امضای تابع حدس بزنیم.
برای روشنتر شدن بحث، مثال بالا را همیشه در ذهن خود داشته باشید. در این مثال، در تابع Divide که عمل تقسیم را انجام میدهد، پارامتر y که یک عدد از نوع int است، میتواند مقدار صفر را داشته باشد و باعث یک exception شود.و از آنجائیکه نوع خروجی این متد هم int است، انتظار دریافت یک exception را نداریم. در مورد exceptionها به طول مفصل در قسمت قبلی صحبت کردیم. در مثال بالا تصور کنید که بجای یک ایمیل، از چند ایمیل به عنوان ورودی میخواهید استفاده کنید. آیا منطق Validation را به ازای هر پارامتر ورودی باید تکرار کنید؟
به طور کلی استفادهی نابجا و بیش از حد از نوعهای دادهی اولیه، باعث میشود تا Honesty متدها را از دست بدهیم و قاعدهی DRY را نقض کنیم.
به جای نوعهای اولیه از چی استفاده کنیم؟
جواب خیلی سادهاست؛ شما نیاز دارید تا یک Type اختصاصی را ایجاد کنید. برای مثال بجای استفاده از نوع string برای یک ایمیل، میتوانید یک کلاس را به عنوان Email ایجاد کنید که مشخصهای به نام Value دارد. این کار به روشهای مختلفی قابل انجام است؛ اما پیشنهاد من استفاده از این روش هست:
در این روش، یک کلاس را به عنوان Value Object ایجاد کردهایم. این کلاس، نوع اولیهای را که با آن سر و کار داریم، در بر خواهد گرفت و منطق مربوط به مقایسه، همچنین عملگرهای == و != را هم از طریق Equals و GetHashCode، پیاده سازی کرده. برای مثال جهت کلاس ایمیل میتوانیم به صورت زیر عمل کنیم:
همچنین برای مقدار دهی این کلاس میتوانید به صورت زیر عمل کنید:
برای مثالهای پیچیدهتر مانند آدرس، که شامل آدرس، کد پستی و … میباشد، میتوانید با استفاده از امکان Tupleها که از سی شارپ 7 به بعد معرفی شده، مانند مثال زیر عمل کنید:
و در نهایت برای نوشتن منطق مربوط به validation میتوانید متد Validate را Override کنید و قاعدهی DRY را هم نقض نکنید.
روش معرفی شدهی در این مقاله، صرفا جهت آشنایی بیشتر شما و داشتن کدی تمیزتر از طریق مفاهیم برنامه نویسی تابعی خواهد بود. در دنیای واقعی، احتمالا مسائلی را برای ذخیره سازی این آبجکتها و یا کار با کتابخانههایی مانند Entity Framework خواهید داشت که به سادگی قابل حل است.
در صورتیکه مشکلی در پیاده سازی داشتید، میتوانید مشکل خود را زیر همین مطلب و یا بر روی gist آن کامنت کنید.
در طراحی مدل دامین، بیشتر مواقع از نوعهای اولیه مانند int , string,… استفاده میکنیم و به عبارتی میتوانیم بگوییم در استفاده از این نوع داده وسواس داریم. قطعه کد زیر را در نظر بگیرید:
public class UserFactory { public User CreateUser(string email) { return new User(email); } }
اگر به خاطر داشته باشید، در قسمتهای قبلی در مورد مفهومی به نام Honesty صحبت کردیم. به طور ساده باید بتوانیم از روی امضای تابع، کاری را که تابع انجام میدهد و خروجی آن را ببینیم. این تابع Honest نیست؛ شرایطی که string میتواند درست نباشد، خالی باشد، طول غیر مجاز داشته باشد و ... را نمیتوانیم از امضای تابع حدس بزنیم.
صحبت در مورد استفاده کردن یا نکردن، جنبههای زیادی دارد و یکی از مواردی است که در معماری DDD تحت عنوان Value Object به آن پرداخته شده. هدف ما در این قسمت از مقاله، صرفا پرداختن به گوشهای از این مورد هست. ولی شما میتوانید برای مطالعه بیشتر و اطلاعات تکمیلی کتاب Domain-Driven Design: Tackling Complexity in the Heart of Software نوشته Eric Evans را مطالعه کنید.
جواب خیلی سادهاست؛ شما نیاز دارید تا یک Type اختصاصی را ایجاد کنید. برای مثال بجای استفاده از نوع string برای یک ایمیل، میتوانید یک کلاس را به عنوان Email ایجاد کنید که مشخصهای به نام Value دارد. این کار به روشهای مختلفی قابل انجام است؛ اما پیشنهاد من استفاده از این روش هست:
using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; namespace ValueOf { public class ValueOf<TValue, TThis> where TThis : ValueOf<TValue, TThis>, new() { private static readonly Func<TThis> Factory; /// <summary> /// WARNING - THIS FEATURE IS EXPERIMENTAL. I may change it to do /// validation in a different way. /// Right now, override this method, and throw any exceptions you need to. /// Access this.Value to check the value /// </summary> protected virtual void Validate() { } static ValueOf() { ConstructorInfo ctor = typeof(TThis) .GetTypeInfo() .DeclaredConstructors .First(); var argsExp = new Expression[0]; NewExpression newExp = Expression.New(ctor, argsExp); LambdaExpression lambda = Expression.Lambda(typeof(Func<TThis>), newExp); Factory = (Func<TThis>)lambda.Compile(); } public TValue Value { get; protected set; } public static TThis From(TValue item) { TThis x = Factory(); x.Value = item; x.Validate(); return x; } protected virtual bool Equals(ValueOf<TValue, TThis> other) { return EqualityComparer<TValue>.Default.Equals(Value, other.Value); } public override bool Equals(object obj) { if (obj is null) return false; if (ReferenceEquals(this, obj)) return true; return obj.GetType() == GetType() && Equals((ValueOf<TValue, TThis>)obj); } public override int GetHashCode() { return EqualityComparer<TValue>.Default.GetHashCode(Value); } public static bool operator ==(ValueOf<TValue, TThis> a, ValueOf<TValue, TThis> b) { if (a is null && b is null) return true; if (a is null || b is null) return false; return a.Equals(b); } public static bool operator !=(ValueOf<TValue, TThis> a, ValueOf<TValue, TThis> b) { return !(a == b); } public override string ToString() { return Value.ToString(); } } }
public class EmailAddress : ValueOf<string, EmailAddress> { }
EmailAddress emailAddress = EmailAddress.From("foo@bar.com");
public class Address : ValueOf<(string firstLine, string secondLine, Postcode postcode), Address> { }
روش معرفی شدهی در این مقاله، صرفا جهت آشنایی بیشتر شما و داشتن کدی تمیزتر از طریق مفاهیم برنامه نویسی تابعی خواهد بود. در دنیای واقعی، احتمالا مسائلی را برای ذخیره سازی این آبجکتها و یا کار با کتابخانههایی مانند Entity Framework خواهید داشت که به سادگی قابل حل است.
در صورتیکه مشکلی در پیاده سازی داشتید، میتوانید مشکل خود را زیر همین مطلب و یا بر روی gist آن کامنت کنید.