بله کار کرد. متشکرم.
نظرات مطالب
آموزش سیلورلایت 4 - قسمتهای 1 تا 5
ممنون ، کار من رو راحت کردی ;)
گاهی از اوقات، برای نوشتن آزمونهای واحد، ایزوله سازی قسمتی که میخواهیم آنرا بررسی کنیم، از سایر قسمتهای سیستم مشکل میشود. برای مثال اگر در کلاسی کار اتصال به بانک اطلاعاتی صورت میگیرد و قصد داریم برای آن آزمون واحد بنویسیم، اما قرار نیست که الزاما با بانک اطلاعاتی کار کنیم، در این حالت نیاز به یک نمونهی تقلیدی یا Mock از بانک اطلاعاتی را خواهیم داشت، تا کار دسترسی به بانک اطلاعاتی را شبیه سازی کند. در این سری با استفاده از کتابخانهی بسیار معروف Moq (ماکیو تلفظ میشود؛ گاهی از اوقات هم ماک)، کار ایزوله سازی کلاسها را انجام خواهیم داد، تا بتوانیم آنها را مستقل از هم آزمایش کنیم.
Mocking چیست؟
فرض کنید برنامهای را داریم که از تعدادی کلاس تشکیل شدهاست. در این بین میخواهیم تعدادی از آنها را به صورت ایزولهی از کل سیستم آزمایش کنیم. البته باید درنظر داشت که این کلاسها در حین اجرای واقعی برنامه، از تعدادی وابستگی خاص در همان سیستم استفاده میکنند. برای مثال کلاسی در این بین برای بررسی میزان اعتبار مالی یک کاربر، نیاز دارد تا با یک وب سرویس خارجی کار کند. اما چون میخواهیم این کلاس را به صورت ایزولهی از کل سیستم آزمایش کنیم، اینبار بجای استفادهی از وابستگی واقعی این کلاس، آن وابستگی را با یک نمونهی تقلیدی یا Mock object در اینجا، جایگزین میکنیم.
بنابراین Mocking به معنای جایگزین کردن یک وابستگی واقعی سیستم که در زمان اجرای آن مورد استفاده قرار میگیرد، با نمونهی تقلیدی مختص زمان آزمایش برنامه، جهت بالابردن سهولت نوشتن آزمونهای واحد است.
دلایل و مزایای استفادهی از Mocking
- یکی از مهمترین دلایل استفادهی از Mocking، کاهش پیچیدگی تنظیمات اولیهی نوشتن آزمونهای واحد است. برای مثال اگر در برنامهی خود از تزریق وابستگیها استفاده میکنید و کلاسی دارای چندین وابستگی تزریق شدهی به آن است، برای آزمایش این کلاس نیاز به تدارک تمام این وابستگیها را خواهید داشت تا بتوان این کلاس را وهله سازی کرد و همچنین برنامه را نیز کامپایل نمود. اما در این بین ممکن است آزمایش متدی در همان کلاس، الزاما از تمام وابستگیهای تزریق شدهی در یک کلاس استفاده نکند. در این حالت، Mocking میتواند تنظیمات پیچیدهی وهله سازی این کلاس را به حداقل برساند.
- Mocking میتواند سبب افزایش سرعت اجرای آزمونهای واحد نیز شود. برای مثال با تقلید سرویسهای خارجی مورد استفادهی در برنامه (هر عملی که از مرزهای سیستم رد شود مانند کار با شبکه، بانک اطلاعاتی، فایل سیستم و غیره)، میتوان میزان I/O و همچنین زمان صرف شدهی به آنرا به حداقل رساند.
- از mock objects میتوان برای رهایی از مشکلات کار با مقادیر غیرمشخص استفاده کرد. برای مثال اگر در کدهای خود از DateTime.Now استفاده میکنید یا اعداد اتفاقی و امثال آن، هربار که آزمونهای واحد را اجرا میکنیم، خروجی متفاوتی را دریافت کرده و بسیاری از آزمونهای نوشته شده با مشکل مواجه میشوند. به کمک mocking میتوان بجای این مقادیر غیرمشخص، یک مقدار ثابت و مشخص را بازگشت دهد.
- چون به سادگی میتوان mock objects را تهیه کرد، میتوان کار توسعه و آزمایش برنامه را پیش از به پایان رسیدن پیاده سازی اصلی سرویسهای مدنظر، همینقدر که اینترفیس آن سرویس مشخص باشد، شروع کرد که میتواند برای کارهای تیمی بسیار مفید باشد.
- اگر وابستگی مورد استفاده ناپایدار و یا غیرقابل پیش بینی است، میتوان توسط mocking به یک نمونهی قابل پیش بینی و پایدار مخصوص آزمونهای برنامه رسید.
- اگر وابستگی خارجی مورد استفاده به ازای هر بار استفاده، هزینهای را شارژ میکند، میتوان توسط mocking، هزینهی آزمونهای برنامه را کاهش داد.
Unit test چیست؟
بدیهی است در کنار آزمایش ایزولهی قسمتهای مختلف برنامه توسط mocking، باید کل برنامه را جهت بررسی دستیابی به نتایج واقعی نیز آزمایش کرد که به این نوع آزمونها، آزمون یکپارچگی (Integration Tests)، API Tests ،UI Tests و غیره میگویند که در کنار Unit tests ما حضور خواهند داشت. بنابراین اکنون این سؤال مطرح میشود که یک Unit چیست؟
در برنامهای که از چندین کلاس تشکیل میشود، به یک کلاس، یک Unit گفته میشود. همچنین اگر در این سیستم، دو یا چند کلاس با هم کار میکنند (کلاسی که از چندین وابستگی استفاده میکند)، اینها با هم نیز یک Unit را تشکیل دهند. بنابراین تعریف Unit بستگی به نحوهی درک عملکرد یک سیستم و تعامل اجزای آن با هم دارد.
واژههای متناظر با Mock objects
در حین مطالعهی منابع مرتبط با آزمونهای واحد ممکن است با این واژههای تقریبا مشابه مواجه شوید: fakes ،stubs ،dummies و mocks. اما تفاوت آنها در چیست؟
- Fakes در حقیقت یک نمونه پیاده سازی واقعی، اما غیرمناسب محیط واقعی و اصلی پروژهاست. برای نمونه EF Core به همراه یک نمونه in-memory database هم هست که دقیقا با مفهوم Fakes تطابق دارد.
- از Dummies صرفا جهت تهیهی پارامترهای مورد نیاز برای اجرای یک آزمایش استفاده میشوند. این پارامترها، هیچگاه در آزمایشهای انجام شده مورد استفاده قرار نمیگیرند.
- از Stubs برای ارائهی پاسخهایی مشخص به فراخوانها استفاده میشود. برای مثال یک متد یا خاصیت، دقیقا چه چیزی را باید بازگشت دهند.
- از Mocks برای بررسی تعامل اجزای مختلف در حال آزمایش استفاده میشود. آیا متدی یا خاصیتی مورد استفاده قرار گرفتهاست یا خیر؟
باید درنظر داشت که زمانیکه یک شیء Mock را توسط کتابخانهی Moq تهیه میکنیم، هر سه مفهوم stubs ،dummies و mocks را با هم به همراه دارد. به همین جهت در این سری زمانیکه به یک mock object اشاره میشود، هر سه مفهوم مدنظر هستند.
واژهی دیگری که ممکن است در این گروه زیاد مشاهده شود، «Test double» نام دارد که ترکیب هر 4 مورد fakes ،stubs ،dummies و mocks میباشد. در کل هر زمانیکه یک شیء مورد استفادهی در زمان اجرای برنامه را جهت آزمایش سادهتر آن جایگزین میکنید، یک Test double را ایجاد کردهاید.
بررسی ساختار برنامهای که میخواهیم آنرا آزمایش کنیم
در این سری قصد داریم یک برنامهی وام دهی را آزمایش کنیم که قسمتهای مختلف آن دارای وابستگیهای خاصی میباشند. ساختار این برنامه را در ادامه مشاهده میکنید:
موجودیتهای برنامهی وام دهی
مدلهای برنامهی وام دهی
سرویسهای برنامهی وام دهی
توضیحات:
هدف از این برنامه، درخواست یک وام جدید است. Application در اینجا به معنای درخواست یا فرم جدید است و Applicant نیز شخصی است که این درخواست را دادهاست.
در اینجا بیشتر تمرکز ما بر روی کلاس LoanApplicationProcessor است که دارای دو وابستگی تزریق شدهی به آن نیز میباشد:
از این وابستگیها برای تصدیق هویت درخواست کننده و همچنین بررسی میزان اعتبار او استفاده میشود.
تمام این منطق نیز در متد Process آن قابل مشاهدهاست که هدف اصلی آن، بررسی قابل پذیرش بودن درخواست یک وام جدید است.
نوشتن اولین تست، برای برنامهی وام دهی
در اولین تصویر این قسمت، پروژهی class library دومی را نیز به نام Loans.Tests مشاهده میکنید. فایل csproj آن به صورت زیر برای کار با MSTest تنظیم شدهاست:
که در آن ارجاعی به پروژهی Loans.csproj و همچنین وابستگیهای MSTest، تنظیم شدهاند.
اکنون اولین آزمون واحد ما در کلاس جدید LoanApplicationProcessorShould چنین شکلی را پیدا میکند:
در حین کار با MSTest، کلاس آزمون واحد باید به ویژگی TestClass و متدهای public void آن به ویژگی TestMethod مزین شوند تا توسط این فریمورک آزمون واحد شناسایی شده و مورد آزمایش قرار گیرند.
در این آزمایش، شخص درخواست کننده، حقوق کمی دارد و میخواهیم بررسی کنیم که آیا LoanApplicationProcessor میتواند آنرا بر اساس مقدار MinimumSalary، رد کند یا خیر؟
در حین وهله سازی LoanApplicationProcessor، دو وابستگی آن به null تنظیم شدهاند؛ چون میدانیم که بررسی MinimumSalary پیش از سایر بررسیها صورت میگیرد و اساسا در این آزمایش، نیازی به این وابستگیها نداریم.
اما اگر سعی در اجرای این آزمایش کنیم (برای مثال با اجرای دستور dotnet test در خط فرمان)، آزمایش اجرا نشده و با استثنای زیر مواجه میشویم:
چون در سازندهی کلاس LoanApplicationProcessor، در صورت نال بودن وابستگیهای دریافتی، یک استثناء صادر میشود. بنابراین ذکر آنها الزامی است:
نصب کتابخانهی Moq جهت برآورده کردن وابستگیهای کلاس LoanApplicationProcessor
در این آزمایش چون وجود وابستگیهای در سازندهی کلاس، برای ما اهمیتی ندارند و همچنین ذکر آنها نیز الزامی است، میخواهیم توسط کتابخانهی Moq، دو نمونهی تقلیدی از آنها را تهیه کرده (همان dummies که پیشتر معرفی شدند) و جهت برآورده کردن بررسی صورت گرفتهی در سازندهی کلاس LoanApplicationProcessor، آنها را ارائه کنیم.
کتابخانهی بسیار معروف Moq، با پروژههای مبتنی بر NETFramework 4.5. و همچنین NETStandard 2.0. به بعد سازگار است و برای نصب آن، میتوان یکی از دو دستور زیر را صادر کرد:
اما چرا کتابخانهی Moq؟
کتابخانهی Moq این اهداف را دنبال میکند: سادهاست، به شدت کاربردیاست و همچنین strongly typed است. این کتابخانه سورس باز بوده و تعداد بار دانلود بستهی نیوگت آن میلیونی است.
پس از نصب آن، اولین آزمایشی را که نوشتیم، به صورت زیر اصلاح میکنیم:
در اینجا بجای ارسال null به سازندهی کلاس LoanApplicationProcessor، جهت برآورده کردن مقدار پیشفرض پارامترهای آن و کامپایل شدن برنامه، نمونههای تقلیدی دو وابستگی مورد نیاز آنرا تهیه و به آن ارسال کردهایم.
کار با ذکر new Mock شروع شده و آرگومان جنریک آنرا از نوع وابستگیهایی که نیاز داریم، مقدار دهی میکنیم. سپس خاصیت Object آن، امکان دسترسی به این شیء تقلید شده را میسر میکند.
اکنون اگر مجددا این آزمون واحد را اجرا کنیم، مشاهده خواهیم کرد که بجای صدور استثناء، با موفقیت به پایان رسیدهاست:
گاهی از اوقات جایگزین کردن یک وابستگی null با نمونهی Mock آن کافی نیست
در مثالی که بررسی کردیم، اشیاء mock، کار برآورده کردن نیازهای ابتدایی آزمایش را انجام داده و سبب اجرای موفقیت آمیز آن شدند؛ اما همیشه اینطور نیست:
تفاوت این آزمایش جدید با قبلی، در دو مورد است: مقدار Salary به MinimumSalary تنظیم شدهاست و در آخر Assert.IsTrue را داریم.
اگر این آزمایش را اجرا کنیم، با شکست مواجه خواهد شد. علت اینجا است که هرچند در حال استفادهی از دو mock object به عنوان وابستگیهای مورد نیاز هستیم، اما تنظیمات خاصی را بر روی آنها انجام ندادهایم و به همین جهت خروجی مناسبی را در اختیار LoanApplicationProcessor قرار نمیدهند. برای مثال مرحلهی بعدی بررسی اعتبار شخص در کلاس LoanApplicationProcessor، فراخوانی سرویس identityVerifier و متد Validate آن است که خروجی آن بر اساس کدهای فعلی، همیشه false است:
در قسمت بعدی، کار تنظیم اشیاء mock را انجام خواهیم داد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MoqSeries-01.zip
Mocking چیست؟
فرض کنید برنامهای را داریم که از تعدادی کلاس تشکیل شدهاست. در این بین میخواهیم تعدادی از آنها را به صورت ایزولهی از کل سیستم آزمایش کنیم. البته باید درنظر داشت که این کلاسها در حین اجرای واقعی برنامه، از تعدادی وابستگی خاص در همان سیستم استفاده میکنند. برای مثال کلاسی در این بین برای بررسی میزان اعتبار مالی یک کاربر، نیاز دارد تا با یک وب سرویس خارجی کار کند. اما چون میخواهیم این کلاس را به صورت ایزولهی از کل سیستم آزمایش کنیم، اینبار بجای استفادهی از وابستگی واقعی این کلاس، آن وابستگی را با یک نمونهی تقلیدی یا Mock object در اینجا، جایگزین میکنیم.
بنابراین Mocking به معنای جایگزین کردن یک وابستگی واقعی سیستم که در زمان اجرای آن مورد استفاده قرار میگیرد، با نمونهی تقلیدی مختص زمان آزمایش برنامه، جهت بالابردن سهولت نوشتن آزمونهای واحد است.
دلایل و مزایای استفادهی از Mocking
- یکی از مهمترین دلایل استفادهی از Mocking، کاهش پیچیدگی تنظیمات اولیهی نوشتن آزمونهای واحد است. برای مثال اگر در برنامهی خود از تزریق وابستگیها استفاده میکنید و کلاسی دارای چندین وابستگی تزریق شدهی به آن است، برای آزمایش این کلاس نیاز به تدارک تمام این وابستگیها را خواهید داشت تا بتوان این کلاس را وهله سازی کرد و همچنین برنامه را نیز کامپایل نمود. اما در این بین ممکن است آزمایش متدی در همان کلاس، الزاما از تمام وابستگیهای تزریق شدهی در یک کلاس استفاده نکند. در این حالت، Mocking میتواند تنظیمات پیچیدهی وهله سازی این کلاس را به حداقل برساند.
- Mocking میتواند سبب افزایش سرعت اجرای آزمونهای واحد نیز شود. برای مثال با تقلید سرویسهای خارجی مورد استفادهی در برنامه (هر عملی که از مرزهای سیستم رد شود مانند کار با شبکه، بانک اطلاعاتی، فایل سیستم و غیره)، میتوان میزان I/O و همچنین زمان صرف شدهی به آنرا به حداقل رساند.
- از mock objects میتوان برای رهایی از مشکلات کار با مقادیر غیرمشخص استفاده کرد. برای مثال اگر در کدهای خود از DateTime.Now استفاده میکنید یا اعداد اتفاقی و امثال آن، هربار که آزمونهای واحد را اجرا میکنیم، خروجی متفاوتی را دریافت کرده و بسیاری از آزمونهای نوشته شده با مشکل مواجه میشوند. به کمک mocking میتوان بجای این مقادیر غیرمشخص، یک مقدار ثابت و مشخص را بازگشت دهد.
- چون به سادگی میتوان mock objects را تهیه کرد، میتوان کار توسعه و آزمایش برنامه را پیش از به پایان رسیدن پیاده سازی اصلی سرویسهای مدنظر، همینقدر که اینترفیس آن سرویس مشخص باشد، شروع کرد که میتواند برای کارهای تیمی بسیار مفید باشد.
- اگر وابستگی مورد استفاده ناپایدار و یا غیرقابل پیش بینی است، میتوان توسط mocking به یک نمونهی قابل پیش بینی و پایدار مخصوص آزمونهای برنامه رسید.
- اگر وابستگی خارجی مورد استفاده به ازای هر بار استفاده، هزینهای را شارژ میکند، میتوان توسط mocking، هزینهی آزمونهای برنامه را کاهش داد.
Unit test چیست؟
بدیهی است در کنار آزمایش ایزولهی قسمتهای مختلف برنامه توسط mocking، باید کل برنامه را جهت بررسی دستیابی به نتایج واقعی نیز آزمایش کرد که به این نوع آزمونها، آزمون یکپارچگی (Integration Tests)، API Tests ،UI Tests و غیره میگویند که در کنار Unit tests ما حضور خواهند داشت. بنابراین اکنون این سؤال مطرح میشود که یک Unit چیست؟
در برنامهای که از چندین کلاس تشکیل میشود، به یک کلاس، یک Unit گفته میشود. همچنین اگر در این سیستم، دو یا چند کلاس با هم کار میکنند (کلاسی که از چندین وابستگی استفاده میکند)، اینها با هم نیز یک Unit را تشکیل دهند. بنابراین تعریف Unit بستگی به نحوهی درک عملکرد یک سیستم و تعامل اجزای آن با هم دارد.
واژههای متناظر با Mock objects
در حین مطالعهی منابع مرتبط با آزمونهای واحد ممکن است با این واژههای تقریبا مشابه مواجه شوید: fakes ،stubs ،dummies و mocks. اما تفاوت آنها در چیست؟
- Fakes در حقیقت یک نمونه پیاده سازی واقعی، اما غیرمناسب محیط واقعی و اصلی پروژهاست. برای نمونه EF Core به همراه یک نمونه in-memory database هم هست که دقیقا با مفهوم Fakes تطابق دارد.
- از Dummies صرفا جهت تهیهی پارامترهای مورد نیاز برای اجرای یک آزمایش استفاده میشوند. این پارامترها، هیچگاه در آزمایشهای انجام شده مورد استفاده قرار نمیگیرند.
- از Stubs برای ارائهی پاسخهایی مشخص به فراخوانها استفاده میشود. برای مثال یک متد یا خاصیت، دقیقا چه چیزی را باید بازگشت دهند.
- از Mocks برای بررسی تعامل اجزای مختلف در حال آزمایش استفاده میشود. آیا متدی یا خاصیتی مورد استفاده قرار گرفتهاست یا خیر؟
باید درنظر داشت که زمانیکه یک شیء Mock را توسط کتابخانهی Moq تهیه میکنیم، هر سه مفهوم stubs ،dummies و mocks را با هم به همراه دارد. به همین جهت در این سری زمانیکه به یک mock object اشاره میشود، هر سه مفهوم مدنظر هستند.
واژهی دیگری که ممکن است در این گروه زیاد مشاهده شود، «Test double» نام دارد که ترکیب هر 4 مورد fakes ،stubs ،dummies و mocks میباشد. در کل هر زمانیکه یک شیء مورد استفادهی در زمان اجرای برنامه را جهت آزمایش سادهتر آن جایگزین میکنید، یک Test double را ایجاد کردهاید.
بررسی ساختار برنامهای که میخواهیم آنرا آزمایش کنیم
در این سری قصد داریم یک برنامهی وام دهی را آزمایش کنیم که قسمتهای مختلف آن دارای وابستگیهای خاصی میباشند. ساختار این برنامه را در ادامه مشاهده میکنید:
موجودیتهای برنامهی وام دهی
namespace Loans.Entities { public class Applicant { public int Id { set; get; } public string Name { set; get; } public int Age { set; get; } public string Address { set; get; } public decimal Salary { set; get; } } }
namespace Loans.Entities { public class LoanProduct { public int Id { set; get; } public string ProductName { set; get; } public decimal InterestRate { set; get; } } }
namespace Loans.Entities { public class LoanApplication { public int Id { set; get; } public LoanProduct Product { set; get; } public LoanAmount Amount { set; get; } public Applicant Applicant { set; get; } public bool IsAccepted { set; get; } } public class LoanAmount { public string CurrencyCode { get; set; } public decimal Principal { get; set; } } }
مدلهای برنامهی وام دهی
namespace Loans.Models { public class IdentityVerificationStatus { public bool Passed { get; set; } } }
سرویسهای برنامهی وام دهی
using Loans.Models; namespace Loans.Services.Contracts { public interface IIdentityVerifier { void Initialize(); bool Validate(string applicantName, int applicantAge, string applicantAddress); void Validate(string applicantName, int applicantAge, string applicantAddress, out bool isValid); void Validate(string applicantName, int applicantAge, string applicantAddress, ref IdentityVerificationStatus status); } }
namespace Loans.Services.Contracts { public interface ICreditScorer { int Score { get; } void CalculateScore(string applicantName, string applicantAddress); } }
using System; using Loans.Entities; using Loans.Services.Contracts; namespace Loans.Services { public class LoanApplicationProcessor { private const decimal MinimumSalary = 1_500_000_0; private const int MinimumAge = 18; private const int MinimumCreditScore = 100_000; private readonly IIdentityVerifier _identityVerifier; private readonly ICreditScorer _creditScorer; public LoanApplicationProcessor( IIdentityVerifier identityVerifier, ICreditScorer creditScorer) { _identityVerifier = identityVerifier ?? throw new ArgumentNullException(nameof(identityVerifier)); _creditScorer = creditScorer ?? throw new ArgumentNullException(nameof(creditScorer)); } public bool Process(LoanApplication application) { application.IsAccepted = false; if (application.Applicant.Salary < MinimumSalary) { return application.IsAccepted; } if (application.Applicant.Age < MinimumAge) { return application.IsAccepted; } _identityVerifier.Initialize(); var isValidIdentity = _identityVerifier.Validate( application.Applicant.Name, application.Applicant.Age, application.Applicant.Address); if (!isValidIdentity) { return application.IsAccepted; } _creditScorer.CalculateScore(application.Applicant.Name, application.Applicant.Address); if (_creditScorer.Score < MinimumCreditScore) { return application.IsAccepted; } application.IsAccepted = true; return application.IsAccepted; } } }
using System; using Loans.Models; using Loans.Services.Contracts; namespace Loans.Services { public class IdentityVerifierServiceGateway : IIdentityVerifier { public DateTime LastCheckTime { get; private set; } public void Initialize() { // Initialize connection to external service } public bool Validate(string applicantName, int applicantAge, string applicantAddress) { Connect(); var isValidIdentity = CallService(applicantName, applicantAge, applicantAddress); LastCheckTime = DateTime.Now; Disconnect(); return isValidIdentity; } private void Connect() { // Open connection to external service } private bool CallService(string applicantName, int applicantAge, string applicantAddress) { // Make call to external service, interpret the response, and return result return false; // Simulate result for demo purposes } private void Disconnect() { // Close connection to external service } public void Validate(string applicantName, int applicantAge, string applicantAddress, out bool isValid) { throw new NotImplementedException(); } public void Validate(string applicantName, int applicantAge, string applicantAddress, ref IdentityVerificationStatus status) { throw new NotImplementedException(); } } }
هدف از این برنامه، درخواست یک وام جدید است. Application در اینجا به معنای درخواست یا فرم جدید است و Applicant نیز شخصی است که این درخواست را دادهاست.
در اینجا بیشتر تمرکز ما بر روی کلاس LoanApplicationProcessor است که دارای دو وابستگی تزریق شدهی به آن نیز میباشد:
public LoanApplicationProcessor( IIdentityVerifier identityVerifier, ICreditScorer creditScorer) { _identityVerifier = identityVerifier ?? throw new ArgumentNullException(nameof(identityVerifier)); _creditScorer = creditScorer ?? throw new ArgumentNullException(nameof(creditScorer)); }
تمام این منطق نیز در متد Process آن قابل مشاهدهاست که هدف اصلی آن، بررسی قابل پذیرش بودن درخواست یک وام جدید است.
نوشتن اولین تست، برای برنامهی وام دهی
در اولین تصویر این قسمت، پروژهی class library دومی را نیز به نام Loans.Tests مشاهده میکنید. فایل csproj آن به صورت زیر برای کار با MSTest تنظیم شدهاست:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netcoreapp2.2</TargetFramework> </PropertyGroup> <ItemGroup> <ProjectReference Include="..\Loans\Loans.csproj" /> </ItemGroup> <ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" /> <PackageReference Include="MSTest.TestAdapter" Version="2.0.0" /> <PackageReference Include="MSTest.TestFramework" Version="2.0.0" /> </ItemGroup> </Project>
اکنون اولین آزمون واحد ما در کلاس جدید LoanApplicationProcessorShould چنین شکلی را پیدا میکند:
using Loans.Entities; using Loans.Services; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Loans.Tests { [TestClass] public class LoanApplicationProcessorShould { [TestMethod] public void DeclineLowSalary() { var product = new LoanProduct {Id = 99, ProductName = "Loan", InterestRate = 5.25m}; var amount = new LoanAmount {CurrencyCode = "Rial", Principal = 2_000_000_0}; var applicant = new Applicant {Id = 1, Name = "User 1", Age = 25, Address = "This place", Salary = 1_100_000_0}; var application = new LoanApplication {Id = 42, Product = product, Amount = amount, Applicant = applicant}; var processor = new LoanApplicationProcessor(null, null); processor.Process(application); Assert.IsFalse(application.IsAccepted); } } }
در این آزمایش، شخص درخواست کننده، حقوق کمی دارد و میخواهیم بررسی کنیم که آیا LoanApplicationProcessor میتواند آنرا بر اساس مقدار MinimumSalary، رد کند یا خیر؟
public class LoanApplicationProcessor { private const decimal MinimumSalary = 1_500_000_0;
در حین وهله سازی LoanApplicationProcessor، دو وابستگی آن به null تنظیم شدهاند؛ چون میدانیم که بررسی MinimumSalary پیش از سایر بررسیها صورت میگیرد و اساسا در این آزمایش، نیازی به این وابستگیها نداریم.
اما اگر سعی در اجرای این آزمایش کنیم (برای مثال با اجرای دستور dotnet test در خط فرمان)، آزمایش اجرا نشده و با استثنای زیر مواجه میشویم:
Test method Loans.Tests.LoanApplicationProcessorShould.DeclineLowSalary threw exception: System.ArgumentNullException: Value cannot be null. Parameter name: identityVerifier
public LoanApplicationProcessor( IIdentityVerifier identityVerifier, ICreditScorer creditScorer) { _identityVerifier = identityVerifier ?? throw new ArgumentNullException(nameof(identityVerifier)); _creditScorer = creditScorer ?? throw new ArgumentNullException(nameof(creditScorer)); }
نصب کتابخانهی Moq جهت برآورده کردن وابستگیهای کلاس LoanApplicationProcessor
در این آزمایش چون وجود وابستگیهای در سازندهی کلاس، برای ما اهمیتی ندارند و همچنین ذکر آنها نیز الزامی است، میخواهیم توسط کتابخانهی Moq، دو نمونهی تقلیدی از آنها را تهیه کرده (همان dummies که پیشتر معرفی شدند) و جهت برآورده کردن بررسی صورت گرفتهی در سازندهی کلاس LoanApplicationProcessor، آنها را ارائه کنیم.
کتابخانهی بسیار معروف Moq، با پروژههای مبتنی بر NETFramework 4.5. و همچنین NETStandard 2.0. به بعد سازگار است و برای نصب آن، میتوان یکی از دو دستور زیر را صادر کرد:
> dotnet add package Moq > Install-Package Moq
اما چرا کتابخانهی Moq؟
کتابخانهی Moq این اهداف را دنبال میکند: سادهاست، به شدت کاربردیاست و همچنین strongly typed است. این کتابخانه سورس باز بوده و تعداد بار دانلود بستهی نیوگت آن میلیونی است.
پس از نصب آن، اولین آزمایشی را که نوشتیم، به صورت زیر اصلاح میکنیم:
using Loans.Entities; using Loans.Services; using Loans.Services.Contracts; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; namespace Loans.Tests { [TestClass] public class LoanApplicationProcessorShould { [TestMethod] public void DeclineLowSalary() { var product = new LoanProduct {Id = 99, ProductName = "Loan", InterestRate = 5.25m}; var amount = new LoanAmount {CurrencyCode = "Rial", Principal = 2_000_000_0}; var applicant = new Applicant {Id = 1, Name = "User 1", Age = 25, Address = "This place", Salary = 1_100_000_0}; var application = new LoanApplication {Id = 42, Product = product, Amount = amount, Applicant = applicant}; var mockIdentityVerifier = new Mock<IIdentityVerifier>(); var mockCreditScorer = new Mock<ICreditScorer>(); var processor = new LoanApplicationProcessor(mockIdentityVerifier.Object, mockCreditScorer.Object); processor.Process(application); Assert.IsFalse(application.IsAccepted); } } }
کار با ذکر new Mock شروع شده و آرگومان جنریک آنرا از نوع وابستگیهایی که نیاز داریم، مقدار دهی میکنیم. سپس خاصیت Object آن، امکان دسترسی به این شیء تقلید شده را میسر میکند.
اکنون اگر مجددا این آزمون واحد را اجرا کنیم، مشاهده خواهیم کرد که بجای صدور استثناء، با موفقیت به پایان رسیدهاست:
گاهی از اوقات جایگزین کردن یک وابستگی null با نمونهی Mock آن کافی نیست
در مثالی که بررسی کردیم، اشیاء mock، کار برآورده کردن نیازهای ابتدایی آزمایش را انجام داده و سبب اجرای موفقیت آمیز آن شدند؛ اما همیشه اینطور نیست:
using Loans.Entities; using Loans.Services; using Loans.Services.Contracts; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; namespace Loans.Tests { [TestClass] public class LoanApplicationProcessorShould { [TestMethod] public void Accept() { var product = new LoanProduct {Id = 99, ProductName = "Loan", InterestRate = 5.25m}; var amount = new LoanAmount {CurrencyCode = "Rial", Principal = 2_000_000_0}; var applicant = new Applicant {Id = 1, Name = "User 1", Age = 25, Address = "This place", Salary = 1_500_000_0}; var application = new LoanApplication {Id = 42, Product = product, Amount = amount, Applicant = applicant}; var mockIdentityVerifier = new Mock<IIdentityVerifier>(); var mockCreditScorer = new Mock<ICreditScorer>(); var processor = new LoanApplicationProcessor(mockIdentityVerifier.Object, mockCreditScorer.Object); processor.Process(application); Assert.IsTrue(application.IsAccepted); } } }
اگر این آزمایش را اجرا کنیم، با شکست مواجه خواهد شد. علت اینجا است که هرچند در حال استفادهی از دو mock object به عنوان وابستگیهای مورد نیاز هستیم، اما تنظیمات خاصی را بر روی آنها انجام ندادهایم و به همین جهت خروجی مناسبی را در اختیار LoanApplicationProcessor قرار نمیدهند. برای مثال مرحلهی بعدی بررسی اعتبار شخص در کلاس LoanApplicationProcessor، فراخوانی سرویس identityVerifier و متد Validate آن است که خروجی آن بر اساس کدهای فعلی، همیشه false است:
_identityVerifier.Initialize(); var isValidIdentity = _identityVerifier.Validate( application.Applicant.Name, application.Applicant.Age, application.Applicant.Address);
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MoqSeries-01.zip
SIMD مخفف «Single Instruction, Multiple Data» است و متشکل است از تعدادی instruction پردازندهها که بجای مقادیر عددی، بر روی بردارها کار میکنند. به این ترتیب امکان کار موازی بر روی مقادیر عددی، در سطح CPU میسر میشود. برای نمونه به تصویر ذیل دقت کنید:
در اینجا قرار است تک تک عناصر آرایهای از اعداد، با عدد 6 جمع شوند. روش متداول آن به این صورت است که حلقهای تشکیل شده و سپس تک تک عناصر این آرایه دریافت و با عدد 6 جمع میشوند. اما در حالت استفادهی از SIMD، هربار گروهی از عناصر این آرایه به صورت یک بردار درنظر گرفته میشوند (Multiple Data) و سپس با برداری حاوی مقدار 6 جمع میشوند (Single Instruction). اینبار این عملیات به صورت موازی، بر روی گروهی از اعداد انجام میشود و به همین دلیل نسبت به حالت کار بر روی یک المان از آرایه در هر مرحله، سرعت بیشتری دارد.
تفاوت چندریسمانی با SIMD چیست؟
شاید عنوان کنید که با وجود امکانات چندریسمانی چه نیازی به SIMD است؟ در حالت پردازشهای چند ریسمانی، یک یا چند کار بر روی چندین هستهی CPU به صورت موازی پردازش میشوند، اما SIMD امکان پردازش موازی را در یک هستهی CPU میسر میکند.
آیا CPU من از SIMD پشتیبانی میکند؟
SIMD instruction sets شامل افزونههای ذیل است:
اگر CPU شما حداقل یکی از این قابلیتها را داشته باشد، امکان استفادهی از SIMD را دارید. برای مشخص سازی آن نیز میتوانید از برنامهی معروف CPU-Z استفاده کنید:
در این برنامه، در برگهی CPU آن به قسمت instructions آن دقت کنید و موارد لیست شدهی در آن را با افزونههای فوق مقایسه نمائید.
پشتیبانی از SIMD در دات نت
با ارائهی دات نت 4.6 و RyuJIT جدید آن، امکان کار با دستورات SIMD در فضای نام System.Numerics.Vectors پیش بینی شدهاست. برای کار با آن باید بستهی نیوگت زیر را نصب کنید:
در ابتدای کار باید بررسی کنید که آیا سخت افزار شما از SIMD پشتیبانی میکند یا خیر. خاصیت Vector.IsHardwareAccelerated بیانگر این موضوع است. اما ... این خاصیت در حال دیباگ ممکن است مساوی false باشد. برای استفادهی از SIMD ، طی این مراحل ضروری است:
الف) نصب دات نت 4.6.x (دریافت دات نت 4.6.1 مخصوص یکپارچه شدن با ویژوال استودیو)
ب) به خواص پروژهی جاری مراجعه کرده و platform target را بر روی x64 قرار دهید. باید دقت داشت که RyuJIT جدید، برای سیستمهای 64 بیتی طراحی شدهاست.
ج) RyuJIT، در حالت release و انتخاب گزینهی optimize code (در همان برگهی خواص پروژه) است که کدهای ویژهی SIMD را تولید میکند.
د) نصب بستهی نیوگت System.Numerics.Vectors
در کل اگر برنامه را داخل دیباگر VS.NET اجرا کنید، مقدار Vector.IsHardwareAccelerated مساوی false خواهد بود. به همین جهت برنامه را در حالت release و 64 بیتی کامپایل کرده و خارج از محیط VS.NET اجرا کنید.
بررسی فضای نام جدید System.Numerics.Vectors
پشتیبانی از SIMD در دات نت به این معنا نیست که هر نوع کدی توسط RyuJIT به صورت خودکار تبدیل به SIMD instruction sets خواهد شد. برای این منظور نیاز است از نوعهای دادهای خاصی به همراه متدهای مرتبط با آنها استفاده کرد.
سری اول این نوعهای جدید برداری، به شرح زیر هستند:
کلاسهای وکتور 2، 3 و 4، بردارهایی از نوع float را با اندازههایی ثابت تعریف میکنند و بر روی 128bit SIMD registers کار میکنند. بر روی این کلاسها، با توجه به operators overloading صورت گرفته، امکان جمع، منها، ضرب و تقسیم نیز وجود دارد و یا میتوان از متدهای متناظر موجود در کلاسهای آنها استفاده کرد. نمونهای از این عملیات را در مثالهای ذیل مشاهده میکنید:
در مثال آخر مطرح شده، روشی کپی و تبدیل یک بردار، به یک آرایهی هم نوع آن، ارائه شدهاست.
علاوه بر اعمال متداول ریاضی، هر کدام از کلاسهای Vector دارای متدهای اضافی ویژهای مانند محاسبهی حداقل، حداکثر، جذر و غیره نیز میباشند:
برای مثال متد Max در اینجا به MAXPS instruction مخصوص پردازشگر ترجمه میشود.
سری دوم بردارهای قابل تعریف، از نوع <Vector<T هستند. برای مثال CPUهایی که از SSE2 پشتیبانی میکنند، قابلیت کار با نوعهای دادهای زیر را نیز دارا هستند:
برای نمونه همان مثال ابتدای بحث را در نظر بگیرید. نسخهی متداول انجام افزودن مقداری به تک تک اعضای یک آرایه به صورت زیر است:
بازنویسی این متد برای کار با SIMD به صورت ذیل خواهد بود:
در اینجا یک Vector از نوع int تعریف شده و سپس بجای تشکیل یک حلقه، فقط کافی است بردار دیگری را حاوی عدد مشخص شده، به آن اضافه کنیم. در پایان برای تبدیل این بردار به آرایهای از نوع int (در صورت نیاز) میتوان از متد Copy استفاده کرد.
در مثال ذیل، نحوهی انتخاب Multiple data (گروهی از اعداد، بجای تک عدد) و سپس اعمال یک تک instruction را ملاحظه میکنید:
در مثال فوق قصد داریم جذر تک تک عناصر آرایهای را محاسبه کرده و سپس در آرایهی دومی ثبت کنیم. بجای روش متداول مراجعهی به تک تک عناصر آرایهی ورودی، اینبار با استفاده از کلاس بردار، به اندازهی طول بردار float، اطلاعات را در vectorIn ذخیره کرده و سپس به صورت یکجا به تک متد SquareRoot ارسال میکنیم. این متد در سمت CPU به معادل SQRTPS instruction ترجمه میشود و تنها یک instruction است.
یک مثال تکمیلی
در اینجا قرار است تک تک عناصر آرایهای از اعداد، با عدد 6 جمع شوند. روش متداول آن به این صورت است که حلقهای تشکیل شده و سپس تک تک عناصر این آرایه دریافت و با عدد 6 جمع میشوند. اما در حالت استفادهی از SIMD، هربار گروهی از عناصر این آرایه به صورت یک بردار درنظر گرفته میشوند (Multiple Data) و سپس با برداری حاوی مقدار 6 جمع میشوند (Single Instruction). اینبار این عملیات به صورت موازی، بر روی گروهی از اعداد انجام میشود و به همین دلیل نسبت به حالت کار بر روی یک المان از آرایه در هر مرحله، سرعت بیشتری دارد.
تفاوت چندریسمانی با SIMD چیست؟
شاید عنوان کنید که با وجود امکانات چندریسمانی چه نیازی به SIMD است؟ در حالت پردازشهای چند ریسمانی، یک یا چند کار بر روی چندین هستهی CPU به صورت موازی پردازش میشوند، اما SIMD امکان پردازش موازی را در یک هستهی CPU میسر میکند.
آیا CPU من از SIMD پشتیبانی میکند؟
SIMD instruction sets شامل افزونههای ذیل است:
• MMX - MultiMedia eXtensions • SSE - Streaming SIMD Extensions • SSE2 - Streaming SIMD Extensions 2 • SSE3 - Streaming SIMD Extensions 3 • SSSE3 - Supplemental Streaming SIMD Extensions 3 • SSE4.1 - Streaming SIMD Extensions 4.1 • SSE4.2 - Streaming SIMD Extensions 4.2 • AES-NI - Advanced Encryption Standard New Instructions • AVX - Advanced Vector eXtensions
در این برنامه، در برگهی CPU آن به قسمت instructions آن دقت کنید و موارد لیست شدهی در آن را با افزونههای فوق مقایسه نمائید.
پشتیبانی از SIMD در دات نت
با ارائهی دات نت 4.6 و RyuJIT جدید آن، امکان کار با دستورات SIMD در فضای نام System.Numerics.Vectors پیش بینی شدهاست. برای کار با آن باید بستهی نیوگت زیر را نصب کنید:
PM> Install-Package System.Numerics.Vectors
الف) نصب دات نت 4.6.x (دریافت دات نت 4.6.1 مخصوص یکپارچه شدن با ویژوال استودیو)
ب) به خواص پروژهی جاری مراجعه کرده و platform target را بر روی x64 قرار دهید. باید دقت داشت که RyuJIT جدید، برای سیستمهای 64 بیتی طراحی شدهاست.
ج) RyuJIT، در حالت release و انتخاب گزینهی optimize code (در همان برگهی خواص پروژه) است که کدهای ویژهی SIMD را تولید میکند.
د) نصب بستهی نیوگت System.Numerics.Vectors
در کل اگر برنامه را داخل دیباگر VS.NET اجرا کنید، مقدار Vector.IsHardwareAccelerated مساوی false خواهد بود. به همین جهت برنامه را در حالت release و 64 بیتی کامپایل کرده و خارج از محیط VS.NET اجرا کنید.
بررسی فضای نام جدید System.Numerics.Vectors
پشتیبانی از SIMD در دات نت به این معنا نیست که هر نوع کدی توسط RyuJIT به صورت خودکار تبدیل به SIMD instruction sets خواهد شد. برای این منظور نیاز است از نوعهای دادهای خاصی به همراه متدهای مرتبط با آنها استفاده کرد.
سری اول این نوعهای جدید برداری، به شرح زیر هستند:
var vector01 = new Vector2(x: 5F, y: 15F); var vector11 = new Vector3(x: 5F, y: 15F, z: 25F); var vector12 = new Vector3(x: 3F, y: 5F, z: 8F); var vector13 = new Vector4(x: 3F, y: 5F, z: 8F, w:1F);
var vector3 = vector11 - vector12; //استفاده از سربارگذاری عملگرها var vector4 = Vector3.Subtract(vector12, vector11);//ویا استفاده از متدهای متناظر vector3 = vector11 * vector12; vector4 = Vector3.Multiply(vector11, vector12); vector3 = vector11 / vector12; vector4 = Vector3.Divide(vector11, vector12); vector3 = vector11 + vector12; vector4 = Vector3.Add(vector11, vector12); var areEqual = (vector11 == vector12); var areNotEqual = (vector11 != vector12); var array = new float[3]; vector11.CopyTo(array);
علاوه بر اعمال متداول ریاضی، هر کدام از کلاسهای Vector دارای متدهای اضافی ویژهای مانند محاسبهی حداقل، حداکثر، جذر و غیره نیز میباشند:
vector3 = Vector3.Max(vector11, vector12); vector3 = Vector3.Min(vector11, vector12); vector3 = Vector3.SquareRoot(vector11); vector3 = Vector3.Abs(vector11); var dotProduct = Vector3.Dot(vector11, vector12);
سری دوم بردارهای قابل تعریف، از نوع <Vector<T هستند. برای مثال CPUهایی که از SSE2 پشتیبانی میکنند، قابلیت کار با نوعهای دادهای زیر را نیز دارا هستند:
Vector<double>.Length: 2 Vector<int>.Length: 4 Vector<long>.Length: 2 Vector<float>.Length: 4
private static int[] simpleIncrement(int[] values, int inc) { var results = new int[values.Length]; for (var i = 0; i < results.Length; i++) { results[i] = values[i] + inc; } return results; }
private static int[] simdIncrement(int[] values, int inc) { var vector = new Vector<int>(values); var vectorAddResults = vector + new Vector<int>(inc); var results = new int[values.Length]; vectorAddResults.CopyTo(results); return results; }
در مثال ذیل، نحوهی انتخاب Multiple data (گروهی از اعداد، بجای تک عدد) و سپس اعمال یک تک instruction را ملاحظه میکنید:
var valuesIn = new float[] { 4f, 16f, 36f, 64f, 9f, 81f, 49f, 25f, 100f, 121f, 144f, 16f, 36f, 4f, 9f, 81f }; var valuesOut = new float[valuesIn.Length]; for (var i = 0; i < valuesIn.Length; i += Vector<float>.Count) { var vectorIn = new Vector<float>(valuesIn, i); var vectorOut = Vector.SquareRoot(vectorIn); vectorOut.CopyTo(valuesOut, i); }
یک مثال تکمیلی
مطالب
Roslyn #3
بررسی Syntax tree
زمانیکه صحبت از Syntax میشود، منظور نمایش متنی سورس کدها است. برای بررسی و آنالیز آن، نیاز است این نمایش متنی، به ساختار دادهای ویژهای به نام Syntax tree تبدیل شود و این Syntax tree مجموعهای است از tokenها. Tokenها بیانگر المانهای مختلف یک زبان، شامل کلمات کلیدی، عملگرها و غیره هستند.
در تصویر فوق، مراحل تبدیل یک قطعه کد #C را به مجموعهای از tokenهای معادل آن مشاهده میکنید. علاوه بر اینها، Roslyn syntax tree شامل موارد ویژهای به نام Trivia نیز هست. برای مثال در حین نوشتن کدها، در ابتدای سطرها تعدادی space یا tab وجود دارند و یا در این بین ممکن است کامنتی نوشته شود. هرچند این موارد از دیدگاه یک کامپایلر بیمعنا هستند، اما ابزارهای Refactoring ایی که به Trivia دقت نداشته باشند، خروجی کد به هم ریختهای را تولید خواهند کرد و سبب سردرگمی استفاده کنندگان میشوند.
در تصویر فوق، اشارهگر ادیتور پس از تایپ semicolon قرار گرفتهاست. در این حالت میتوانید دو نوع trivia مخصوص فضای خالی و کامنتها را در syntax visualizer، مشاهده کنید.
به علاوه پس از هر token بازهای از اعداد را مشاهده میکنید که بیانگر محل قرارگیری آنها در سورس کد هستند. این محلها جهت ارائهی خطاهای دقیق مرتبط با آن نقاط، بسیار مفید هستند.
یک Syntax tree از مجموعهای از syntax nodes تشکیل میشود و هر node شامل مواردی مانند تعاریف، عبارات و امثال آن است. در افزونهی Syntax visualizer نودهایی که رنگ قرمز متمایل به قهوهای دارند، بیانگر نودهای Trivia، نودهای آبی، Syntax nodes و نودهای سبز، Syntax token هستند.
مفاهیم این رنگها را با کلیک بر روی دکمهی Legend هم میتوان مشاهده کرد.
تفاوت Syntax با Semantics
در Roslyn امکان کار با Syntax و Semantics کدها وجود دارد.
یک Syntax، از گرامر زبان خاصی پیروی میکند. در Syntax اطلاعات بسیار زیادی وجود دارند که معنای برنامه را تغییر نمیدهند؛ مانند کامنتها، فضاهای خالی و فرمت ویژهی کدها. البته فضاهای خالی در زبانهایی مانند پایتون دارای معنا هستند؛ اما در سیشارپ خیر. همچنین در Syntax، توافق نامهای وجود دارد که بیانگر تعدادی واژهی از پیش رزرو شده، مانند کلمات کلیدی هستند.
اما Semantics در نقطهی مقابل Syntax قرار میگیرد و بیانگر معنای سورس کد است. برای مثال در اینجا تقدم و تاخر عملگرها مفهوم پیدا میکنند و یا اینکه Type system چیست و چه نوعهایی را میتوان به دیگری نسبت داد و تبدیل کرد. عملیات Binding در این مرحله رخ میدهد و مفهوم identifierها را مشخص میکند. برای مثال x در این قسمت از سورس کد، به چه معنایی است و به کجا اشاره میکند؟
خواص ویژهی Syntax tree در Roslyn
- تمام اجزای کد را شامل عناصر سازندهی زبان و همچنین Trivia، به همراه دارد.
- API آن توسط کتابخانههای ثالث قابل دسترسی است.
- Immutable طراحی شدهاست. به این معنا که زمانیکه syntax tree توسط Roslyn ایجاد شد، دیگر تغییر نمیکند. به این ترتیب امکان دسترسی همزمان و موازی به آن بدون نیاز به انواع قفلهای مسایل همزمانی وجود دارد. اگر کتابخانهی ثالثی به Syntax tree ارائه شده دسترسی پیدا میکند، میتواند کاملا مطمئن باشد که این اطلاعات دیگر تغییری نمیکنند و نیازی به قفل کردن آنها نیست. همچنین این مساله امکان استفادهی مجدد از sub treeها را در حین ویرایش کدها میسر میکند. به آنها mutating trees نیز گفته میشود.
- مقاوم است در برابر خطاها. اگر از قسمت اول به خاطر داشته باشید، Roslyn میبایستی جایگزین کامپایلر دومی به نام کامپایلر پس زمینهی ویژوال استودیو که خطوط قرمزی را ذیل سطرهای مشکل دار ترسیم میکند، نیز میشد. فلسفهی طراحی این کامپایلر، مقاوم بودن در برابر خطاهای تایپی و هماهنگی آن با تایپ کدها توسط برنامه نویس بود. Syntax tree در Roslyn نیز چنین خاصیتی را دارد و اگر مشغول به تایپ شوید، باز هم کار کرده و اینبار خطاهای موجود را نمایش میدهد که میتواند توسط ابزارهای نمایش دهندهی ویژوال استودیو یا سایر ابزارهای ثالث استفاده شود.
برای نمونه در تصویر فوق، تایپ semicolon فراموش شدهاست؛ اما همچنان Syntax tree در دسترس است و به علاوه گزارش میدهد که semicolon مفقود است و تایپ نشدهاست.
Parse سورس کد توسط Roslyn
ابتدا یک پروژهی کنسول سادهی دات نت 4.6 را در VS 2015 آغاز کنید. سپس از طریق خط فرمان نیوگت، دستور ذیل را صادر نمائید:
به این ترتیب API لازم جهت کار با Roslyn به پروژه اضافه خواهند شد.
سپس کدهای ذیل را به آن اضافه کنید:
توضیحات:
کار Parse سورس کد دریافتی، بر اساس سرویسهای زبان متناظر با آنها آغاز میشود. برای مثال سرویسهایی مانند VisualBasicSyntaxTree و یا CSharpSyntaxTree مثال فوق که سورس کد مورد آنالیز آن، از نوع سیشارپ است.
این کلاسهای Factory، دارای دو متد Create و ParseText هستند. کار متد ParseText آن مشخص است؛ یک قطعهی متنی از کد را آنالیز کرده و معادل Syntax Tree آنرا تولید میکند. متد Create آن، اشیایی مانند نودهای Syntax visualizer را دریافت کرده و بر اساس آنها یک Syntax tree را تولید میکند.
کار با متد Create آنچنان ساده نیست. به همین جهت یکی از اعضای تیم Roslyn برنامهای را به نام Roslyn Quoter ایجاد کردهاست که نسخهی آنلاین آنرا در اینجا و سورس کد آنرا در اینجا میتوانید بررسی کنید.
جهت آزمایش، همان قطعهی متنی سورس کد مثال فوق را در نسخهی آنلاین آن جهت آنالیز و تولید ورودی متد Create، وارد کنید. خروجی آنرا میتوان مستقیما در متد Create بکار برد.
فرمت کردن خودکار کدها به کمک Roslyn
اگر بر روی tree حاصل، متد ToString را فراخوانی کنیم، خروجی آن مجددا سورس کد مورد آنالیز است. اگر علاقمند بودید که Roslyn به صورت خودکار کدهای ورودی را فرمت کند و تمام آنها را در یک سطر نمایش ندهد، متد NormalizeWhitespace را بر روی ریشهی Syntax tree فراخوانی کنید:
اینبار خروجی فراخوانی فوق به صورت ذیل است:
کوئری گرفتن از سورس کد توسط Roslyn
در ادامه قصد داریم با سه روش مختلف کوئری گرفتن از Syntax tree، آشنا شویم. برای این منظور متد ذیل را به پروژهای که در ابتدای برنامه آغاز کردیم، اضافه کنید:
توضیحات:
روش اول کوئری گرفتن از Syntax tree، استفاده از object model آن است. در اینجا هربار، نوع و Kind هر نود را بررسی کرده و در نهایت به اجزای مدنظر خواهیم رسید. شروع کار هم با دریافت ریشهی syntax tree توسط متد GetRoot و تبدیل نوع آن نود به CompilationUnitSyntax میباشد.
روش دوم استفاده از روش LINQ است؛ با توجه به اینکه ساختار یک Syntax tree بسیار شبیه است به LINQ to XML. در اینجا یک سری نود، ریشه و فرزندان آنها را داریم که با روش LINQ بسیار سازگار هستند. برای نمونه در مثال فوق، در ریشهی Parse شده، در تمام کلاسهای آن، به دنبال متد یا متدهایی هستیم که نام آنها Bar است.
و در نهایت روش مرسوم و متداول کار با Syntax trees، استفاده از الگوی Visitors است. همانطور که در کدهای دو روش قبل مشاهده میکنید، باید تعداد زیادی حلقه و if و else نوشت تا به جزء و المان مدنظر رسید. راه سادهتری نیز برای مدیریت این پیچیدگی وجود دارد و آن استفاده از الگوی Visitor است. کار این الگو ارائهی متدهایی قابل override شدن است و فراخوانی آنها، در طی حلقههایی پشت صحنه که این Visitor را اجرا میکنند، صورت میگیرد. بنابراین در اینجا دیگر برای رسیدن به یک متد، حلقه نخواهید نوشت. تنها کاری که باید صورت گیرد، override کردن متد Visit المانی خاص در Syntax tree است.
هر نود در syntax tree دارای متدی است به نام Accept که یک Visitor را دریافت میکند. همچنین Visitorهای نوشته شده نیز دارای متد Visit یک نود هستند.
نمونهای از این Visitors را در کلاس ذیل مشاهده میکنید:
در اینجا برای رسیدن به تعاریف متدها دیگر نیازی نیست تا حلقه نوشت. بازنویسی متد VisitMethodDeclaration، دقیقا همین کار را انجام میدهد و در طی پروسهی Visit یک Syntax tree، اگر متدی در آن تعریف شده باشد، متد VisitMethodDeclaration حداقل یکبار فراخوانی خواهد شد.
کلاس پایهی CSharpSyntaxWalker از کلاس CSharpSyntaxVisitor مشتق شدهاست و به تمام امکانات آن دسترسی دارد. علاوه بر آنها، کلاس CSharpSyntaxWalker به Tokens و Trivia نیز دسترسی دارد.
نحوهی استفاده از Visitor سفارشی نوشته شده نیز به صورت ذیل است:
در اینجا متد Visit این Visitor را بر روی نود ریشهی Syntax tree اجرا کردهایم.
زمانیکه صحبت از Syntax میشود، منظور نمایش متنی سورس کدها است. برای بررسی و آنالیز آن، نیاز است این نمایش متنی، به ساختار دادهای ویژهای به نام Syntax tree تبدیل شود و این Syntax tree مجموعهای است از tokenها. Tokenها بیانگر المانهای مختلف یک زبان، شامل کلمات کلیدی، عملگرها و غیره هستند.
در تصویر فوق، مراحل تبدیل یک قطعه کد #C را به مجموعهای از tokenهای معادل آن مشاهده میکنید. علاوه بر اینها، Roslyn syntax tree شامل موارد ویژهای به نام Trivia نیز هست. برای مثال در حین نوشتن کدها، در ابتدای سطرها تعدادی space یا tab وجود دارند و یا در این بین ممکن است کامنتی نوشته شود. هرچند این موارد از دیدگاه یک کامپایلر بیمعنا هستند، اما ابزارهای Refactoring ایی که به Trivia دقت نداشته باشند، خروجی کد به هم ریختهای را تولید خواهند کرد و سبب سردرگمی استفاده کنندگان میشوند.
در تصویر فوق، اشارهگر ادیتور پس از تایپ semicolon قرار گرفتهاست. در این حالت میتوانید دو نوع trivia مخصوص فضای خالی و کامنتها را در syntax visualizer، مشاهده کنید.
به علاوه پس از هر token بازهای از اعداد را مشاهده میکنید که بیانگر محل قرارگیری آنها در سورس کد هستند. این محلها جهت ارائهی خطاهای دقیق مرتبط با آن نقاط، بسیار مفید هستند.
یک Syntax tree از مجموعهای از syntax nodes تشکیل میشود و هر node شامل مواردی مانند تعاریف، عبارات و امثال آن است. در افزونهی Syntax visualizer نودهایی که رنگ قرمز متمایل به قهوهای دارند، بیانگر نودهای Trivia، نودهای آبی، Syntax nodes و نودهای سبز، Syntax token هستند.
مفاهیم این رنگها را با کلیک بر روی دکمهی Legend هم میتوان مشاهده کرد.
تفاوت Syntax با Semantics
در Roslyn امکان کار با Syntax و Semantics کدها وجود دارد.
یک Syntax، از گرامر زبان خاصی پیروی میکند. در Syntax اطلاعات بسیار زیادی وجود دارند که معنای برنامه را تغییر نمیدهند؛ مانند کامنتها، فضاهای خالی و فرمت ویژهی کدها. البته فضاهای خالی در زبانهایی مانند پایتون دارای معنا هستند؛ اما در سیشارپ خیر. همچنین در Syntax، توافق نامهای وجود دارد که بیانگر تعدادی واژهی از پیش رزرو شده، مانند کلمات کلیدی هستند.
اما Semantics در نقطهی مقابل Syntax قرار میگیرد و بیانگر معنای سورس کد است. برای مثال در اینجا تقدم و تاخر عملگرها مفهوم پیدا میکنند و یا اینکه Type system چیست و چه نوعهایی را میتوان به دیگری نسبت داد و تبدیل کرد. عملیات Binding در این مرحله رخ میدهد و مفهوم identifierها را مشخص میکند. برای مثال x در این قسمت از سورس کد، به چه معنایی است و به کجا اشاره میکند؟
خواص ویژهی Syntax tree در Roslyn
- تمام اجزای کد را شامل عناصر سازندهی زبان و همچنین Trivia، به همراه دارد.
- API آن توسط کتابخانههای ثالث قابل دسترسی است.
- Immutable طراحی شدهاست. به این معنا که زمانیکه syntax tree توسط Roslyn ایجاد شد، دیگر تغییر نمیکند. به این ترتیب امکان دسترسی همزمان و موازی به آن بدون نیاز به انواع قفلهای مسایل همزمانی وجود دارد. اگر کتابخانهی ثالثی به Syntax tree ارائه شده دسترسی پیدا میکند، میتواند کاملا مطمئن باشد که این اطلاعات دیگر تغییری نمیکنند و نیازی به قفل کردن آنها نیست. همچنین این مساله امکان استفادهی مجدد از sub treeها را در حین ویرایش کدها میسر میکند. به آنها mutating trees نیز گفته میشود.
- مقاوم است در برابر خطاها. اگر از قسمت اول به خاطر داشته باشید، Roslyn میبایستی جایگزین کامپایلر دومی به نام کامپایلر پس زمینهی ویژوال استودیو که خطوط قرمزی را ذیل سطرهای مشکل دار ترسیم میکند، نیز میشد. فلسفهی طراحی این کامپایلر، مقاوم بودن در برابر خطاهای تایپی و هماهنگی آن با تایپ کدها توسط برنامه نویس بود. Syntax tree در Roslyn نیز چنین خاصیتی را دارد و اگر مشغول به تایپ شوید، باز هم کار کرده و اینبار خطاهای موجود را نمایش میدهد که میتواند توسط ابزارهای نمایش دهندهی ویژوال استودیو یا سایر ابزارهای ثالث استفاده شود.
برای نمونه در تصویر فوق، تایپ semicolon فراموش شدهاست؛ اما همچنان Syntax tree در دسترس است و به علاوه گزارش میدهد که semicolon مفقود است و تایپ نشدهاست.
Parse سورس کد توسط Roslyn
ابتدا یک پروژهی کنسول سادهی دات نت 4.6 را در VS 2015 آغاز کنید. سپس از طریق خط فرمان نیوگت، دستور ذیل را صادر نمائید:
PM> Install-Package Microsoft.CodeAnalysis
سپس کدهای ذیل را به آن اضافه کنید:
using System; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Roslyn01 { class Program { static void Main(string[] args) { parseText(); } static void parseText() { var tree = CSharpSyntaxTree.ParseText("class Foo { void Bar(int x) {} }"); Console.WriteLine(tree.ToString()); Console.WriteLine(tree.GetRoot().NormalizeWhitespace().ToString()); var res = SyntaxFactory.ClassDeclaration("Foo") .WithMembers(SyntaxFactory.List<MemberDeclarationSyntax>(new[] { SyntaxFactory.MethodDeclaration( SyntaxFactory.PredefinedType( SyntaxFactory.Token(SyntaxKind.VoidKeyword) ), "Bar" ) .WithBody(SyntaxFactory.Block()) })) .NormalizeWhitespace(); Console.WriteLine(res); } } }
کار Parse سورس کد دریافتی، بر اساس سرویسهای زبان متناظر با آنها آغاز میشود. برای مثال سرویسهایی مانند VisualBasicSyntaxTree و یا CSharpSyntaxTree مثال فوق که سورس کد مورد آنالیز آن، از نوع سیشارپ است.
این کلاسهای Factory، دارای دو متد Create و ParseText هستند. کار متد ParseText آن مشخص است؛ یک قطعهی متنی از کد را آنالیز کرده و معادل Syntax Tree آنرا تولید میکند. متد Create آن، اشیایی مانند نودهای Syntax visualizer را دریافت کرده و بر اساس آنها یک Syntax tree را تولید میکند.
کار با متد Create آنچنان ساده نیست. به همین جهت یکی از اعضای تیم Roslyn برنامهای را به نام Roslyn Quoter ایجاد کردهاست که نسخهی آنلاین آنرا در اینجا و سورس کد آنرا در اینجا میتوانید بررسی کنید.
جهت آزمایش، همان قطعهی متنی سورس کد مثال فوق را در نسخهی آنلاین آن جهت آنالیز و تولید ورودی متد Create، وارد کنید. خروجی آنرا میتوان مستقیما در متد Create بکار برد.
فرمت کردن خودکار کدها به کمک Roslyn
اگر بر روی tree حاصل، متد ToString را فراخوانی کنیم، خروجی آن مجددا سورس کد مورد آنالیز است. اگر علاقمند بودید که Roslyn به صورت خودکار کدهای ورودی را فرمت کند و تمام آنها را در یک سطر نمایش ندهد، متد NormalizeWhitespace را بر روی ریشهی Syntax tree فراخوانی کنید:
tree.GetRoot().NormalizeWhitespace().ToString()
class Foo { void Bar(int x) { } }
کوئری گرفتن از سورس کد توسط Roslyn
در ادامه قصد داریم با سه روش مختلف کوئری گرفتن از Syntax tree، آشنا شویم. برای این منظور متد ذیل را به پروژهای که در ابتدای برنامه آغاز کردیم، اضافه کنید:
static void querySyntaxTree() { var tree = CSharpSyntaxTree.ParseText("class Foo { void Bar() {} }"); var node = (CompilationUnitSyntax)tree.GetRoot(); // Using the object model foreach (var member in node.Members) { if (member.Kind() == SyntaxKind.ClassDeclaration) { var @class = (ClassDeclarationSyntax)member; foreach (var member2 in @class.Members) { if (member2.Kind() == SyntaxKind.MethodDeclaration) { var method = (MethodDeclarationSyntax)member2; // do stuff } } } } // Using LINQ query methods var bars = from member in node.Members.OfType<ClassDeclarationSyntax>() from member2 in member.Members.OfType<MethodDeclarationSyntax>() where member2.Identifier.Text == "Bar" select member2; var res = bars.ToList(); // Using visitors new MyVisitor().Visit(node); }
روش اول کوئری گرفتن از Syntax tree، استفاده از object model آن است. در اینجا هربار، نوع و Kind هر نود را بررسی کرده و در نهایت به اجزای مدنظر خواهیم رسید. شروع کار هم با دریافت ریشهی syntax tree توسط متد GetRoot و تبدیل نوع آن نود به CompilationUnitSyntax میباشد.
روش دوم استفاده از روش LINQ است؛ با توجه به اینکه ساختار یک Syntax tree بسیار شبیه است به LINQ to XML. در اینجا یک سری نود، ریشه و فرزندان آنها را داریم که با روش LINQ بسیار سازگار هستند. برای نمونه در مثال فوق، در ریشهی Parse شده، در تمام کلاسهای آن، به دنبال متد یا متدهایی هستیم که نام آنها Bar است.
و در نهایت روش مرسوم و متداول کار با Syntax trees، استفاده از الگوی Visitors است. همانطور که در کدهای دو روش قبل مشاهده میکنید، باید تعداد زیادی حلقه و if و else نوشت تا به جزء و المان مدنظر رسید. راه سادهتری نیز برای مدیریت این پیچیدگی وجود دارد و آن استفاده از الگوی Visitor است. کار این الگو ارائهی متدهایی قابل override شدن است و فراخوانی آنها، در طی حلقههایی پشت صحنه که این Visitor را اجرا میکنند، صورت میگیرد. بنابراین در اینجا دیگر برای رسیدن به یک متد، حلقه نخواهید نوشت. تنها کاری که باید صورت گیرد، override کردن متد Visit المانی خاص در Syntax tree است.
هر نود در syntax tree دارای متدی است به نام Accept که یک Visitor را دریافت میکند. همچنین Visitorهای نوشته شده نیز دارای متد Visit یک نود هستند.
نمونهای از این Visitors را در کلاس ذیل مشاهده میکنید:
class MyVisitor : CSharpSyntaxWalker { public override void VisitMethodDeclaration(MethodDeclarationSyntax node) { if (node.Identifier.Text == "Bar") { // do stuff } base.VisitMethodDeclaration(node); } }
کلاس پایهی CSharpSyntaxWalker از کلاس CSharpSyntaxVisitor مشتق شدهاست و به تمام امکانات آن دسترسی دارد. علاوه بر آنها، کلاس CSharpSyntaxWalker به Tokens و Trivia نیز دسترسی دارد.
نحوهی استفاده از Visitor سفارشی نوشته شده نیز به صورت ذیل است:
new MyVisitor().Visit(node);
خلاصهای را در مورد SQL Server CE قبلا در این سایت مطالعه کردهاید. در ادامه خلاصهای کاربردی را از تنظیمات و نکات مرتبط به کار با SQL-CE به کمک NHibernate ملاحظه خواهید نمود:
1) دریافت SQL-CE 4.0
همین مقدار برای استفاده از SQL-CE 4.0 به کمک NHibernate کفایت میکند و حتی نیازی به نصب سرویس پک یک VS 2010 هم نیست.
2) ابزار سازی جهت ایجاد یک بانک اطلاعاتی خالی SQL-CE
using System;
using System.IO;
namespace NHibernate.Helper.DbSpecific
{
public class SqlCEDbHelper
{
const string engineTypeName = "System.Data.SqlServerCe.SqlCeEngine, System.Data.SqlServerCe";
/// <summary>
/// note: this method will delete existing db and then creates a new one.
/// </summary>
/// <param name="filename"></param>
/// <param name="password"></param>
public static void CreateEmptyDatabaseFile(string filename, string password = "")
{
if (File.Exists(filename))
File.Delete(filename);
var type = System.Type.GetType(engineTypeName);
var localConnectionString = type.GetProperty("LocalConnectionString");
var createDatabase = type.GetMethod("CreateDatabase");
var engine = Activator.CreateInstance(type);
string connectionStr = string.Format("Data Source='{0}';Password={1};Encrypt Database=True", filename, password);
if (string.IsNullOrWhiteSpace(password))
connectionStr = string.Format("Data Source='{0}'", filename);
localConnectionString.SetValue(
obj: engine,
value: connectionStr,
index: null);
createDatabase.Invoke(engine, new object[0]);
}
/// <summary>
/// use this method to compact or encrypt existing db or decrypt it to a new db with all records
/// </summary>
/// <param name="sourceConnection"></param>
/// <param name="destConnection"></param>
public static void CompactDatabase(string sourceConnection, string destConnection)
{
var type = System.Type.GetType(engineTypeName);
var engine = Activator.CreateInstance(type);
var localConnectionString = type.GetProperty("LocalConnectionString");
localConnectionString.SetValue(
obj: engine,
value: sourceConnection,
index: null);
var compactDatabase = type.GetMethod("Compact");
compactDatabase.Invoke(engine, new object[] { destConnection });
}
}
}
کلاس فوق، یک کلاس عمومی است و مرتبط به NHibernate نیست و در همه جا قابل استفاده است.
متد CreateEmptyDatabaseFile یک فایل بانک اطلاعاتی خالی با فرمت مخصوص SQL-CE را برای شما تولید خواهد کرد. به این ترتیب میتوان بدون نیاز به ابزار خاصی، سریعا یک بانک خالی را تولید و شروع به کار کرد. در این متد اگر کلمه عبوری را وارد نکنید، بانک اطلاعاتی رمزنگاری شده نخواهد بود و اگر کلمه عبور را وارد کنید، دیتابیس اولیه به همراه کلیه اعمال انجام شده بر روی آن در طول زمان، با کمک الگوریتم AES به صورت خودکار رمزنگاری خواهند شد. کل کاری را هم که باید انجام دهید ذکر این کلمه عبور در کانکشن استرینگ است.
متد CompactDatabase، یک متد چند منظوره است. اگر بانک اطلاعاتی SQL-CE رمزنگاری نشدهای دارید و میخواهید کل آنرا به همراه تمام اطلاعات درون آن رمزنگاری کنید، میتوانید جهت سهولت کار از این متد استفاده نمائید. آرگومان اول آن به کانکشن استرینگ بانکی موجود و آرگومان دوم به کانکشن استرینگ بانک جدیدی که تولید خواهد شد، اشاره میکند.
همچنین اگر یک بانک اطلاعاتی SQL-CE رمزنگاری شده دارید و میخواهید آنرا به صورت یک بانک اطلاعاتی جدید به همراه تمام رکوردهای آن رمزگشایی کنید، باز هم میتوان از این متد استفاده کرد. البته بدیهی است که کلمه عبور را باید داشته باشید و این کلمه عبور جایی درون فایل بانک اطلاعاتی ذخیره نمیشود. در این حالت در کانکشن استرینگ اول باید کلمه عبور ذکر شود و کانکشن استرینگ دوم نیازی به کلمه عبور نخواهد داشت.
فرمت کلی کانکشن استرینگ SQL-CE هم به شکل زیر است:
Data Source=c:\path\db.sdf;Password=1234;Encrypt Database=True
البته این برای حالتی است که قصد داشته باشید بانک اطلاعاتی مورد استفاده را رمزنگاری کنید یا از یک بانک اطلاعاتی رمزنگاری شده استفاده نمائید. اگر بانک اطلاعاتی شما کلمه عبوری ندارد، ذکر Data Source=c:\path\db.sdf کفایت میکند.
این کلاس هم از این جهت مطرح شد که NHibernate میتواند ساختار بانک اطلاعاتی را بر اساس تعاریف نگاشتها به صورت خودکار تولید و اعمال کند، «اما» بر روی یک بانک اطلاعاتی خالی SQL-CE از قبل تهیه شده (در غیراینصورت خطای The database file cannot be found. Check the path to the database را دریافت خواهید کرد).
نکته:
اگر دقت کرده باشید در این کلاس engineTypeName به صورت رشته ذکر شده است. چرا؟
علت این است که با ذکر engineTypeName به صورت رشته، میتوان از این کلاس در یک کتابخانه عمومی هم استفاده کرد، بدون اینکه مصرف کننده نیازی داشته باشد تا ارجاع مستقیمی را به اسمبلی SQL-CE به برنامه خود اضافه کند. اگر این ارجاع وجود داشت، متدهای یاد شده کار میکنند، در غیراینصورت در گوشهای ساکت و بدون دردسر و بدون نیاز به اسمبلی خاصی برای روز مبادا قرار خواهند گرفت.
3) ابزار مرور اطلاعات بانک اطلاعاتی SQL-CE
با استفاده از management studio خود SQL Server هم میشود با بانکهای اطلاعاتی SQL-CE کار کرد، اما ... اینبار برخلاف نگارش کامل اس کیوال سرور، با یک نسخهی بسیار بدوی، که حتی امکان rename فیلدها را هم ندارد مواجه خواهید شد. به همین جهت به شخصه برنامه SqlCe40Toolbox را ترجیح میدهم و اطمینان داشته باشید که امکانات آن برای کار با SQL-CE از امکانات ارائه شده توسط management studio مایکروسافت، بیشتر و پیشرفتهتر است!
4) تنظیمات NHibernate جهت کار با SQL-CE
الف) پس از نصب SQL-CE ، فایلهای آنرا در مسیر C:\Program Files\Microsoft SQL Server Compact Edition\v4.0 میتوان یافت. درایور ADO.NET آن هم در مسیر C:\Program Files\Microsoft SQL Server Compact Edition\v4.0\Desktop قرار دارد. بنابراین در ابتدا نیاز است تا ارجاعی را به اسمبلی System.Data.SqlServerCe.dll به برنامه خود اضافه کنید (نام پوشه desktop آن هم غلط انداز است. از این جهت که نگارش 4 آن، به راحتی در برنامههای ذاتا چند ریسمانی ASP.Net بدون مشکل قابل استفاده است).
نکته مهم: در این حالت NHibernate قادر به یافتن فایل درایور یاد شده نخواهد بود و پیغام خطای «Could not create the driver from NHibernate.Driver.SqlServerCeDriver» را دریافت خواهید کرد. برای رفع آن، اسمبلی System.Data.SqlServerCe.dll را در لیست ارجاعات برنامه یافته و در برگه خواص آن، خاصیت «Copy Local» را true کنید. به این معنا که NHibernate این اسمبلی را در کنار فایل اجرایی برنامه شما جستجو خواهد کرد.
ب) مطلب بعد، تنظیمات ابتدایی NHibernate است جهت شناساندن SQL-CE . مابقی مسایل (نکات mapping، کوئریها و غیره) هیچ تفاوتی با سایر بانکهای اطلاعاتی نخواهد داشت و یکی است. به این معنا که اگر برنامه شما از ویژگیهای خاص بانکهای اطلاعاتی استفاده نکند (مثلا اگر از رویههای ذخیره شده اس کیوال سرور استفاده نکرده باشد)، فقط با تغییر کانکشن استرینگ و معرفی dialect و driver جدید، به سادگی میتواند به یک بانک اطلاعاتی دیگر سوئیچ کند؛ بدون اینکه حتی بخواهید یک سطر از کدهای اصلی برنامه خود را تغییر دهید.
تنها نکته جدید آن این متد است:
private Configuration getConfig()
{
var configure = new Configuration();
configure.SessionFactoryName("BuildIt");
configure.DataBaseIntegration(db =>
{
db.ConnectionProvider<DriverConnectionProvider>();
db.Dialect<MsSqlCe40Dialect>();
db.Driver<SqlServerCeDriver>();
db.KeywordsAutoImport = Hbm2DDLKeyWords.AutoQuote;
db.IsolationLevel = IsolationLevel.ReadCommitted;
db.ConnectionString = ConnectionString;
db.Timeout = 10;
//for testing ...
db.LogFormattedSql = true;
db.LogSqlInConsole = true;
});
return configure;
}
که در آن نحوه تعریف MsSqlCe40Dialect و SqlServerCeDriver مشخص شده است.
نکته حاشیهای!
در این مثال primary key از نوع identity تعریف شده و بدون مشکل کار کرد. همین را اگر با EF تست کنید، این خطا را دریافت میکنید: «Server-generated keys and server-generated values are not supported by SQL Server Compact». بله، EF نمیتواند با primary key از نوع identity حین کار با SQL-CE کار کند. برای رفع آن توصیه شده است که از Guid استفاده کنید!
نکته تکمیلی:
استفاده از Dialect سفارشی در NHibernate
نکته پایانی!
و در پایان باید اشاره کرد که SQL-CE یک بانک اطلاعاتی نوشته شده با دات نت نیست (با CPP نوشته شده است و نصب آن هم نیاز به ران تایم به روز VC را دارد). به این معنا که جهت سیستمهای 64 بیتی و 32 بیتی باید نسخه مناسب آنرا توزیع کنید. یا اینکه Target platform پروژه جاری دات نت خود را بر روی X86 قرار دهید (نه بر روی Any CPU پیش فرض) و در این حالت تنها یک نسخه X86 بانک اطلاعاتی SQL-CE و همچنین برنامه خود را برای تمام سیستمها توزیع کنید.
داتنت 8 به همراه بهبودهای قابل ملاحظهای در کارآیی برنامههای داتنتی است و در این بین تعدادی قابلیت جدید را نیز به زبان سیشارپ اضافه کردهاست. در این مطلب ویژگی جدید «Alias any type» آنرا بررسی میکنیم. پیشنیاز کار با این قابلیت تنها نصب SDK داتنت 8 است.
امکان تعریف alias، قابلیت جدیدی نیست!
در نگارشهای پیشین زبان #C نیز میتوان برای نوعهای نامدار داتنت، alias/«نام مستعار» تعریف کرد؛ برای مثال:
Aliasها در قسمت using تعاریف یک کلاس معرفی میشوند و یکی از اهدف آنها، کوتاه کردن تعاریف فضاهای نام طولانی است و یا رفع تداخلها؛ همچنین تنها به Named types، محدود هستند و Named types فقط شامل این موارد میشوند: classes ،delegates ،interfaces ،records و structs
بنابراین دو حالت تعریف Namespace alias برای کوتاه سازی فضاهای نام طولانی و یا تعریف Type alias برای معرفی یک نام مستعار جدید برای نوعی مشخص، میسر است:
تنها کارکرد نامهای مستعار، کوتاه و زیبا سازی نامهای طولانی نیستند. برای مثال گاهی از اوقات ممکن است که بین نام نوعهای موجود در usingهای جاری، تداخل حاصل شود و برنامه کامپایل نشود. برای مثال فرض کنید که دو using زیر را تعریف کردهاید:
کامپایل این برنامه میسر نیست. چون هر دو نوع System.Random و UnityEngine.Random پیشتر تعریف شدهاند و در اینجا دقیقا مشخص نیست که تامین کنندهی شیء Random، کدام فضای نام است. در این حالت میتوان برای مثال در حین نمونه سازی، فضای نام را صراحتا ذکر کرد:
و یا میتوان برای آن نام مستعاری نیز تعریف کرد:
در این حالت دیگر تداخلی وجود نداشته و کامپایلر دقیقا میداند که تامین کنندهی Random، کدام کتابخانه و کدام فضای نام است.
امکان تعریف alias برای هر نوعی در C# 12.0
محدودیت امکان تعریف alias برای نوعهای نامدار داتنت در C# 12.0 برطرف شده و اکنون میتوان برای انواع و اقسام نوعها مانند آرایهها، tuples و غیره نیز alias تعریف کرد:
در اینجا امکان تعریف alias را برای آرایهها، nullable value types، نوعهای توکار، tupleها و حتی نوعهای unsafe، مشاهده میکنید.
یک نکته: امکان تعریف alias برای nullable reverence types وجود ندارد.
بررسی یک مثال C# 12.0
در اینجا محتویات یک فایل Program.cs یک برنامهی کنسول داتنت 8 را مشاهده میکنید:
در این مثال برای یک نوع tuple سفارشی، یک alias به نام Person تعریف شده و سپس از آن برای نمونه سازی یک شیء جدید و یا ارسال آن به عنوان یک پارامتر متد، استفاده شدهاست. خروجی برنامهی فوق به صورت زیر است:
چه زمانی بهتر است از قابلیت تعریف نامهای مستعار نوعها و یا فضاهای نام استفاده شود؟
اگر یک نام طولانی را بتوان به این صورت خلاصه کرد، مفید هستند؛ برای مثال ساده سازی تعریف یک لیست طولانی به صورت زیر:
و مثالی دیگر در این زمینه، کوتاه سازی تعاریف متداول جنریک طولانی است:
و یا اگر بتوانند رفع تداخلی را حاصل کنند، بکارگیری آنها ضروری است (مانند مثال شیء Random ابتدای بحث) و یا اگر بتوانند از تکرار تعریف یک tuple جلوگیری کنند، ذکر آنها یک refactoring مثبت بهشمار میرود؛ مانند مثال زیر که در آن از تعریف نوع tuple ای، دوبار استفاده شدهاست:
اما ... آیا واقعا تعاریفی مانند ذیل، مفید یا ضروری هستند؟
اینجا فقط قطعه کدی اضافی را که بیشتر سبب سردرگمی و بالا بردن درجهی پیچیدگی برنامه میشود، تولید کردهایم. خوانایی و سادگی درک برنامه در این حالت کاهش پیدا میکند.
میدان دید نامهای مستعار
به صورت پیشفرض، تمام نامهای مستعار تنها در داخل همان فایلی که تعریف شدهاند، قابل استفاده میباشند. از زمان C# 10.0 ، میتوان پیش از واژهی کلیدی using از واژهی کلیدی global نیز استفاده کرد تا تعریف آنها فقط در پروژهی جاری به صورت سراسری قابل دسترسی شود.
به همین جهت اگر نوعی قرار است در سایر پروژهها استفاده شود، بهتر است از global using استفاده نشده و از همان روشهای متداول تعریف records و یا classes استفاده شود.
امکان تعریف alias، قابلیت جدیدی نیست!
در نگارشهای پیشین زبان #C نیز میتوان برای نوعهای نامدار داتنت، alias/«نام مستعار» تعریف کرد؛ برای مثال:
using MyConsole = System.Console; MyConsole.WriteLine("Test console");
بنابراین دو حالت تعریف Namespace alias برای کوتاه سازی فضاهای نام طولانی و یا تعریف Type alias برای معرفی یک نام مستعار جدید برای نوعی مشخص، میسر است:
// Namespace alias using SuperJSON = System.Text.Json; var document = SuperJSON.JsonSerializer.Serialize("{}"); // Type alias using SuperJSON = System.Text.Json.JsonSerializer; var document = SuperJSON.Serialize("{}");
تنها کارکرد نامهای مستعار، کوتاه و زیبا سازی نامهای طولانی نیستند. برای مثال گاهی از اوقات ممکن است که بین نام نوعهای موجود در usingهای جاری، تداخل حاصل شود و برنامه کامپایل نشود. برای مثال فرض کنید که دو using زیر را تعریف کردهاید:
using UnityEngine; using System; Random rnd = new Random();
var rnd = new System.Random();
using Random = System.Random;
امکان تعریف alias برای هر نوعی در C# 12.0
محدودیت امکان تعریف alias برای نوعهای نامدار داتنت در C# 12.0 برطرف شده و اکنون میتوان برای انواع و اقسام نوعها مانند آرایهها، tuples و غیره نیز alias تعریف کرد:
using Ints = int[]; using DatabaseInt = int?; using OptionalFloat = float?; using Grade= decimal; using Point3D = (int, int, int); using Person = (string name, int age, string country); using unsafe P = char*; using Matrix = int[][]; Matrix aMatrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
یک نکته: امکان تعریف alias برای nullable reverence types وجود ندارد.
بررسی یک مثال C# 12.0
در اینجا محتویات یک فایل Program.cs یک برنامهی کنسول داتنت 8 را مشاهده میکنید:
using MyConsole = System.Console; using Person = (string name, int age, string country); Person person = new("User 1", 33, "Iran"); Console.WriteLine(person); PrintPerson(person); MyConsole.WriteLine("Test console"); static void PrintPerson(Person person) { MyConsole.WriteLine($"{person.name}, {person.age}, {person.country}"); }
(User 1, 33, Iran) User 1, 33, Iran Test console
چه زمانی بهتر است از قابلیت تعریف نامهای مستعار نوعها و یا فضاهای نام استفاده شود؟
اگر یک نام طولانی را بتوان به این صورت خلاصه کرد، مفید هستند؛ برای مثال ساده سازی تعریف یک لیست طولانی به صورت زیر:
using Companies = System.Collections.Generic.List<Company>; Companies GetCompanies() { // logic here } class Company { public string Name; public int Id; }
using EventHandlers = System.Collections.Generic.IEnumerable<System.Func<System.Threading.Tasks.Task>>;
و یا اگر بتوانند رفع تداخلی را حاصل کنند، بکارگیری آنها ضروری است (مانند مثال شیء Random ابتدای بحث) و یا اگر بتوانند از تکرار تعریف یک tuple جلوگیری کنند، ذکر آنها یک refactoring مثبت بهشمار میرود؛ مانند مثال زیر که در آن از تعریف نوع tuple ای، دوبار استفاده شدهاست:
using Country = (string Abbreviation, string Name); Country GetCountry(string abbreviation) { // Logic here } List<Country> GetCountries() { // Logic here }
using Ints = int[]; using DatabaseInt = int?; using OptionalFloat = float?;
میدان دید نامهای مستعار
به صورت پیشفرض، تمام نامهای مستعار تنها در داخل همان فایلی که تعریف شدهاند، قابل استفاده میباشند. از زمان C# 10.0 ، میتوان پیش از واژهی کلیدی using از واژهی کلیدی global نیز استفاده کرد تا تعریف آنها فقط در پروژهی جاری به صورت سراسری قابل دسترسی شود.
به همین جهت اگر نوعی قرار است در سایر پروژهها استفاده شود، بهتر است از global using استفاده نشده و از همان روشهای متداول تعریف records و یا classes استفاده شود.
ASP.NET Core 2.2 به همراه تعدادی قابلیت جدید است که یکی از آنها بررسی سلامت برنامه یا Health Check نام دارد. در بسیاری از اوقات ممکن است از سرویسهای ping و یا درخواست مشاهدهی صفحات وب سایت در بازههای زمانی مشخصی، جهت اطمینان حاصل کردن از برپایی و سلامت آن استفاده کنید. اما این سرویسها الزاما وضعیت سلامت برنامه را نمیتوانند به خوبی گزارش کنند. به همین جهت امکان ارائهی گزارشهای دقیقتری توسط ویژگی Health Check به ASP.NET Core اضافه شدهاست.
پیاده سازی ویژگی Health Check بدون استفاده از قابلیتهای ASP.NET Core 2.2
اگر بخواهیم در بررسی سلامت برنامه، وضعیت بانک اطلاعاتی آنرا گزارش دهیم، میتوان یک چنین اکشن متدی را طراحی کرد که در آن اتصالی به بانک اطلاعاتی باز شده و اگر در حین فراخوانی مسیر working/، استثنائی رخ داد، با بازگشت status code مساوی 503، عدم سلامت برنامه اعلام شود؛ کاری که سرویسهای ping متداول نمیتوانند آنرا با این دقت انجام دهند:
بازنویسی قطعه کد فوق با ویژگی جدید Health Check در ASP.NET Core 2.2
اکنون اگر بخواهیم قطعه کد فوق را با کمک ویژگیهای جدید ASP.NET Core 2.2 بازنویسی کنیم، روش کار به صورت زیر خواهد بود:
- ابتدا توسط متد services.AddHealthChecks، سرویس بررسی سلامت برنامه، ثبت و معرفی میشود.
- سپس توسط متد app.UseHealthChecks، بدون اینکه نیاز باشد کنترلر و اکشن متد جدیدی را جهت بازگشت وضعیت سلامت برنامه، تعریف کنیم، مسیر working/ قابل دسترسی خواهد شد.
تا اینجا اگر این مسیر را به سرویس بررسی uptime برنامهی خود معرفی کنید، صرفا وضعیت قابل دسترسی بودن مسیر working/ را دریافت خواهید کرد. اگر نیاز به گزارش دقیقتری وجود داشت، میتوان به کمک متد AddCheck، یک منطق سفارشی را نیز به آن افزود؛ همانند بررسی امکان اتصال به بانک اطلاعاتی، به روشی که ملاحظه میکنید. در اینجا اگر منطق مدنظر با موفقیت اجرا شد، HealthCheckResult.Healthy بازگشت داده میشود و یا HealthCheckResult.Unhealthy در صورت عدم موفقیت. هر کدام از این متدها میتوانند توضیحات و یا اطلاعات بیشتری را نیز توسط پارامترهای خود ارائه دهند.
امکان تهیه سرویسهای سفارشی بررسی سلامت برنامه
در مثال قبل، منطق بررسی سلامت برنامه را همانجا داخل متد ConfigureServices، به کمک متد services.AddHealthChecks().AddCheck معرفی کردیم. امکان انتقال این کدها به سرویسهای سفارشی، با پیاده سازی اینترفیس IHealthCheck نیز وجود دارد:
در اینجا کدهای AddCheck را به متد CheckHealthAsync منتقل کردیم. پس از آن برای معرفی آن به سیستم میتوان از روش زیر استفاده کرد:
متد AddCheck، کلاس SqlServerHealthCheck را به صورت یک سرویس جدید با طول عمر Transient به سیستم تزریق وابستگیهای NET Core. معرفی میکند (یعنی با هربار درخواست مسیر working/، یک وهلهی جدید از این کلاس ساخته شده و استفاده میشود) که امکان تزریق در سازندهی کلاس آن نیز وجود دارد.
سفارشی سازی خروجی بررسی سلامت برنامهها
تا اینجا از متدهای کلی Unhealthy و Healthy برای بازگشت وضعیت سلامت برنامه استفاده کردیم؛ خروجیهای بهتری را نیز میتوان ارائه داد:
در نهایت نیاز است خروجی از نوع HealthCheckResult بازگشت داده شود. این خروجی را یا میتوان توسط متدهای Healthy و Unhealthy با پارامترهای مخصوص آنها ایجاد کرد و یا مانند این مثال، توسط وهله سازی مستقیم آن.
روش دیگر سفارشی سازی خروجی آن، استفاده از پارامتر دوم متد app.UseHealthChecks است:
در اینجا یک خروجی JSON، از ریز خطاهای گزارش شده، تهیه شده و توسط context.Response.WriteAsync به فراخوان ارائه میشود.
معرفی کتابخانهای از IHealthCheckهای سفارشی
از مخزن کد AspNetCore.Diagnostics.HealthChecks میتوانید IHealthCheckهای سفارشی مخصوص SQL Server، MySQL و غیره را نیز دریافت و استفاده کنید.
پیاده سازی ویژگی Health Check بدون استفاده از قابلیتهای ASP.NET Core 2.2
اگر بخواهیم در بررسی سلامت برنامه، وضعیت بانک اطلاعاتی آنرا گزارش دهیم، میتوان یک چنین اکشن متدی را طراحی کرد که در آن اتصالی به بانک اطلاعاتی باز شده و اگر در حین فراخوانی مسیر working/، استثنائی رخ داد، با بازگشت status code مساوی 503، عدم سلامت برنامه اعلام شود؛ کاری که سرویسهای ping متداول نمیتوانند آنرا با این دقت انجام دهند:
[Route("working")] public ActionResult Working() { using (var connection = new SqlConnection(_connectionString)) { try { connection.Open(); } catch (SqlException) { return new HttpStatusCodeResult(503, "Generic error"); } } return new EmptyResult(); }
بازنویسی قطعه کد فوق با ویژگی جدید Health Check در ASP.NET Core 2.2
اکنون اگر بخواهیم قطعه کد فوق را با کمک ویژگیهای جدید ASP.NET Core 2.2 بازنویسی کنیم، روش کار به صورت زیر خواهد بود:
namespace MvcHealthCheckTest { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddHealthChecks() .AddCheck("sql", () => { using (var connection = new SqlConnection(Configuration["connectionString"])) { try { connection.Open(); } catch (SqlException) { return HealthCheckResult.Unhealthy(); } } return HealthCheckResult.Healthy(); }); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseHealthChecks("/working");
- سپس توسط متد app.UseHealthChecks، بدون اینکه نیاز باشد کنترلر و اکشن متد جدیدی را جهت بازگشت وضعیت سلامت برنامه، تعریف کنیم، مسیر working/ قابل دسترسی خواهد شد.
تا اینجا اگر این مسیر را به سرویس بررسی uptime برنامهی خود معرفی کنید، صرفا وضعیت قابل دسترسی بودن مسیر working/ را دریافت خواهید کرد. اگر نیاز به گزارش دقیقتری وجود داشت، میتوان به کمک متد AddCheck، یک منطق سفارشی را نیز به آن افزود؛ همانند بررسی امکان اتصال به بانک اطلاعاتی، به روشی که ملاحظه میکنید. در اینجا اگر منطق مدنظر با موفقیت اجرا شد، HealthCheckResult.Healthy بازگشت داده میشود و یا HealthCheckResult.Unhealthy در صورت عدم موفقیت. هر کدام از این متدها میتوانند توضیحات و یا اطلاعات بیشتری را نیز توسط پارامترهای خود ارائه دهند.
امکان تهیه سرویسهای سفارشی بررسی سلامت برنامه
در مثال قبل، منطق بررسی سلامت برنامه را همانجا داخل متد ConfigureServices، به کمک متد services.AddHealthChecks().AddCheck معرفی کردیم. امکان انتقال این کدها به سرویسهای سفارشی، با پیاده سازی اینترفیس IHealthCheck نیز وجود دارد:
public class SqlServerHealthCheck : IHealthCheck { private readonly IConfiguration _configuration; public SqlServerHealthCheck(IConfiguration configuration) { _configuration = configuration; } public Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken)) { using (var connection = new SqlConnection(_configuration["connectionString"])) { try { connection.Open(); } catch (SqlException) { return Task.FromResult(HealthCheckResult.Unhealthy()); } } return Task.FromResult(HealthCheckResult.Healthy()); } }
namespace MvcHealthCheckTest { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddHealthChecks() .AddCheck<SqlServerHealthCheck>("sql");
سفارشی سازی خروجی بررسی سلامت برنامهها
تا اینجا از متدهای کلی Unhealthy و Healthy برای بازگشت وضعیت سلامت برنامه استفاده کردیم؛ خروجیهای بهتری را نیز میتوان ارائه داد:
public Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken)) { using (var connection = new SqlConnection(_configuration["connectionString"])) { try { connection.Open(); } catch (SqlException) { return Task.FromResult(new HealthCheckResult( status: context.Registration.FailureStatus, description: "It is dead!")); } } return Task.FromResult(HealthCheckResult.Healthy("Healthy as a horse")); }
روش دیگر سفارشی سازی خروجی آن، استفاده از پارامتر دوم متد app.UseHealthChecks است:
namespace MvcHealthCheckTest { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseHealthChecks("/working", new HealthCheckOptions { ResponseWriter = async (context, report) => { var result = JsonConvert.SerializeObject(new { status = report.Status.ToString(), errors = report.Entries.Select(e => new { key = e.Key, value = Enum.GetName(typeof(HealthStatus), e.Value.Status) }) }); context.Response.ContentType = MediaTypeNames.Application.Json; await context.Response.WriteAsync(result); } });
معرفی کتابخانهای از IHealthCheckهای سفارشی
از مخزن کد AspNetCore.Diagnostics.HealthChecks میتوانید IHealthCheckهای سفارشی مخصوص SQL Server، MySQL و غیره را نیز دریافت و استفاده کنید.
برخلاف AngularJS، در برنامههای Angular امکانات two way data binding به صورت پیشفرض ارائه نمیشوند تا از تمام مشکلات آن مانند digest cycle ،watchers و غیره خبری نباشد. اما گاهی از اوقات نیاز است انقیاد دو طرفهی سفارشی را بین دو کامپوننت ایجاد کنیم. در این مطلب روش ایجاد یک چنین انقیادهایی را بررسی خواهیم کرد و در اینجا در ابتدا نیاز است دو پیشنیاز Property Binding و Event Binding را بررسی کنیم که از جمع آنها two way data binding حاصل میشود:
البته Angular به همراه دایرکتیو ویژهای به نام ngModel است که two-way data binding را با import ماژول ویژهی فرمها میسر میکند:
که آن نیز در اصل از جمع Property Binding و Event Binding تشکیل شدهاست:
و یا به صورت خلاصه:
در اینجا میخواهیم یک چنین امکانی را بدون استفاده از ngModel و ماژول فرمها پیاده سازی کنیم.
انقیاد به خواص یا Property binding
فرض کنید دو کامپوننت والد و فرزند را ایجاد کردهایم:
در کامپوننت والد، مقداری را توسط متد deposit هربار 100 آیتم افزایش میدهیم:
با این قالب:
که در آن مقدار amount کامپوننت والد نمایش داده شدهاست و همچنین این مقدار به خاصیت ورودی کامپوننتی به نام app-child نیز نسبت داده شدهاست.
کامپوننت فرزند به صورت ذیل تعریف میشود:
که در آن خاصیت amount، از والد آن، توسط ویژگی Input دریافت میشود. سپس در متد withdraw هربار میتوان 100 آیتم را از آن کسر کرد.
با این قالب:
که در آن مقدار amount فرزند نمایش داده شدهاست و همچنین امکان فراخوانی متد withdraw وجود دارد.
در اینجا زمانیکه data binding را به صورت ذیل تعریف میکنیم:
روش مقدار دهی خاصیت amount داخل [] ، انقیاد به خواص نامیده میشود و سمت راست آن نیز یک خاصیت درنظر گرفته میشود. یعنی مقدار خاصیت amount والد (درون "") به مقدار خاصیت amount فرزند (درون []) نسبت داده خواهد شد.
این ارتباط نیز یک طرفهاست. برای مثال اگر بر روی دکمهی Deposit والد کلیک کنیم:
مقدار افزایش یافتهی در والد، به فرزند نیز منتقل میشود و نمایش داده خواهد شد. اما اگر بر روی دکمهی withdraw فرزند کلیک کنیم:
تغییر صورت گرفته، به والد انعکاس پیدا نمیکند. برای اطلاع رسانی به والد، به انقیاد به رخدادها نیاز داریم.
انقیاد به رخدادها یا Event binding
یک کامپوننت میتواند به رخدادهای صادر شدهی توسط کامپوننتی دیگر گوش فرا دهد:
برای این منظور در کامپوننت فرزند، یک خاصیت Output را به نام amountChange از نوع EventEmitter تعریف میکنیم. سپس جایی که قرار است کار کاهش amount صورت گیرد، با صدور رخدادی (this.amountChange.emit)، این مقدار را به والد اعلام میکنیم.
اکنون در قالب کامپوننت والد، این رخداد را درون یک () معرفی خواهیم کرد:
به این ترتیب زمانیکه کامپوننت فرزند، مقدار amount را تغییر میدهد، این مقدار توسط this.amountChange.emit به والد منتشر خواهد شد و میتوان در سمت والد توسط event$ به آن دسترسی یافته و آنرا به خاصیت this.amount کامپوننت والد نسبت دهیم.
اکنون اگر برنامه را آزمایش کنیم، با کلیک بر روی دکمهی withdraw فرزند، مقدار کاهش یافته به والد نیز منعکس میشود:
پیاده سازی syntax ویژهی Banana in a box
تا اینجا پیاده سازی two way data-binding سفارشی به پایان میرسد. اما تعریف طولانی:
به صورت ذیل هم قابل نوشتن و ساده سازی است:
که به آن syntax ویژه Banana in a box نیز گفته میشود.
نکتهی ویژهی آن، وجود پسوند Change در نام رخداد تعریف شدهاست:
اگر نام خاصیت Input مساوی x باشد، باید جهت فعالسازی syntax ویژه Banana in a box، نام رخداد متناظر با آن دقیقا مساوی xChange انتخاب شود. مانند amount ورودی در اینجا و amountChange خروجی تعریف شده.
بنابراین به صورت خلاصه جهت تعریف یک انقیاد دو طرفه سفارشی:
- ابتدا باید انقیاد به یک خاصیت ورودی x را تعریف کرد.
- سپس نیاز است انقیاد به یک رخداد خروجی همنام، که نام آن، پسوند Change را اضافهتر دارد، یعنی xChange را تعریف کرد.
- اکنون میتوان two-way data binding syntax ویژهای را به نام banana in a box بر روی ایندو تعریف کرد[(x)].
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید.
البته Angular به همراه دایرکتیو ویژهای به نام ngModel است که two-way data binding را با import ماژول ویژهی فرمها میسر میکند:
که آن نیز در اصل از جمع Property Binding و Event Binding تشکیل شدهاست:
<input [ngModel]="username" (ngModelChange)="username = $event">
<input [(ngModel)]='username' />
انقیاد به خواص یا Property binding
فرض کنید دو کامپوننت والد و فرزند را ایجاد کردهایم:
در کامپوننت والد، مقداری را توسط متد deposit هربار 100 آیتم افزایش میدهیم:
import { Component, OnInit } from "@angular/core"; @Component({ selector: "app-parent", templateUrl: "./parent.component.html", styleUrls: ["./parent.component.css"] }) export class ParentComponent implements OnInit { amount = 500; constructor() { } ngOnInit() { } deposit() { this.amount += 100; } }
<h2>Custom two way data binding</h2> <div class="panel panel-primary"> <div class="panel-heading"> <h2 class="panel-title">Parnet Component</h2> </div> <div class="panel-body"> <label>Available amount:</label> {{amount}} <button (click)="deposit()" class="btn btn-success">Deposit 100</button> <div> <app-child [amount]="amount"> </app-child> </div> </div> </div>
کامپوننت فرزند به صورت ذیل تعریف میشود:
import { Component, OnInit, Input } from "@angular/core"; @Component({ selector: "app-child", templateUrl: "./child.component.html", styleUrls: ["./child.component.css"] }) export class ChildComponent implements OnInit { @Input() amount: number; constructor() { } ngOnInit() { } withdraw() { this.amount -= 100; } }
با این قالب:
<div class="panel panel-default"> <div class="panel-heading"> <h2 class="panel-title">Child Component</h2> </div> <div class="panel-body"> <label>Amount available: </label> {{amount}} <button (click)="withdraw()" class="btn btn-danger">Withdraw 100</button> </div> </div>
در اینجا زمانیکه data binding را به صورت ذیل تعریف میکنیم:
<app-child [amount]="amount"> </app-child>
این ارتباط نیز یک طرفهاست. برای مثال اگر بر روی دکمهی Deposit والد کلیک کنیم:
مقدار افزایش یافتهی در والد، به فرزند نیز منتقل میشود و نمایش داده خواهد شد. اما اگر بر روی دکمهی withdraw فرزند کلیک کنیم:
تغییر صورت گرفته، به والد انعکاس پیدا نمیکند. برای اطلاع رسانی به والد، به انقیاد به رخدادها نیاز داریم.
انقیاد به رخدادها یا Event binding
یک کامپوننت میتواند به رخدادهای صادر شدهی توسط کامپوننتی دیگر گوش فرا دهد:
import { Component, OnInit, Input, Output, EventEmitter } from "@angular/core"; @Component({ selector: "app-child", templateUrl: "./child.component.html", styleUrls: ["./child.component.css"] }) export class ChildComponent implements OnInit { @Input() amount: number; @Output() amountChange = new EventEmitter(); constructor() { } ngOnInit() { } withdraw() { this.amount -= 100; this.amountChange.emit(this.amount); } }
اکنون در قالب کامپوننت والد، این رخداد را درون یک () معرفی خواهیم کرد:
<app-child [amount]="amount" (amountChange)="this.amount= $event"> </app-child>
اکنون اگر برنامه را آزمایش کنیم، با کلیک بر روی دکمهی withdraw فرزند، مقدار کاهش یافته به والد نیز منعکس میشود:
پیاده سازی syntax ویژهی Banana in a box
تا اینجا پیاده سازی two way data-binding سفارشی به پایان میرسد. اما تعریف طولانی:
<app-child [amount]="amount" (amountChange)="this.amount= $event"> </app-child>
<app-child [(amount)]="amount"> </app-child>
نکتهی ویژهی آن، وجود پسوند Change در نام رخداد تعریف شدهاست:
@Input() amount: number; @Output() amountChange = new EventEmitter();
بنابراین به صورت خلاصه جهت تعریف یک انقیاد دو طرفه سفارشی:
- ابتدا باید انقیاد به یک خاصیت ورودی x را تعریف کرد.
- سپس نیاز است انقیاد به یک رخداد خروجی همنام، که نام آن، پسوند Change را اضافهتر دارد، یعنی xChange را تعریف کرد.
- اکنون میتوان two-way data binding syntax ویژهای را به نام banana in a box بر روی ایندو تعریف کرد[(x)].
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید.