اشتراکها
یکی از دغدغههای جدی امروزه توسعه دهندگان نرم افزار در سمت Front end، توسعه برنامههای Cross Platform است. در این سری آموزشی به صورت قدم به قدم و پروژه محور میخواهیم برنامهای را برای Android/iOS/Windows توسعه دهیم که روی کامپیوتر، تبلت و موبایل به خوبی کار کند.
انتخاب ابزار درست برای شروع به کار از اهمیت شایانی برخوردار است و بد نیست در ابتدا به بررسی دلایل انتخاب ابزارهایی بپردازیم که قرار است در این دوره از آنها استفاده شود.
۱- زبان برنامه نویسی: CSharp
CSharp با وجود امکاناتی مانند Generics، Lambda Expressions، Linq، Async و ... که تا حدودی در سایر زبانها هم هستند، زبانی خوش ساختار و کاربردی است. همچنین اضافه شدن امکانات جدیدی مانند ref returns و ... نشان دهنده این است که این زبان رو به جلو در حرکت و در برخی موارد پیشرو است. اما در توسعه یک برنامه Cross Platform مواردی اهمیت پیدا میکنند که شاید توسعه دهنده نرم افزار مستقیما با آنها درگیر نشود، ولی از آنها تاثیر میپذیرد. در زبان CSharp مواردی مانند P/Invoke ،Pointers، Extern و ... جزء این دست از موارد هستند که کمک میکنند CSharp به یکی از لذت بخشترین زبان هایی تبدیل شود که قابلیت فراخوانی 100% امکانات زبانهای دیگر را بدون اما و اگرهای فراوان داشته باشد.
در سایر زبانهای Cross Platform اگر کتابخانههای توسعه داده شده و ترکیب زبانهای برنامه نویسی استفاده شده در آنها را بررسی کنید، میبینید که اگر قرار است کتابخانه مربوطه مثلا در JavaScript استفاده شود، توسعه دهنده کد، درصدی از کد را با Java، درصدی را با Swift و درصدی را با JavaScript توسعه داده است! اگر معادل همان کتابخانه را برای CSharp پیدا کنید، میبینید که تمامی قسمتهای مربوط به اندروید، iOS و ویندوز به زبان CSharp است.
برای مثال در ادامه کدهای مربوط به پروژهای را میبینید که هدف آن، ارائه متدهایی ساده برای کار با امکانات مختلف دستگاه، به صورت Cross Platform هست. مثلا برای بررسی وضعیت باطری بنویسید:
var state = Battery.State; // Charging, Full, Discharging, ...
که تماما با CSharp توسعه داده شده است.
اما معادل چنین پروژهای در هیچ زبان دیگری به صورت 100% با خود آن زبان توسعه داده نشدهاست و بیشتر مواقع با چنین چیزی مواجه میشوید:
این مسئله وقتی حائز اهمیت میشود که در پروژهتان به سمت کارهایی حرکت کنید که کمی خاص باشند و نتوانید کتابخانهای را پیدا کنید که نیازهای شما را پوشش دهد و یا از کیفیت خوبی برخوردار نباشد و ... و خلاصه بخواهید کمی بیشتر دست به کد شوید. در چنین مواقعی شما عملا درگیر چندین زبان و محیط توسعه و سیستم عامل و Debugger و ... میشوید. به هر میزان که برنامه شما خاص باشد، این هزینه افزایش پیدا میکند تا جایی که ممکن است ادامه توسعه نرم افزار را غیر ممکن کند.
در CSharp شما به صد در صد امکانات سیستم عاملها (Android/iOS/Windows/Linux/Mac/Tizen) دسترسی دارید.
۲- اجرا کننده برنامه: NET.
انتخاب NET. و کتابخانههای آن مانند Task Parallel Library - Entity Framework(Sqlite) - Noda - JSON.NET که در هر زمینهای بالاترین کیفیت ممکن را به شما ارائه میکنند به خودی خود منطقی به نظر میرسد. اما تمامی اینها در کنار سرعت اجرای NET. به صورت Native و همچنین قابلیت اجرای NET. در تمامی سیستم عاملها و همچنین امکان اجرای آن در مرورگر به کمک استاندارد Web Assembly آن را به انتخابی فوق العاده بدل میکند. سرعت گسترش محبوبیت و استفاده از NET. در دنیا نیز دلیل دیگری است برای اطمینان خاطر از انتخاب درست.
۳- Xamarin forms
Xamarin forms همه آن چیزهای پایهای است که برای نوشتن یک برنامه لازم داریم. کنترل هایی مانند ListView، Button و ...به همراه Binding - Navigation و ...
در عمل میتوانید آن را معادل Angular & Angular Material بدانید. وقتی شما فرمی را با Xamarin Forms توسعه میدهید و درون آن دکمهای است که از فرم اول، شما را به فرم دوم میبرد، میتوانید آن را در هر جایی که Xamarin forms پشتیبانی میکند، استفاده کنید. پشتیبانی Xamarin forms برای Android/iOS/Windows خوب و برای Linux/Mac/Tizen و Web در مراحل اولیه است.
در Xamarin forms شما UI کاملا Native خواهید داشت.
۴- Prism Patterns & practices
Prism همه آن چیزی است که برای نوشتن یک برنامه با کیفیت، با قابلیت نگهداری بالا و تست پذیر احتیاج داریم.
با نقش Bit و کمکهای آن در طول مسیر آموزش بیشتر آشنا خواهیم شد.
در قسمتهای بعدی به آموزش نصب و نحوه دیباگ کردن کد و ارائه پابلیش در Android-iOS-Windows خواهیم پرداخت و سپس وارد کدنویسی شده و پروژه اولیه را خواهیم ساخت و در قسمتهای بعد از آن هم کار با دیتابیس کلاینت ساید، ارتباط با سرور و ... را آموزش میبینیم.
اگر قبلا Xamarin Forms را تست کردهاید و به علت مسائلی مانند حجم بالای خروجی برنامه و یا کندی در توسعه برنامه یا اجرای آن در دستگاه مشتری آن را کنار گذاشتهاید، توصیه میکنم بار دیگر آن را با ما تست کنید و با رعایت چند نکته ساده از نوشتن برنامه Cross Platform به بهترین شکل لذت ببرید و خروجی خوبی را در نهایت به مشتریان سیستم ارائه کنید.
مطالب
مروری بر Claim
تعریف :
در این پست قصد دارم در مورد claim که از آن به عنوان یک Abstraction برای شناسایی نام برده شده ، صحبت کنم و گریزی با ارتباط آن با شیرپوینت بزنم . مایکروسافت در جایی Claim را این گونه تعریف کرده بود : یک عبارت که یک شیئ ، آن را در باره خودش یا شیئ دیگری میسازد . Claim یک Abstraction برای شناسایی فراهم میکند . برای مثال میتوان گفت که یک عبارت که شامل نام ، شناسه ، کلید ، گروه بندی ، ظرفیت و ... باشد ، فراهم میکند .
لازم است به تعریف Token هم اشاره ای شود . هنگامی که یک شناسه دیجیتالی در شبکه در حال گذر است ، فقط حاوی مجموعه ای از بایتها است .( ارجاع به مجموعه ای از بایتها که حاوی اطلاعات شناسایی به عنوان یک Token امنیتی با فقط یک Token باشد، امری عادی است ) . در محیطی که بر مبنای Claim بنا شده است ، یک Token حاوی یک یا چند Claim است که هر یک میتواند برخی تکههای اطلاعاتی را برای شناسایی (بیشتر در مورد کاربران و افراد استفاده میشود) ، در خود جای دهد
Claimها تقریبا هر چیزی را در مورد یک کاربر میتواند ارائه دهد. . برای مثال در Token تصویر بالا ، 3 claim اول به اطلاعات نام و نقش و سن کاربر اشاره دارند .
فراهم کننده - توزیع کننده :
Claimها توسط یک فراهم کننده (Provider) توزیع میشوند (Issuer) و سپس به آنها یک یا چند مقدار ، اختصاص مییابد و در Security Token هایی که توسط یک توزیع کننده ، توزیع میشوند ، بسته بندی میشود و معمولا به عنوان Security Token Service یا STS شناخته میشوند . برای مشاهده تعریف اصطلاحات مرتبط به Claim به اینجا مراجعه کنید
STS ، میتواند توسط چند Identity Provider - IdP به مالکیت در بیاید . یک فراهم کننده شناسه در STS یا IP-STS ، یک سرویس است که درخواستها را برای اطمینان از شناسایی Claimها مدیریت میکند . یک IP-STS از یک پایگاه داده که Identity Store نامیده میشود برای نگهداری و مدیریت شناسهها و خصیصههای مرتبط با آنها استفاده میکند .Identity Store میتواند یک دیتا بیس معمولی مانند SQL Server باشد یا یک محیط پیچیدهتر مانند Active Directory . (از قبیل Active Directory Domain Services یا Active Directory Lightweight Directory Service ) .
قلمرو - Realm
بیانگر مجموعه ای از برنامهها ، URLها ، دامنهها یا سایت هایی میباشد که برای Token ، معتبر باشد .معمولا یک Realm با استفاده از دامنه (microsoft.com) یا مسیری داخل دامنه (microsoft.com/practices/guides) تعریف میشود .بعضی وقتها یک realm ، به عنوان Security Domain بیان میشود چرا که تمام برنامههای داخل یک مرز امنیتی ویژه ای را احاطه کرده است .
Identity Federation
Identity Federation در حقیقت دریافت کننده Token هایی است که در خارج از Realm شما ایجاد شده اند و در صورتی Token را میپذیرد که شما Issuer یا توزیع کننده را مورد اطمینان معرفی کرده باشد . این امر به کاربران اجازه میدهد تا بدون نیاز به ورود به realm تعریف شده خودشان ، از realm دیگری وارد برنامه شوند . کاربران با یک بار ورود به محیط برنامه ، به چندین realm دسترسی پیدا خواهند کرد .
Relying party application
هر برنامه سمت client که از Claim پشتیبانی کند
مزایای Claim
- جدا سازی برنامه از جزییات شناسایی
- انعطاف پذیری در احراز هویت
- Single sign-on
- عدم نیاز به VPN
- متحد کردن مجموعه با دیگر شرکت ها
- متحد کردن مجموعه با سرویسهای غیر از AD
عناصر Claim
Claim شامل عناصر زیر میباشد :
- Token
- Claim
- Provider/Issuer
- Sharepoint STS
- ADFS
- ACS
- OID
- ,و غیره
توزیع کنندهی ADFS
پرنکلها و Tokenهای Claim
شاید این بخش ، یکی از سردرگم کنندهترین مفاهیم باشد . هنگامی که صحبت از Claim میشود ، عده ای دچار این عدم توجه صحیح میشوند که هر دو نوع مختلفی از Tokenها که با Claimها استفاده میشوند ، توسط تمام برنامهها پشتیبانی نمیشوند . نکته قابل توجه نوع پروتکلی است که میخواهید از آن استفاده کنید و باید کامل از آن مطلع باشید .
Security Token هایی که در اینترنت رفت و آمد میکنند ، معمولا یکی از دو نوع زیر هستند :
- توکنهای Security Assertion Markup Language یا SAML که ساختار XMLی دارند و encode شده اند و داخل ساختارهای دیگر از قبیل پیغامهای HTTP و SOAP جای میگیرند
- Simple Web Token یا SWT که درون هدرهای درخواست یا پاسخ HTTP جای میگیرند .(WS-Federation)
نوع متفاوتی از Token که وابسته به مکانیسم احراز هویت است، ایحاد شده است . برای مثال اگر از Claim با Windows Sign-in استفاده میکنید ، شیرپوینت 2010 ، شیئ UserIdentity را به شیئ ClaimIdentity نبدیل میکند و claim را تقویت کرده و Token حاصله را مدیریت میکند . (این نوع Toaken جزء SAML نمیشود)
تنها راه به گرفتن توکنهای SAML ، استفاده از یک Provider برای SAML است . مانند Windows Live ID یا ADFS . [+ ]
معماری برنامههای مبتنی بر Claim
نام مدل : Direct Hub Model
نام مدل : Direct Trust Model
مزایا :
- مدیریت راحتتر برای multiple trust relationships دز ADFS نسبت به Sharepoint
- مدیریت سادهتر در single trust relationship در شیرپوینت و عدم نیاز به فراهم کنندههای سفارشی سازی شده برای Claim
- قایلیت استفاده از ویژگیهای ADFS برای پیگیری توزیع Token ها
- ADFS از هز دوی SAML و WS-Federation پشتیبانی میکند
- توزیع کننده ADFS اجازه میدهد تا خصیصههای LDAP را از AD استخراج کنید
- ADFS به شما اجازه استفاده از قواعد دستوری SQL را برای استخراج دادهها از دیگر پایگاههای داده میدهد
- کارایی و اجرای مناسب
معایب :
- کند بودن
- عدم پشتیبانی از SAML-P
- نیازمند تعریف کاربرها در AD یا نواحی مورد اطمینان
در جهت تکمیل بحث بارگذاری اطلاعات وابسته: اضافه شدن Lazy Loading به نگارش 2.1
برخلاف نگارشهای پیشین EF، اینبار Lazy loading به صورت پیشفرض فعال نیست که در بسیاری از موارد یک مزیت مهم، در جهت بهبود کارآیی برنامه به حساب میآید؛ چون پیشتر مدام میبایستی توسط ابزارهای profiler، برنامه را بررسی میکردیم تا از وجود مشکلی به نام select n+1 مطلع میشدیم (lazy loading اشتباه، در جائی که نیازی به آن نبوده و رفت و برگشت بیش از اندازهای را به بانک اطلاعاتی سبب شدهاست).
در این حالت ابتدا نیاز است بستهی نیوگت Microsoft.EntityFrameworkCore.Proxies را نصب کنید. سپس در متد OnConfiguring مربوط به Context برنامه، متد UseLazyLoadingProxies را فراخوانی نمائید:
و یا اینکار در فایل آغازین برنامه نیز میسر است:
اکنون EF Core 2.1 خواص راهبری (navigation properties) را که قابل بازنویسی باشند (همان مباحث AOP و تشکیل پروکسیها)، lazy load میکند.
این خواص نیز حتما باید به صورت virtual معرفی شوند تا قابلیت بازنویسی را داشته باشند؛ مانند:
در این مثال با فعال بودن lazy loading، به محض لمس خاصیت Blog، اطلاعات مرتبط با آن از بانک اطلاعاتی واکشی خواهند شد و نه پیش از آن مانند eager loading که تمام اطلاعات وابستهی به یک موجودیت را نیز واکشی میکند.
هرچند این قابلیت بارگذاری اطلاعات وابسته در آینده، جذاب به نظر میرسد اما در عمل در حین رندر یک گرید و یا بکارگیری حلقهها، چون سبب رفت و برگشت بیش از اندازهای به بانک اطلاعاتی خواهد شد، باید با دقت مورد استفاده قرار گیرد و اساسا استفادهی از آن در برنامههای وب توصیه نمیشود (با بررسیهای پروژههای بسیاری مشخص شدهاست که این قابلیت ضررش بیشتر از نفعش است).
ب) فعالسازی Lazy loading بدون استفاده از Proxyها
در این حالت نیازی به نصب بستهی AOP جدید تشکیل پروکسیها نیست. در اینجا در کلاس موجودیت خود باید سرویس ILazyLoader را تزریق کنید:
در این روش نیازی به virtual معرفی کردن خواص راهبری نیست. اما در این حالت به علت استفادهی از سرویس ILazyLoader، نیاز خواهید داشت تا بستهی نیوگت Microsoft.EntityFrameworkCore.Abstractions را نیز نصب کنید.
برخلاف نگارشهای پیشین EF، اینبار Lazy loading به صورت پیشفرض فعال نیست که در بسیاری از موارد یک مزیت مهم، در جهت بهبود کارآیی برنامه به حساب میآید؛ چون پیشتر مدام میبایستی توسط ابزارهای profiler، برنامه را بررسی میکردیم تا از وجود مشکلی به نام select n+1 مطلع میشدیم (lazy loading اشتباه، در جائی که نیازی به آن نبوده و رفت و برگشت بیش از اندازهای را به بانک اطلاعاتی سبب شدهاست).
برای فعالسازی lazy loading در EF Core 2.1 (اگر واقعا به آن نیاز دارید البته) دو روش وجود دارد:
الف) فعالسازی Lazy loading توسط Proxyها در این حالت ابتدا نیاز است بستهی نیوگت Microsoft.EntityFrameworkCore.Proxies را نصب کنید. سپس در متد OnConfiguring مربوط به Context برنامه، متد UseLazyLoadingProxies را فراخوانی نمائید:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder .UseLazyLoadingProxies() .UseSqlServer(myConnectionString);
.AddDbContext<BloggingContext>( b => b.UseLazyLoadingProxies() .UseSqlServer(myConnectionString));
این خواص نیز حتما باید به صورت virtual معرفی شوند تا قابلیت بازنویسی را داشته باشند؛ مانند:
public class Blog { public int Id { get; set; } public string Name { get; set; } public virtual ICollection<Post> Posts { get; set; } } public class Post { public int Id { get; set; } public string Title { get; set; } public string Content { get; set; } public virtual Blog Blog { get; set; } }
هرچند این قابلیت بارگذاری اطلاعات وابسته در آینده، جذاب به نظر میرسد اما در عمل در حین رندر یک گرید و یا بکارگیری حلقهها، چون سبب رفت و برگشت بیش از اندازهای به بانک اطلاعاتی خواهد شد، باید با دقت مورد استفاده قرار گیرد و اساسا استفادهی از آن در برنامههای وب توصیه نمیشود (با بررسیهای پروژههای بسیاری مشخص شدهاست که این قابلیت ضررش بیشتر از نفعش است).
ب) فعالسازی Lazy loading بدون استفاده از Proxyها
در این حالت نیازی به نصب بستهی AOP جدید تشکیل پروکسیها نیست. در اینجا در کلاس موجودیت خود باید سرویس ILazyLoader را تزریق کنید:
public class Blog { private ICollection<Post> _posts; public Blog() { } private Blog(ILazyLoader lazyLoader) { LazyLoader = lazyLoader; } private ILazyLoader LazyLoader { get; set; } public int Id { get; set; } public string Name { get; set; } public ICollection<Post> Posts { get => LazyLoader?.Load(this, ref _posts); set => _posts = value; } } public class Post { private Blog _blog; public Post() { } private Post(ILazyLoader lazyLoader) { LazyLoader = lazyLoader; } private ILazyLoader LazyLoader { get; set; } public int Id { get; set; } public string Title { get; set; } public string Content { get; set; } public Blog Blog { get => LazyLoader?.Load(this, ref _blog); set => _blog = value; } }
الگوی Service locator را در قسمت دوم بررسی کردیم. همانطور که عنوان شد، بهتر است تا جائیکه امکان دارد از بکارگیری آن به علت ضدالگو بودن پرهیز کرد. در ادامه قسمتهای مختلف یک برنامهی ASP.NET Core را که میتوان بدون نیاز به استفادهی الگوی Service locator، تزریق وابستگیها را در آنها انجام داد، مرور میکنیم.
در کلاس آغازین برنامه
در اینجا در متد Configure آن تنها کافی است اینترفیس سرویس مدنظر خود را مانند IAmACustomService، به صورت یک پارامتر جدید اضافه کنید. کار وهله سازی آن توسط Service Provider برنامه به صورت خودکار صورت میگیرد:
یک نکتهی مهم: اگر طول عمر IAmACustomService را Scoped تعریف کردهاید و این سرویس از نوع IDisposable نیز میباشد، این روش کارآیی نداشته و باید از نکتهی «روش صحیح Dispose اشیایی با طول عمر Scoped، در خارج از طول عمر یک درخواست ASP.NET Core» که در قسمت قبل معرفی شد استفاده کنید.
در میان افزارها
هم سازندهی یک میان افزار و هم متد Invoke آن قابلیت تزریق وابستگیها را دارند:
از سازندهی آن برای تزریق وابستگی سرویسهایی با طول عمر Singleton استفاده کنید. ServiceProvider به همراه ویژگی است به نام Scope Validation. در این حالت اگر طول عمر سرویسی Singleton باشد (مانند طول عمر یک میانافزار) و در سازندهی آن یک سرویس با طول عمر Scoped تزریق شود، در زمان اجرا یک استثناء را صادر میکند؛ چون در این حالت رفتار این سرویس Scoped نیز Singleton میشود که احتمالا مدنظر شما نیست. در این حالت از پارامترهای اضافی متد Invoke میتوان برای تزریق وابستگیهایی با طول عمر Transient و یا Scoped استفاده کرد.
البته میتوان این Scope Validation را در فایل program.cs به نحو زیر غیرفعال کرد، ولی بهتر است اینکار را انجام ندهید و همان مقدار پیشفرض آن بسیار مناسب است:
در کنترلرها
سازندههای کنترلرهای برنامههای ASP.NET Core قابلیت تزریق وابستگیها را دارند:
در اینجا حتی میتوان با استفاده از ویژگی FromServices، یک سرویس را توسط پارامترهای یک اکشن متد نیز درخواست کرد:
در این حالت بجای model binding، کار دریافت این سرویس درخواستی صورت میگیرد.
در مدلها
ویژگی FromServices بر روی مدلها نیز کار میکند.
در اینجا نحوهی تعریف TestModel را به همراه ویژگی FromServices مشاهده میکنید:
این حالت که property injection نیز نام دارد، نیاز به خاصیتی با یک public setter را دارد.
در Viewها
در Razor Views نیز میتوان توسط inject directive@ کار تزریق وابستگیها را انجام داد:
در ویژگیها و فیلترها
در ASP.NET Core تزریق وابستگیهای در سازندههای فیلترها نیز کار میکند:
در این حالت چون سازندهی این ویژگی، پارامتر دار شدهاست و این پارامترها نیز یک مقدار ثابت قابل کامپایل نیستند، برای معرفی یک چنین فیلتری باید از ServiceFilterها به صورت زیر استفاده کرد:
در کلاس آغازین برنامه
در اینجا در متد Configure آن تنها کافی است اینترفیس سرویس مدنظر خود را مانند IAmACustomService، به صورت یک پارامتر جدید اضافه کنید. کار وهله سازی آن توسط Service Provider برنامه به صورت خودکار صورت میگیرد:
public class Startup { public void ConfigureServices(IServiceCollection services) { } public void Configure(IApplicationBuilder app, IAmACustomService customService) { // .... } }
یک نکتهی مهم: اگر طول عمر IAmACustomService را Scoped تعریف کردهاید و این سرویس از نوع IDisposable نیز میباشد، این روش کارآیی نداشته و باید از نکتهی «روش صحیح Dispose اشیایی با طول عمر Scoped، در خارج از طول عمر یک درخواست ASP.NET Core» که در قسمت قبل معرفی شد استفاده کنید.
در میان افزارها
هم سازندهی یک میان افزار و هم متد Invoke آن قابلیت تزریق وابستگیها را دارند:
public class TestMiddleware { public TestMiddleware(RequestDelegate next, IAmACustomService service) { // ... } public async Task Invoke(HttpContext context, IAmACustomService service) { // ... } }
البته میتوان این Scope Validation را در فایل program.cs به نحو زیر غیرفعال کرد، ولی بهتر است اینکار را انجام ندهید و همان مقدار پیشفرض آن بسیار مناسب است:
public static IWebHostBuilder CreateDefaultBuilder(string[] args) { var builder = new WebHostBuilder() //... .UseDefaultServiceProvider((context, options) => { options.ValidateScopes = context.HostingEnvironment.IsDevelopment(); }) //...
در کنترلرها
سازندههای کنترلرهای برنامههای ASP.NET Core قابلیت تزریق وابستگیها را دارند:
public class HelloController : Controller { private readonly IAmACustomService _customService; public HelloController(IAmACustomService customService) { _customService = customService; } public IActionResult Get() { // ... } }
[HttpGet("[action]")] public IActionResult Index([FromServices] IAmACustomService service) { // ... }
در مدلها
ویژگی FromServices بر روی مدلها نیز کار میکند.
public IActionResult Index(TestModel model) { // ... }
public class TestModel { public string Name { get; set; } [FromServices] public IAmACustomService CustomService { get; set; } }
در Viewها
در Razor Views نیز میتوان توسط inject directive@ کار تزریق وابستگیها را انجام داد:
@inject IAmACustomService CustomService
در ویژگیها و فیلترها
در ASP.NET Core تزریق وابستگیهای در سازندههای فیلترها نیز کار میکند:
public class ApiExceptionFilter : ExceptionFilterAttribute { private ILogger<ApiExceptionFilter> _logger; private IHostingEnvironment _environment; private IConfiguration _configuration; public ApiExceptionFilter(IHostingEnvironment environment, IConfiguration configuration, ILogger<ApiExceptionFilter> logger) { _environment = environment; _configuration = configuration; _logger = logger; }
[Route("api/[controller]")] [ApiController] [ServiceFilter(typeof(ApiExceptionFilter))] public class ValuesController : ControllerBase {
احتمالا در بیشتر مقالات (فارسی/انگلیسی) عبارات هایی مثل نمونههای زیر را دیده اید :
در این مقاله قصد داریم بپردازیم به «مقید سازی پارامترهای نوع جنریک» و اینکه چه کاربردی دارند و در چه زمانی بهتر است از آنها استفاده کنیم و نحوه استفاده از آنها چگونه است. فرض میکنیم که خوانندهی محترم با مفاهیم جنریک آشنایی دارد. در صورتیکه با جنریکها آشنا نیستید ابتدا مروری داشته باشید بر جنریکها و بعد این مقاله را مطالعه فرمایید؛ به این دلیل که موضوع مورد بحث بر پایهی جنریکها میباشد.
همانطور که مطلع هستید هر عنصری جنریکی را که تعریف میکنید حداقل دارای یک پارامتر نوع هست و در زمان بکارگیری آن جنریک باید نوع آن را مشخص نمایید. برای نمونه مثال زیر را در نظر بگیرید :
کلاس فوق یک کلاس جنریک است که در هنگام ساخت نمونهای از آن، باید ابتدا data type نوعی را که که میخواهیم با آن کار کنیم، تعیین کنیم. برای مثال در کد فوق در هنگام ساخت نمونهای از آن، نوع int را برای آن مشخص میکنیم و هر وقت بخواهیم متد Add آن را فراخوانی کنیم، فقط نوعی را قبول خواهد کرد که در ابتدا برای آن تعیین کرده ایم (int):
سؤال: میخواهیم فقط نوعهایی را بتوان به T نسبت داد که از نوع ارجاعی (reference type) هستن و یا فقط نوع هایی را به T نسبت داد که یک سازنده دارند؛ چگونه؟
ایجاد قیدها یا محدودیتها بر روی پارامترهای جنریکها شامل پنج حالت میباشد:
حالت اول : Where T:struct
در این حالت T باید یک ساختار باشد .
حالت دوم : where T:class
T باید یک نوع ارجاعی باشد. اگر در مثال فوق این قید را به آن اضافه کنیم، در هنگام ساخت نمونهای از کلاس فوق، اگر یک نوع value type را به T نسبت دهیم، در هنگام وارد کردن یک نوع value type با خطا مواجه خواهیم شد. مثال:
و برای استفاده :
حالت سوم : ()Where T:new
نوعی که به T نسبت داده میشود باید یک سازندهی پیش فرض داشته باشد.
داخل پرانتز : سازندهی پیش فرض: زمانی که شما یک کلاس مینویسید اگر آن کلاس دارای هیچ سازندهای نباشد، کامپایلر یک سازندهی بدون پارامتر را به کلاس فوق اضافه میکند که کار آن مقدار دهی به فیلدهای کلاس است. در اینجا از مقادیر پیش فرض استفاده میشود. مثلا برای int مقدار صفر و برای string مقدار "" و به همین ترتیب.
اگر از مقدار دهی پیش فرض توسط کامپایلر خرسند نیستید، میتوانید سازنده پیش فرض را تغییر داده و مطابق میل خود فیلدها را مقدار دهی اولیه کنید .
حالت چهارم : where T:NameOfBaseClass
نوعی که به T نسبت داده میشود باید از کلاس NameOfBaseClass ارث بری کرده باشد.
حالت پنجم : where T:NameOfInterface
همانند حالت چهارم میباشد؛ با این تفاوت: نوعی که به T نسبت داده میشود باید واسط NameOfInterface را پیاده سازی کرده باشد.
پنج حالت فوق نمونههایی از ایجاد محدودیت بر روی پرامتر نوع اعضای جنریک بودند و اما در ادامه قصد داریم نکاتی را در این باب، بیان کنیم:
نکته اول : میتوانید محدودیتهای فوق را با هم ترکیب کنید برای اینکار آنها را با کاما از هم جدا کنید :
نوعی که به T نسبت داده میشود
نکته چهارم : زمانیکه کلاس و یا متدهای شما بیش از یک نوع پارامتر از نوع جنریک را دریافت میکنند، باید محدودیتهای مورد نظر را برای هر کدام به صورت جداگانه قید کنید. به طور مثال به کلاس زیر که دو پارمتر T و K را دارد، باید برای هر کدام جداگانه محدودیتهای مورد نظر را اعمال کنیم (در صورت نیاز):
where T:clas where T:struc ...
همانطور که مطلع هستید هر عنصری جنریکی را که تعریف میکنید حداقل دارای یک پارامتر نوع هست و در زمان بکارگیری آن جنریک باید نوع آن را مشخص نمایید. برای نمونه مثال زیر را در نظر بگیرید :
public class MyCollection<T> { private List<T> collections = new List<T>(); public void Add(T value) { collections.Add(value); } }
MyCollection<int> myintObj = new MyCollection<int>(); myintObj.Add(12); myintObj.Add(33); myintObj.Add(33.3);// ERROR z
ایجاد قیدها یا محدودیتها بر روی پارامترهای جنریکها شامل پنج حالت میباشد:
حالت اول : Where T:struct
در این حالت T باید یک ساختار باشد .
حالت دوم : where T:class
T باید یک نوع ارجاعی باشد. اگر در مثال فوق این قید را به آن اضافه کنیم، در هنگام ساخت نمونهای از کلاس فوق، اگر یک نوع value type را به T نسبت دهیم، در هنگام وارد کردن یک نوع value type با خطا مواجه خواهیم شد. مثال:
public class MyCollection<T> where T:class { private List<T> collections = new List<T>(); public void Add(T value) { collections.Add(value); } }
MyCollection<int> myintObj = new MyCollection<int>(); // ERROR , int is value type
حالت سوم : ()Where T:new
نوعی که به T نسبت داده میشود باید یک سازندهی پیش فرض داشته باشد.
داخل پرانتز : سازندهی پیش فرض: زمانی که شما یک کلاس مینویسید اگر آن کلاس دارای هیچ سازندهای نباشد، کامپایلر یک سازندهی بدون پارامتر را به کلاس فوق اضافه میکند که کار آن مقدار دهی به فیلدهای کلاس است. در اینجا از مقادیر پیش فرض استفاده میشود. مثلا برای int مقدار صفر و برای string مقدار "" و به همین ترتیب.
اگر از مقدار دهی پیش فرض توسط کامپایلر خرسند نیستید، میتوانید سازنده پیش فرض را تغییر داده و مطابق میل خود فیلدها را مقدار دهی اولیه کنید .
حالت چهارم : where T:NameOfBaseClass
نوعی که به T نسبت داده میشود باید از کلاس NameOfBaseClass ارث بری کرده باشد.
حالت پنجم : where T:NameOfInterface
همانند حالت چهارم میباشد؛ با این تفاوت: نوعی که به T نسبت داده میشود باید واسط NameOfInterface را پیاده سازی کرده باشد.
پنج حالت فوق نمونههایی از ایجاد محدودیت بر روی پرامتر نوع اعضای جنریک بودند و اما در ادامه قصد داریم نکاتی را در این باب، بیان کنیم:
نکته اول : میتوانید محدودیتهای فوق را با هم ترکیب کنید برای اینکار آنها را با کاما از هم جدا کنید :
public class MyCollection<T> where T:class,IDisposable,new() { //content }
- باید از نوع ارجاعی باشد.
- باید واسط IDisposable را پیاده سازی کرده باشد.
- باید یک سازندهی پیش فرض داشته باشد.
نکته دوم : زمانیکه از چندین محدودیت استفاده میکنید مثل مثال فوق، باید محدودیت ()new در آخرین جایگاه محدودیتها قرار گیرد؛ در غیر اینصورت با خطای زمان ترجمه روبه رو خواهید شد .
نکته سوم : میتوان محدودیتهای فوق را علاوه بر کلاس، بر روی متدهای جنریک نیز اعمال کنید:
public void Swap<T>(ref T val1,ref T val2) where T:struct { //content }
public class MyCollection<T,K> where T:class where K:IDisposable,new() { //content }
فرض کنید مطابق اصول نامگذاری که تعیین کردهاید، تمام جداول بانک اطلاعاتی شما باید با پیشوند tbl شروع شوند. برای انجام اینکار در نگارشهای قبلی EF Code first میبایستی از ویژگی Table جهت مزین کردن تمامی کلاسها استفاده میشد و یا به ازای تک تک موجودیتها، یک کلاس تنظیمات ویژه را افزود و سپس از متد ToTable برای تعیین نامی جدید، استفاده میشد. در EF 6 امکان بازنویسی سادهتر پیش فرضهای تعیین نام جداول، طول فیلدها و غیره، پیش بینی شدهاند که در ادامه تعدادی از آنها را مرور خواهیم کرد.
تعیین پیشوندی برای نام کلیهی جداول بانک اطلاعاتی
اگر نیاز باشد تا به تمامی جداول تهیه شده، بر اساس نام کلاسهای مدلهای برنامه، یک پیشوند tbl اضافه شود، میتوان با بازنویسی متد OnModelCreating کلاس Context برنامه شروع کرد:
سپس متد modelBuilder.Types، کلیه موجودیتهای برنامه را در اختیار قرار داده و در ادامه میتوان برای مثال از متد ToTable، برای تعیین نامی جدید به ازای کلیه کلاسهای مدلهای برنامه استفاده کرد.
تعیین نام دیگری برای کلید اصلی کلیهی جداول برنامه
فرض کنید نیاز است کلیه PKها، با پیشوند نام جدول جاری در بانک اطلاعاتی تشکیل شوند. یعنی اگر نام PK مساوی Id است و نام جدول Menu، نام کلید اصلی نهایی تشکیل شده در بانک اطلاعاتی باید MenuId باشد و نه Id.
این مورد نیز با بازنویسی متد OnModelCreating کلاس Context و سپس استفاده از متد modelBuilder.Properties برای دسترسی به کلیه خواص در حال نگاشت، قابل انجام است. در اینجا کلیه خواصی که نام Id دارند، توسط متد IsKey تبدیل به PK شده و سپس به کمک متد HasColumnName، نام دلخواه جدیدی را خواهند یافت.
تعیین حداکثر طول کلیه فیلدهای رشتهای تمامی جداول بانک اطلاعاتی
اگر نیاز باشد تا پیش فرض MaxLength تمام خواص رشتهای را تغییر داد، میتوان از پیاده سازی اینترفیس جدید IStoreModelConvention کمک گرفت:
در اینجا MaxLength کلیه خواص رشتهای در حال نگاشت به بانک اطلاعاتی، به 450 تنظیم میشود. سپس برای معرفی آن به برنامه خواهیم داشت:
توسط متد modelBuilder.Conventions.Add، میتوان قراردادهای جدید سفارشی را به برنامه افزود.
نظم بخشیدن به تعاریف قراردادهای پیش فرض
اگر علاقمند نیستید که کلاس Context برنامه را شلوغ کنید، میتوان با ارث بری از کلاس پایه Convention، قراردادهای جدید را تعریف و سپس توسط متد modelBuilder.Conventions.Add، کلاس نهایی تهیه شده را به برنامه معرفی کرد.
مثالهای بیشتر
اگر به مستندات EF 6 مراجعه کنید، مثالهای بیشتری را در مورد بکارگیری اینترفیس IStoreModelConvention و یا بازنویسی قراردادهای موجود، خواهید یافت.
تعیین پیشوندی برای نام کلیهی جداول بانک اطلاعاتی
اگر نیاز باشد تا به تمامی جداول تهیه شده، بر اساس نام کلاسهای مدلهای برنامه، یک پیشوند tbl اضافه شود، میتوان با بازنویسی متد OnModelCreating کلاس Context برنامه شروع کرد:
protected override void OnModelCreating(DbModelBuilder modelBuilder) { // TableNameConvention modelBuilder.Types() .Configure(entity => entity.ToTable("tbl" + entity.ClrType.Name)); base.OnModelCreating(modelBuilder); }
تعیین نام دیگری برای کلید اصلی کلیهی جداول برنامه
فرض کنید نیاز است کلیه PKها، با پیشوند نام جدول جاری در بانک اطلاعاتی تشکیل شوند. یعنی اگر نام PK مساوی Id است و نام جدول Menu، نام کلید اصلی نهایی تشکیل شده در بانک اطلاعاتی باید MenuId باشد و نه Id.
protected override void OnModelCreating(DbModelBuilder modelBuilder) { // PrimaryKeyNameConvention modelBuilder.Properties() .Where(p => p.Name == "Id") .Configure(p => p.IsKey().HasColumnName(p.ClrPropertyInfo.ReflectedType.Name + "Id")); base.OnModelCreating(modelBuilder); }
تعیین حداکثر طول کلیه فیلدهای رشتهای تمامی جداول بانک اطلاعاتی
اگر نیاز باشد تا پیش فرض MaxLength تمام خواص رشتهای را تغییر داد، میتوان از پیاده سازی اینترفیس جدید IStoreModelConvention کمک گرفت:
public class StringConventions : IStoreModelConvention<EdmProperty> { public void Apply(EdmProperty property, DbModel model) { if (property.PrimitiveType.PrimitiveTypeKind == PrimitiveTypeKind.String) { property.MaxLength = 450; } } }
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Conventions.Add<StringConventions>(); base.OnModelCreating(modelBuilder); }
نظم بخشیدن به تعاریف قراردادهای پیش فرض
اگر علاقمند نیستید که کلاس Context برنامه را شلوغ کنید، میتوان با ارث بری از کلاس پایه Convention، قراردادهای جدید را تعریف و سپس توسط متد modelBuilder.Conventions.Add، کلاس نهایی تهیه شده را به برنامه معرفی کرد.
public class MyConventions : Convention { public MyConventions() { // PrimaryKeyNameConvention this.Properties() .Where(p => p.Name == "Id") .Configure(p => p.IsKey().HasColumnName(p.ClrPropertyInfo.ReflectedType.Name + "Id")); // TableNameConvention this.Types() .Configure(entity => entity.ToTable("tbl" + entity.ClrType.Name)); } }
مثالهای بیشتر
اگر به مستندات EF 6 مراجعه کنید، مثالهای بیشتری را در مورد بکارگیری اینترفیس IStoreModelConvention و یا بازنویسی قراردادهای موجود، خواهید یافت.
ذخیره سازی اطلاعات بازدیدهای کاربران، در طول زمان حجم بالایی از بانک اطلاعاتی را به خود اختصاص خواهد داد؛ به علاوه کند شدن کوئریهای مرتبط با آن، به همراه مصرف بالای منابع سیستم. به همین جهت اکثر سایتها از Google analytics برای مدیریت جمع آوری بازدیدهای کاربران خود استفاده میکنند و این ابزار واقعا عالی و حرفهای طراحی شده و پیاده سازی همانند آن شاید در حد یک پروژهی چندساله باشد.
اضافه کردن Google analytics به یک سایت، بسیار ساده است. در آن ثبت نام میکنید؛ سپس آدرس دومین خود را وارد کرده و یک قطعه کد جاوا اسکریپتی را دریافت خواهید کرد که باید به انتهای تمام صفحات سایت خود اضافه نمائید و ... همین.
اضافه کردن این کد در ASP.NET MVC میتواند در فایل layout یا همان master page سایت انجام شود تا به صورت خودکار به تمام صفحات اعمال گردد.
مشکل!
من نمیخواهم که صفحات غیرعمومی سایت نیز دارای کدهای Google analytics باشند و بیجهت Google به اینجاها نیز سرکشی زاید کند! چکار باید کرد؟
احتمالا عنوان میکنید که باید یک if و else به همراه آرایهای از نامها و آدرسهای صفحات غیرعمومی سایت تهیه کرد و بر این اساس کدهای Google analytics را در master page درج کرد یا خیر.
بله. این روش کار میکنه ولی بهینه نیست و همچنین نگهداری آن در طول زمان مشکل است. سایت توسعه خواهد یافت، صفحات غیرعمومی بیشتر خواهند شد و ممکن است در این بین فراموش شود که کدهای مرتبط به روز شوند.
روش بهتر:
آیا میتوان در یک View مشخص کرد که فیلتر Authorize در اکشن متد متناظری که آنرا رندر کرده است بکار گرفته شده است یا خیر؟
صفحات غیرعمومی سایت در ASP.NET MVC با فیلتر Authorize محافظت میشوند. این فیلتر را میتوان به کل یک کنترلر اعمال کرد تا به تمام اکشن متدهای آن اعمال شود؛ یا فقط به یک اکشن متد خاص که Viewایی خاص را رندر میکند.
نحوه پیاده سازی تشخیص وجود فیلتر Authorize را در یک View رندر شده، در متد کمکی زیر میتوان مشاهده کرد:
توضیحات:
این متد در فایلی به نام HtmlUtils قرار گرفته در پوشه app_code تعریف شده است و بکارگیری آن در یک فایل master page به نحو زیر خواهد بود:
در این متد به کمک خاصیت page.ViewContext.Controller میتوان به کنترلری که در حال رندر کردن View جاری است دسترسی یافت. اکنون که به کنترلر دسترسی داریم، به کمک Reflection، ویژگیها یا Attributes آنرا یافته و بررسی میکنیم که آیا دارای AuthorizeAttribute است یا خیر. بر این اساس میتوان تصمیم گرفت که آیا View در حال نمایش عمومی است یا خصوصی. اگر عمومی بود، کدهای اسکریپتی Google analytics به صورت خودکار به صفحه تزریق میشوند.
همچنین در اینجا فرض بر این است که Id منتسب به دومین جاری در کلیدی به نام GoogleAnalyticsID در فایل کانفیگ برنامه در قسمت app settings آن تعریف شده است.
اضافه کردن Google analytics به یک سایت، بسیار ساده است. در آن ثبت نام میکنید؛ سپس آدرس دومین خود را وارد کرده و یک قطعه کد جاوا اسکریپتی را دریافت خواهید کرد که باید به انتهای تمام صفحات سایت خود اضافه نمائید و ... همین.
اضافه کردن این کد در ASP.NET MVC میتواند در فایل layout یا همان master page سایت انجام شود تا به صورت خودکار به تمام صفحات اعمال گردد.
مشکل!
من نمیخواهم که صفحات غیرعمومی سایت نیز دارای کدهای Google analytics باشند و بیجهت Google به اینجاها نیز سرکشی زاید کند! چکار باید کرد؟
احتمالا عنوان میکنید که باید یک if و else به همراه آرایهای از نامها و آدرسهای صفحات غیرعمومی سایت تهیه کرد و بر این اساس کدهای Google analytics را در master page درج کرد یا خیر.
بله. این روش کار میکنه ولی بهینه نیست و همچنین نگهداری آن در طول زمان مشکل است. سایت توسعه خواهد یافت، صفحات غیرعمومی بیشتر خواهند شد و ممکن است در این بین فراموش شود که کدهای مرتبط به روز شوند.
روش بهتر:
آیا میتوان در یک View مشخص کرد که فیلتر Authorize در اکشن متد متناظری که آنرا رندر کرده است بکار گرفته شده است یا خیر؟
صفحات غیرعمومی سایت در ASP.NET MVC با فیلتر Authorize محافظت میشوند. این فیلتر را میتوان به کل یک کنترلر اعمال کرد تا به تمام اکشن متدهای آن اعمال شود؛ یا فقط به یک اکشن متد خاص که Viewایی خاص را رندر میکند.
نحوه پیاده سازی تشخیص وجود فیلتر Authorize را در یک View رندر شده، در متد کمکی زیر میتوان مشاهده کرد:
@helper IncludeGoogleAnalytics(WebViewPage page) { var controller = page.ViewContext.Controller; var controllerHasAuthorizeAttribute = controller.GetType().GetCustomAttributes(typeof(AuthorizeAttribute), true).Any(); var currentActionName = page.ViewContext.Controller.ValueProvider.GetValue("action").RawValue.ToString(); var actionHasAuthorizeAttribute = controller.GetType().GetMethods() .Where(x => x.Name == currentActionName && x.GetCustomAttributes(typeof(AuthorizeAttribute), true).Any()) .Any(); if (!controllerHasAuthorizeAttribute && !actionHasAuthorizeAttribute) { string trackingId = ConfigurationManager.AppSettings["GoogleAnalyticsID"]; <script type="text/javascript"> var _gaq = _gaq || []; _gaq.push(['_setAccount', '@trackingId']); _gaq.push(['_trackPageview']); (function () { var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); })(); </script> }
توضیحات:
این متد در فایلی به نام HtmlUtils قرار گرفته در پوشه app_code تعریف شده است و بکارگیری آن در یک فایل master page به نحو زیر خواهد بود:
@HtmlUtils.IncludeGoogleAnalytics(this)
در این متد به کمک خاصیت page.ViewContext.Controller میتوان به کنترلری که در حال رندر کردن View جاری است دسترسی یافت. اکنون که به کنترلر دسترسی داریم، به کمک Reflection، ویژگیها یا Attributes آنرا یافته و بررسی میکنیم که آیا دارای AuthorizeAttribute است یا خیر. بر این اساس میتوان تصمیم گرفت که آیا View در حال نمایش عمومی است یا خصوصی. اگر عمومی بود، کدهای اسکریپتی Google analytics به صورت خودکار به صفحه تزریق میشوند.
همچنین در اینجا فرض بر این است که Id منتسب به دومین جاری در کلیدی به نام GoogleAnalyticsID در فایل کانفیگ برنامه در قسمت app settings آن تعریف شده است.
مطالب
کمپین ضد IF !
بکارگیری بیش از حد If و خصوصا Switch برخلاف اصول طراحی شیءگرا است؛ تا این حد که یک کمپین ضد IF هم وجود دارد!
البته سایت فوق بیشتر جنبه تبلیغی برای سمینارهای گروه مذکور را دارد تا اینکه جنبهی آموزشی/خود آموزی داشته باشد.
یک مثال کاربردی:
فرض کنید دارید یک سیستم گزارشگیری را طراحی میکنید. به جایی میرسید که نیاز است با Aggregate functions سروکار داشته باشید؛ مثلا جمع مقادیر یک ستون را نمایش دهید یا معدل امتیازهای نمایش داده شده را محاسبه کنید و امثال آن. طراحی متداول آن به صورت زیر خواهد بود:
using System.Collections.Generic;
using System.Linq;
namespace CircularDependencies
{
public enum AggregateFunc
{
Sum,
Avg
}
public class AggregateFuncCalculator
{
public decimal Calculate(IList<decimal> list, AggregateFunc func)
{
switch (func)
{
case AggregateFunc.Sum:
return getSum(list);
case AggregateFunc.Avg:
return getAvg(list);
default:
return 0m;
}
}
private decimal getAvg(IList<decimal> list)
{
if (list == null || !list.Any()) return 0;
return list.Sum() / list.Count;
}
private decimal getSum(IList<decimal> list)
{
if (list == null || !list.Any()) return 0;
return list.Sum();
}
}
}
در کلاس AggregateFuncCalculator یک متد Calculate داریم که توسط آن قرار است روی list دریافتی یک سری عملیات انجام شود. عملیات پشتیبانی شده هم توسط یک enum معرفی شده؛ برای مثال اینجا فقط جمع و میانگین پشتیبانی میشوند.
و مشکل طراحی این کلاس، همان switch است که برخلاف اصول طراحی شیءگرا میباشد. یکی از اصول طراحی شیءگرا بر این مبنا است که:
یک کلاس باید جهت تغییر، بسته اما جهت توسعه، باز باشد.
یعنی چی؟
داستان طراحی Aggregate functions که فقط به جمع و میانگین خلاصه نمیشود. امروز میگویند واریانس چطور؟ فردا خواهند گفت حداقل و حداکثر چطور؟ پس فردا ...
به عبارتی این کلاس جهت تغییر بسته نیست و هر روز باید بر اساس نیازهای جدید دستکاری شود.
چکار باید کرد؟
آیا میتوانید در کلاس AggregateFuncCalculator یک الگوی تکراری را تشخیص دهید؟ الگوی تکراری موجود، محاسبات بر روی یک لیست است. پس میشود بر اساس آن یک اینترفیس عمومی را تعریف کرد:
public interface IAggregateFunc
{
decimal Calculate(IList<decimal> list);
}
اکنون هر کدام از پیاده سازیهای موجود در کلاس AggregateFuncCalculator را به یک کلاس جدا منتقل خواهیم کرد تا یک اصل دیگر طراحی شیءگرا نیز محقق شود:
هر کلاس باید تنها یک کار را انجام دهد.
public class Sum : IAggregateFunc
{
public decimal Calculate(IList<decimal> list)
{
if (list == null || !list.Any()) return 0;
return list.Sum();
}
}
public class Avg : IAggregateFunc
{
public decimal Calculate(IList<decimal> list)
{
if (list == null || !list.Any()) return 0;
return list.Sum() / list.Count;
}
}
تا اینجا 2 هدف مهم حاصل شده است:
- کم کم کلاس AggregateFuncCalculator دارد خلوت میشود. قرار است هر کلاس یک کار را بیشتر انجام ندهد.
- برنامه از بسته بودن جهت توسعه هم خارج شده است (یکی دیگر از اصول طراحی شیءگرا). اگر تعاریف توابع محاسباتی را تماما در یک کلاس قرار دهیم صاحب اول و آخر آن کتابخانه خودمان خواهیم بود. این کلاس بسته است جهت تغییر. اما با معرفی IAggregateFunc، من امروز 2 تابع را تعریف کردهام، شما فردا توابع خاص خودتان را تعریف کنید. باز هم برنامه کار خواهد کرد. نیازی نیست تا من هر روز یک نگارش جدید از کتابخانه را ارائه دهم که در آن فقط یک تابع دیگر اضافه شده است.
اکنون یکی از چندین و چند روش بازنویسی کلاس AggregateFuncCalculator به صورت زیر میتواند باشد
public class AggregateFuncCalculator
{
public decimal Calculate(IList<decimal> list, IAggregateFunc func)
{
return func.Calculate(list);
}
}
بله! دیگر سوئیچی در کار نیست. این کلاس تنها یک کار را انجام میدهد. همچنین دیگر نیازی به تغییر هم ندارد (محاسبات از آن خارج شده) و باز است جهت توسعه (شما نگارشهای دلخواه IAggregateFunc دیگر خود را توسعه داده و استفاده کنید).
در مورد static reflection مقدمهای پیشتر در این سایت قابل مطالعه است (^) و پیشنیاز بحث جاری است. در ادامه قصد داریم یک سری از کاربردهای متداول آنرا که این روزها در گوشه و کنار وب یافت میشود، به زبان ساده بررسی کنیم.
بهبود کدهای موجود
از static reflection در دو حالت کلی میتوان استفاده کرد. یا قرار است کتابخانهای را از صفر طراحی کنیم یا اینکه خیر؛ کتابخانهای موجود است و میخواهیم کیفیت آنرا بهبود ببخشیم. هدف اصلی هم «حذف رشتهها» و «استفاده از کد بجای رشتهها» است.
برای مثال قطعه کد زیر یک مثال متداول مرتبط با WPF و یا Silverlight است. در آن با پیاده سازی اینترفیس INotifyPropertyChanged و استفاده از متد raisePropertyChanged ، به رابط کاربری برنامه اعلام خواهیم کرد که لطفا خودت را بر اساس اطلاعات جدید تنظیم شده در قسمت set خاصیت Name ، به روز کن:
using System.ComponentModel;
namespace StaticReflection
{
public class User : INotifyPropertyChanged
{
string _name;
public string Name
{
get { return _name; }
set
{
if (_name == value) return;
_name = value;
raisePropertyChanged("Name");
}
}
public event PropertyChangedEventHandler PropertyChanged;
void raisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler == null) return;
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
تعاریف قسمت PropertyChangedEventArgs این پیاده سازی، خارج از کنترل ما است و در دات نت فریم ورک تعریف شده است. حتما هم نیاز به رشته دارد؛ آن هم نام خاصیتی که تغییر کرده است. چقدر خوب میشد اگر میتوانستیم این رشته را حذف کنیم تا کامپایلر بتواند صحت بکارگیری اطلاعات وارد شده را دقیقا پیش از اجرای برنامه بررسی کند. الان فقط در زمان اجرا است که متوجه خواهیم شد، مثلا آیا به روز رسانی مورد نظر صورت گرفتهاست یا خیر؛ اگر نه، یعنی احتمالا یک اشتباه تایپی جایی وجود دارد.
برای بهبود این کد همانطور که در قسمت قبل نیز گفته شد، از ترکیب کلاسهای Expression و Func استفاده خواهیم کرد. در اینجا Func قرار نیست چیزی را اجرا کند، بلکه از آن به عنوان قطعه کدی که اطلاعاتش قرار است استخراج شود (Lambdas as Data) استفاده میشود. این استخراج اطلاعات هم توسط کلاس Expression انجام میشود. بنابراین قسمت اول بهبود کد به صورت زیر شروع میشود:
void raisePropertyChanged(Expression<Func<object>> expression)
الان اگر متد raisePropertyChanged بکارگرفته شده در خاصیت Name را بخواهیم اصلاح کنیم، حداقل با دو واقعهی مطلوب زیر مواجه خواهیم شد:
Intellisense به صورت خودکار کار میکند:
حتی بدویترین ابزارهای Refactoring موجود (منظور همان ابزار توکار VS.NET است!) هم امکان Refactoring را در اینجا فراهم خواهند ساخت:
در پایان کد تکمیل شده فوق به شرح زیر خواهد بود که در آن از کلاس Expression جهت استخراج Member.Name استفاده شده است:
using System;
using System.ComponentModel;
using System.Linq.Expressions;
namespace StaticReflection
{
public class User : INotifyPropertyChanged
{
string _name;
public string Name
{
get { return _name; }
set
{
if (_name == value) return;
_name = value;
raisePropertyChanged(() => Name);
}
}
public event PropertyChangedEventHandler PropertyChanged;
void raisePropertyChanged(Expression<Func<object>> expression)
{
var memberExpression = expression.Body as MemberExpression;
if (memberExpression == null)
throw new InvalidOperationException("Not a member access.");
var handler = PropertyChanged;
if (handler == null) return;
handler(this, new PropertyChangedEventArgs(memberExpression.Member.Name));
}
}
}