نوعهای نال نپذیر در TypeScript
مقدمهای بر NET MAUI.
An Introduction to .NET MAUI For Mobile Development
.NET Multi-platform App UI (.NET MAUI) is a cross-platform framework for creating native mobile and desktop apps with C# and XAML.
.NET MAUI is open-source and is the evolution of Xamarin.Forms, extended from mobile to desktop scenarios, with UI controls rebuilt from the ground up for performance and extensibility. If you've previously used Xamarin.Forms to build cross-platform user interfaces, you'll notice many similarities with .NET MAUI. However, there are also some differences. Using .NET MAUI, you can create multi-platform apps using a single project, but you can add platform-specific source code and resources if necessary. One of the key aims of .NET MAUI is to enable you to implement as much of your app logic and UI layout as possible in a single code-base.
0:00 - Setup Visual Studio and MAUI Project
00:16:25 - Create MAUI Pages with C#
00:27:42 - Create MAUI Pages with XAML
00:32:28 - Explore MAUI Layouts
00:39:38 - Static Shared Resources
00:44:36 - Platform Specific Values
00:50:11 - Page Navigation
NET Core SDK 3.1.106. منتشر شد
همانطور که قول داده بودم، به اصول GRASP میپردازیم.
اصول GRASP-General Responsibility Assignment
Software Principles
این اصول به بررسی نحوه تقسیم وظایف بین کلاسها و مشارکت اشیاء برای به انجام رساندن یک مسئولیت میپردازند. اینکه هر کلاس در ساختار نرم افزار چه وظیفهای دارد و چگونه با کلاسهای دیگر مشارکت میکند تا یک عملکرد به سیستم اضافه گردد. این اصول به چند بخش تقسیم میشوند:
- کنترلر ( Controller )
- ایجاد کننده ( Creator )
- انسجام قوی ( High Cohesion )
- واسطه گری ( Indirection )
- دانای اطلاعات ( Information Expert )
- اتصال ضعیف ( Low Coupling )
- چند ریختی ( Polymorphism )
- حفاظت از تاثیر تغییرات ( Protected Variations )
- مصنوع خالص ( Pure Fabrication )
Controller
این الگو بیان میکند که مسئولیت پاسخ به رویدادهای (Events ) یک سناریوی محدود مانند یک مورد کاربردی ( Use Case ) باید به عهده یک کلاس غیر UI باشد. کنترلر باید کارهایی را که نیاز است در پاسخ رویداد انجام شود، به دیگران بسپرد و نتایج را طبق درخواست رویداد بازگرداند. در اصل، کنترلر دریافت کننده رویداد، راهنمای مسیر پردازش برای پاسخ به رویداد و در نهایت برگرداننده پاسخ به سمت مبداء رویداد است. در زیر مثالی را میبینیم که رویداد اتفاق افتاده توسط واسط گرافیکی به سمت یک handler (که متدی است با ورودیِ فرستنده و آرگمانهای مورد نیاز) در کنترلر فرستاده میشود. این روش event handling، در نمونههای وب فرم و ویندوز فرم دیده میشود. به صورتی خود کلاسهای .Net وظیفه Event Raising از سمت UI با کلیک روی دکمه را انجام میدهد:
public class UserController { protected void OnClickCreate(object sender, EventArgs e) { // call validation services // call create user services } }
در مثال بعد عملیات مربوط به User در یک WebApiController پاسخ داده میشود. در اینجا به جای استفاده از Event Raising برای کنترل کردن رویداد، از فراخوانی یک متد در کنترلر توسط درخواست HttpPost انجام میگیرد. در اینجا نیاز است که در سمت کلاینت درخواستی را ارسال کنیم:
public class UserWebApiController { [HttpPost] public HttpResponseMessage Create(UserViewModel user) { // call validation services // call create user services } }
Creator :
این اصل میگوید شیء ای میتواند یک شیء دیگر را بسازد ( instantiate ) که: (اگر کلاس B بخواهد کلاس A را instantiate کند)
- کلاس B شیء از کلاس A را در خود داشته باشد؛
- یا اطلاعات کافی برای instantiate کردن از A را داشته باشد؛
- یا به صورت نزدیک با A در ارتباط باشد؛
- یا بخواهد شیء A را ذخیره کند.
از آنجایی که این اصل بدیهی به نظر میرسد، با مثال نقض، درک بهتری را نسبت به آن میتوان پیدا کرد:
// سازنده public class B { public static A CreateA(string name, string lastName, string job) { return new A() { Name =name, LastName = lastName, Job = job }; } } // ایجاد شونده public class A { public string Name { get; set; } public string LastName { get; set; } public string Job { get; set; } } public class Context { public void Main() { var name = "Rasoul"; var lastName = "Abbasi"; var job = "Developer"; var obj = B.CreateA(name, lastName, job); } }
و اما چرا این مثال، اصل Creator را نقض میکند. در مثال میبینید که کلاس B، یک شیء از نوع A را در متد Main کلاس Context ایجاد میکند. کلاس B فقط یک متد برای تولید A دارد و در عملیات تولید A هیچ منطق خاصی را پیاده سازی نمیکند.کلاس B شیء ای را از کلاس A ، در خود ندارد، با آن ارتباط نزدیک ندارد و آنرا ذخیره نمیکند. با اینکه کلاس B اطلاعات کافی را برای تولید A از ورودی میگیرد، ولی این کلاس Context است که اطلاعات کافی را ارسال مینماید. اگر در کلاس B منطقی اضافه بر instance گیریِ ساده وجود داشت (مانند بررسی صحت و اعتبار سنجی)، میتوانستیم بگوییم کلاس B از یک مجموعه عملیات instance گیری با خبر است که کلاس Context نباید از آن خبر داشته باشد. لذا اکنون هیچ دلیلی وجود ندارد که وظیفه تولید A را در Context انجام ندهیم و این مسئولیت را به کلاس B منتقل کنیم. این مورد ممکن است در ذهن شما با الگوی Factory تناقض داشته باشد. ولی نکته اصلی در الگو Factory انجام عملیات instance گیری با توجه به منطق برنامه است؛ یعنی وظیفهای که کلاس Context نباید از آن خبر داشته باشد را به کلاس Factory منتقل میکنیم. در غیر اینصورت ایجاد کلاس Factory بی معنا خواهد بود (مگر به عنوان افزایش انعطاف پذیری معماری که بتوان به راحتی نوع پیاده سازی یک واسط را تغییر داد).
High Cohesion :
این اصل اشاره به یکی از اصول اساسی طراحی نرم افزار دارد. انسجام واحدهای نرم افزاری باعث افزایش خوانایی، سهولت اشکال زدایی، قابلیت نگهداری و کاهش تاثیر زنجیرهای تغییرات میشود. طبق این اصل، مسئولیتهای هر واحد باید مرتبط باشد. لذا اجزایی کوچک با مسئولیتهای منسجم و متمرکز بهتر از اجزایی بزرگ با مسئولیتهای پراکنده است. اگر واحدهای سازنده نرم افزار انسجام ضعیفی داشته باشند، درک همکاریها، استفاده مجدد آنها، نگه داری نرم افزار و پاسخ به تغییرات سختتر خواهد شد.
در مثال زیر نقض این اصل را مشاهده میکنیم:
class Controller { public void CreateProduct(string name, int categoryId) { } public void EditProduct(int id, string name) { } public void DeleteProduct(int id) { } public void CreateCategory(string name) { } public void EditCategory(int id, string name) { } public void DeleteCategory(int id) { } }
همانطور که میبینید، کلاس
کنترلر ما، مسئولیت مدیریت Product و Category را بر عهده دارد. بزرگ شدن این کلاس، باعث سختتر شدن
خواندن کد و رفع اشکال میگردد. با جداسازی کنترلر مربوط به Product از Category میتوان انسجام را بالا برد.
Indirection :
این اصل بیان میکند که با تعریف یک واسط بین دو مولفه نرم افزاری میتوان میزان اتصال نرم افزار را کاهش داد. بدین ترتیب وظیفه هماهنگی ارتباط دو مؤلفه، به عهده این واسط خواهد بود و نیازی نیست دادههای ورودی و خروجی دو مؤلفه، هماهنگ باشند. در اینجا واسط، از وابستگی بین دو مؤلفه با پنهان کردن ضوابط هر مؤلفه از دیگری و ایجاد وابستگی ضعیف خود با دو مؤلفه، باعث کاهش اتصال کلی طراحی میگردد.
الگوهای Adapter و Delegate و همچنین نقش کنترلر در الگوی معماری MVC از این اصل پیروی میکنند.
class SenderA { public Mediator mediator { get; } public SenderA() { mediator = new Mediator(); } public void Send(string message, string reciever) { mediator.Send(message, reciever); } } class SenderB { public Mediator mediator { get; } public SenderB() { mediator = new Mediator(); } public void Send(string message) { } } public class RecieverA { public void DoAction(string message) { // انجام عملیات بر اساس پیغام دریافت شده switch (message) { case "create": break; case "delete": break; default: break; } } } public class RecieverB { public void DoAction(string message) { // انجام عملیات بر اساس پیغام دریافت شده switch (message) { case "edit": break; case "rollback": break; default: break; } } } class Mediator { internal void Send(string message, string reciever) { switch (reciever) { case "A": var recieverObjA = new RecieverA(); recieverObjA.DoAction(message); break; case "B": var recieverObjB = new RecieverB(); recieverObjB.DoAction(message); break; default: break; } } } class IndirectionContext { public void Main() { var senderA = new SenderA(); senderA.Send("rollback", "B"); var senderB = new SenderA(); senderB.Send("create", "A"); } }
در این مثال کلاس Mediator به عنوان واسط ارتباطی بین کلاسهای Sender و Receiver قرار گرفته و نقش تحویل پیغام را دارد.
در مقاله بعدی، به بررسی سایر اصول GRASP خواهم پرداخت.
Class Coupling and Cohesion
تعدادی از قواعد شهودی هم، با Coupling و Cohesion به ترتیب مابین و درون کلاسها، سروکار دارند. تلاش ما در راستای افزایش Cohesion درون کلاسها و سست کردن و کاهش Coupling مابین کلاسها میباشد. این قواعد شهودی همین اهداف را در پارادایم action-oriented، در ارتباط با توابع دارند. هدف از Tight Cohesion (انسجام و چسبندگی قوی) در توابع، انسجام بالا و ارتباط نزدیک مابین کدهای موجود در تابع، میباشد. هدفی که Loose Coupling (اتصال سست و ضعیف، وابستگی ضعیف) در بین توابع دنبال میکند، اشاره دارد به اینکه اگر تابعی قصد استفاده از تابع دیگری را داشته باشد، باید وارد شدن و خروج از آن، از یک نقطه صورت گیرد. این مباحث منجربه مطرح شدن قواعد شهودی از جمله: «یک تابع باید طوری سازماندهی شود که تنها یک دستور return داشته باشد»، در پارادایم action-oriented میشود.
ما در پارادایم شیء گرا، اهداف خود از Loose Coupling و Tight Cohesion را در سطح کلاس مطرح میکنیم. 5 شکل اصلی Coupling مابین کلاسها به شرح زیر میباشد:
- Ni Coupling
- Export Coupling
- Overt Coupling
- Covert Coupling
- Surreptitious Coupling
بهترین حالتی که دو کلاس به طور مطلق هیچ وابستگی به یکدیگر ندارند. در این صورت میتوان یکی کلاسها را حذف کرد، بدون اینکه تأثیری بر روی سایر آنها داشته باشد. البته وجود برنامهای کاربردی با این نوع اتصال ممکن نخواهد بود. بهترین چیزی که میشود با این نوع اتصال ایجاد کرد، Class Libraryای میباشد که شامل مجموعه ای از کلاسهای مستقل بوده، به طوری که هیچ تأثیری بر روی یکدیگر ندارند.
در این شکل از اتصال، یک کلاس به واسط عمومی کلاس دیگر وابسته میباشد؛ به این معنی که از عملیاتی که کلاس مورد نظر در واسط عمومی خود قرار داده است، استفاده میکند.
این نوع اتصال زمانی رخ میدهد که یک کلاس از جزئیات پیاده سازی کلاس دیگر با داشتن اجازه دسترسی از جانب آن، استفاده کند. به عنوان مثال، مکانیزم کلاسهای friend در زبان سی پلاس پلاس، که امکان این را میدهد کلاس X اجازه دوستی به کلاس Y را اعطا کند و در این صورت کلاس Y میتواند به جزئیات پیاده سازی خصوصی کلاس X دسترسی داشته باشد.
این نوع اتصال هم به مانند Overt میباشد؛ با این تفاوت که هیچ اجازه دسترسی به کلاس Y داده نشده است. اگر زبانی داشته باشیم که به کلاس Y اجازه دهد خود را به عنوان دوست کلاس X معرفی کند، در این صورت نوع اتصال بین دو کلاس X و Y از نوع Covert میباشد. به عنوان مثال واقعی، میتوان به استفاده از Reflection در دات نت اشاره کرد.
آخرین نوع اتصال که بدترین حالت هم محسوب میشود، مربوط است به زمانیکه کلاس X به هر طریقی که شده از جزئیات داخلی کلاس Y آگاه باشد و از اعضای عمومی دادهای (public data member) آن کلاس استفاده کند. منظور این است که با تغییر این دادههای کلاس متوجه میشود که بر روی عملیات b از کلاس چه تأثیری میگذارد و با اتکاء به این دستاورد، جزئیات داخلی خود را پیاده سازی میکند و یک اتصال پنهان را با کلاس Y ایجاد کرده است. در این حالت یک وابستگی قوی به صورت پنهان مابین رفتاری از کلاس Y و پیاده سازی کلاس X ایجاد شده است.
اتصال و پیوستگی مابین کلاسها باید از نوع Nil یا Export باشد؛ به این معنی که یک کلاس فقط از واسط عمومی کلاس دیگر استفاده کند یا کاری با آن نداشته باشد. (Classes should only exhibit nil or export coupling with other classes, that is, a class should only use operations in the public interface of another class or have nothing to do with that class.)بجز این دو نوع اتصال، بقیه شکلهای اتصال به طریقی اجازه دسترسی به جزئیات پیاده سازی کلاسها را اعطا میکنند. در نتیجه باعث ایجاد وابستگی مابین پیاده سازی دو کلاس میشوند. این وابستگی ما بین پیاده سازیها به محض نیاز به تغییر پیاده سازی یکی از کلاسها ، باعث به وجود آمدن مشکلات نگهداری خواهند شد.
Cohesion درون کلاسها سعی بر این دارد که مطمئن شود تمام اجزای یک کلاس به شدت باهم مرتبط هستند. تعدادی از قواعد شهودی نیز در ادامه بر این خصوصیت دلالت دارند.
قاعده شهودی 2.8
یک کلاس باید یک و تنها یک Key Abstraction را تسخیر نماید. (A class should capture one and only one key abstraction)یک Key Abstraction به عنوان یک Entity در Domain Model تعریف میشود و اغلب در غالب اسم در اسناد و مشخصات نیازمندیها ظاهر میشوند. هر کدام از آنها باید فقط به یک کلاس نگاشت پیدا کنند. اگر این نگاشت به بیش از یک کلاس انجام گیرد، در نتیجه احتمالا طراح هر تابع را به عنوان یک کلاس تسخیر کرده است. اگر بیش از یک Key Abstraction به یک کلاس نگاشت پیدا کرده باشد، پس احتمالا طراح یک سیستم متمرکز را طراحی کرده است. این کلاسها Vague Classes نامیده میشوند و باید آنها در دو کلاس یا بیشتر، تسخیر شوند.
قاعده شهودی 2.9
داده و رفتار مرتبط را در یک جا (کلاس) نگه دارید. (Keep related data and behavior in one place)در واقع هدفی که این قاعده به دنبال آن میباشد این است که هر دو جزء تشکیل دهنده یک Key Abstraction ، یعنی همان داده و رفتار، باید توسط فقط یک کلاس تسخیر شوند. با نقض این قاعده، توسعه دهنده باید با قرار داد (Convention) خاصی برنامه نویسی کند.
طراح باید کلاسهایی را که مرتبا دادههای مورد نیاز خود را با متدهای get از سایر کلاسها دریافت میکنند، شناسایی کند. زیرا این نوع کلاسها این قاعده شهودی را نقض کردهاند.
مثال واقعی
یا برعکس آن ضد الگوی Anemic Domain Model که ناقض این قاعده میباشد.
در قسمت اول اشاره کردیم این قواعد را به راحتی میتوان در صورت نیاز نقض کرد. بعضی از مواقع نیاز به طراحی فیزیکی است که باعث تغییر در طراحی منطقی شده و چه بسا میتواند باعث نقض هر کدام از این قواعد شهودی نیز شود. اگر به بخش پروژههای سایت رجوع کنید این نقض کاملا مشهود (DomainClasses و ServiceLayer موجود در طراحی فیزیکی آنها) میباشد (بیشتر از Anemic Domain Model استفاده شده است)؛ ولی نمیتوان گفت که این کار اشتباه است.
قاعده شهودی 2.10
اطلاعات نامرتبط به هم را در کلاسهای جدا از هم قرار دهید. ((Spin off nonrelated information into another class (i.e., noncommunicating behavior)
هدف از این قاعده این است که اگر کلاسی داریم که یکسری از متدهایش با بخشی از داده و یکسری دیگر با بخش دیگر دادهها کار میکنند، در واقع شما دو Key Abstraction را به یک کلاس نگاشت کرده اید (Vague Class) و باید آنها را به کلاسهای جدا نگاشت کنید.
به کلاس Dictionary در تصویر زیر توجه کنید.
برای تعداد کمی داده، بهترین پیاده سازی با استفاده از List و در مقابل برای تعداد داده زیاد بهترین پیاده سازی، استفاده از HashTable میباشد. هر یک از این پیاده سازیها، به متدهایی برای add و find کلمات نیاز دارند. طراحی سمت چپ تصویر نشان از نقض این قاعده شهودی دارد.
در طرح سمت چپ، استفاده کننده باید بداند که چقدر داده وارد کند. از طرفی نمایش جزئیات پیاده سازی در نام کلاس هم ایده خوبی نیست (طرح سمت چپ). بهترین راه حل که در مقالات آینده به آنها خواهیم رسید، بحث استفاده از ارث بری میباشد. به این ترتیب، با استفاده از یک کلاس Dictionary که نمایش جزئیات داخلی خود را مخفی کرده و در شرایط لازم نمایش جزئیات داخلی خود را تغییر دهد. منظور این است که استفاده کننده درگیر جزئیات داخلی آن نشود و این جزئیات که کدام نوع PDict یا HDict استفاده خواهد شد، از دید او مخفی باشد.