ایجاد متغیرها در Rust
در Rust، متغیرها را میتوان با استفاده از کلمه کلیدی let و به دنبال آن، نام متغیر و مقدار اختصاص داده شدهی به آن ایجاد کرد. مثلا:
let x = 10;
let x: i32 = 10;
متغیرهای تغییرپذیر (Mutable) و تغییرناپذیر (Immutable) در Rust
در Rust، متغیرها به طور پیش فرض تغییر ناپذیر هستند؛ به این معنا که پس از تخصیص، مقدار آنها قابل تغییر نیست؛ مثلا:
let x = 10; x = 20; //compile-time error
let mut x = 10; x = 20;
Scope متغیرها در Rust
متغیرها در Rust دارای دامنه خاصی هستند که توسط curly braces که بیانیه آنها را احاطه کردهاند، تعریف میشود. مثلا
{ let x = 10; } // این متغیر خارج از این محدوده در دسترس نخواهد بود
به این دلایل، به طور کلی توصیه میشود که از متغیرهای global به مقدار کم در Rust استفاده کنید. در عوض، اغلب بهتر است از متغیرهایی استفاده شود که در تابع یا بلوکی که در آن مورد استفاده قرار میگیرند، تعریف و scope شدهاند. این مورد میتواند درک رفتار برنامه را آسانتر کند و از عوارض جانبی ناخواسته ناشی از متغیرهای سراسری جلوگیری کند.
Shadowing Variables in Rust
Shadowing یک مفهوم برنامه نویسی است که به شما امکان میدهد یک متغیر را در یک scope، دوبار اعلام کنید و به طور موثر متغیر اصلی را با متغیر جدیدی به همین نام shadow کنید. وقتی متغیری را shadow میکنید، متغیر جدید، متغیر قبلی را در scope داخلی، "shadow" میکند و هر ارجاعی به این متغیر در آن محدوده، به متغیر جدید اشاره میکند.
fn main() { let x = 5; println!("The value of x is: {}", x); // خروجی برابر 5 است let x = "hello"; println!("The value of x is: {}", x); // خروجی برابر hello }
سایه زدن زمانی میتواند مفید باشد که بخواهید مقدار، یا نوع یک متغیر را در یک scope، بدون اینکه نام آن را تغییر دهید. همچنین میتواند کد را با استفادهی مجدد از یک نام متغیر، برای اهداف مختلف خواناتر کند. با این حال، همچنین میتواند کد را پیچیدهتر و درک آن را سختتر کند؛ بنابراین باید با دقت و با دلیل موجه از آن استفاده کنید .
- Singleton
- Scoped
- Transient
Singleton (یگانه)
فقط و فقط یک شیء از سرویس ثبت شده با این طول عمر، در اولین درخواست ایجاد میشود و سپس در کل طول حیات برنامه، از همین شیء ایجاد شده، استفاده میگردد. به همین دلیل به آن «یگانه» یا Singleton میگویند. هر زمانیکه این سرویس در خواست داده میشود، DI Container، همان یک شیء را در اختیار درخواست دهنده قرار میدهد و این شیء، هیچگاه از بین نمیرود. به بیان دیگر، DI Container هیچگاه این شیء را از بین نمیبرد. شیء ساخته شده از سرویس ثبت شدهی با حالت Singleton، بین تمامی استفاده کنندگان، به صورت اشتراکی استفاده میشود. این طول عمر تقریبا مشابهی اشیاء ساخته شده توسط Singleton Pattern عمل میکند.
با توجه به مطالب گفته شده، ویژگیهای سرویسهای Singleton به شرح زیر هستند:
- در اولین درخواست به سرویس، یک نمونه از آن ساخته میشود و تا پایان برنامه در حافظه نگه داشته میشود.
- در سایر درخواستها، همان یک نمونهی ساخته شدهی از سرویس، ارائه داده میشود.
- به علت موجود بودن در حافظه، معمولا دسترسی به آنها و عملکرد آنها سریعتر است.
- بار کاری بر روی Garbage Collector فریمورک را کاهش میدهند.
بنابراین در هنگام تعریف کردن یک سرویس به صورت Singleton باید نکات زیر را مد نظر قرار بدهید:
- باید سرویس مورد نظر Thread Safe باشد .
- نباید استفاده کنندهی از این سرویس، امکان تغییر State آن را داشته باشد.
- اگر ساخت شیءای از یک سرویس، هزینهی زیادی را داشته باشد ، احتمالا Singleton کردن آن میتواند ایدهی خوبی باشد.
- شیء ساخته شدهی از این سرویس، تا زمان اجرای برنامه، بخشی از حافظهی برنامه را اشغال میکند. پس باید حجم اشغالی در حافظه را نیز مد نظر قرار داد.
- تعداد دفعات استفاده را در برابر مصرف حافظه در نظر بگیرید.
services.AddSingleton(services => new GuidProvider());
public HomeController(ILogger<HomeController> logger, IMessageServiceA messageService, LiteDbConfig liteDbConfig, GuidProvider guidHelper) { _logger = logger; _messageService = messageService; _messageService = new MessageServiceAA(); _guidHelper = guidHelper; }
حالا اگر برنامه
را اجرا کنیم، میبینید که با تازه
سازی صفحهی Home/Index ، همچنان Id، برابر با یک رشتهی یکسان
است. حتی اگر تب دیگری را در مرورگر باز کنیم و دوباره به این صفحه برویم، میبینیم
که Id برابر همان
رشتهی قبلی است و دلیل این موضوع، ثبت
سرویس Guid Service به صورت Singleton است.
Scoped ( محدود شده )
به ازای هر درخواست (در اینجا معمولا درخواستهای Http مد نظر است) یک نمونه از این سرویس ساخته میشود و در طول حیات این درخواست، DI Container به هر کلاسی که به این سرویس نیاز دارد، همان یک نمونه را برگشت میدهد و این نمونه در کل طول اجرای این درخواست، بین تمامی سرویس گیرندگان، یکسان است. هر زمانی، درخواست به پایان برسد، نمونهی ساخته شده از سرویس، Disposed میگردد و GC میتواند آن را از بین ببرد.
معمولا سرویسهای اتصال به پایگاه دادهها و کار بر روی آنها که شامل خواندن، نوشتن، ویرایش، حذف میشوند را با طول حیات Scoped ، درون DI Container ثبت میکنند . EF Core به صورت پیش فرض ، Db Context را به صورت Scoped ثبت میکند.
سرویسهای Scoped در محدودهی درخواست، مانند Singleton عمل میکنند و شیء ساخته شده و وضعیت آن در بین تمامی سرویسهایی که به آن نیاز دارند، مشترک است. بنابراین باید به این نکته در هنگام تعریف سرویس به صورت Scoped ، توجه داشته باشید.
تمام Middleware های ASP.NET Core هم فقط همان نمونهی ایجاد شده از سرویس Scoped را در طی اجرای یک درخواست خاص، میگیرند.
هر سرویسی که به سرویسهای Scoped نیاز دارد، یا باید به صورت Transient و یا باید به صورت Scoped ثبت شود، تا مانع از این شویم که شیء ساخته شده، فراتر از طول حیات موردنظرمان، در حافظه بماند و از آن استفاده شود .
برای ثبت یک سرویس
به صورت Scoped میتوانیم از متدهای توسعهای با نام AddScoped() با سربارهای
مختلف بر روی IServiceCollection استفاده کنیم. در اینجا از نسخهای که دو
پارامتر جنریک را میگیرد، برای ثبت یک سرویس به صورت Scoped استفاده میکنیم:
services.AddScoped<IMessageServiceB, MessageServiceBA>();
می توانیم سرویس GuidProvider را به جای Signleton ، به صورت Scoped ثبت کنیم:
services.AddScoped(services => new GuidProvider());
Transient (گذرا)
به ازای هر درخواست دهندهی جدید، یک نمونهی جدید از سرویس، توسط DI Container ساخته میشود و در اختیار آن قرار میگیرد.
سرویسهایی را به این صورت ثبت کنید که:
- نیاز به Thread Safe بودن داشته باشند.
- نمی توانید طول عمر سرویس را حدس بزنید.
سرویسهای Transient ، کارآیی پائینتری دارند و سربار عملکردی زیادی بر روی Garbage Collector می گذارند؛ ولی به علت اینکه به ازای هر واکشی، یک نمونهی جدید از آنها ساخته میشود و State بین این اشیاء به اشتراک گذاشته نمیشود، امنیت بیشتری دارند و درک و خطایابی آنها سادهتر است.
برای ثبت سرویسهای Transient از متد توسعهای AddTransient() استفاده میکنیم. سربارهای این متد مانند سربارهای متدهای AddSingleton() و AddScoped() است:
services.AddTransient<IMessageServiceC, MessageServiceCA>();
وابستگیهای محصور شده
یک سرویس نباید وابستهی به سرویسی باشد که طول حیاتی کمتر از طول حیات خودش را دارد.
برای مثال اگر درون سرویسی با طول حیات Singleton، از یک سرویس با طول حیات Transient استفاده کنیم، اینکار باعث میشود که یک نمونه از سرویس Transient در طول حیات برنامه، همیشه درون حافظه بماند و این ممکن است باعث خطاهای عجیبی در هنگام اجرا شود که معمولا خطایابی و رفع آنها مشکل است.
اثرات جانبی وابستگیهای محصور شده:
- به اشتراک گذاری تصادفی وضعیت یک شیء بین Thread ها درون سرویسهایی که Thread Safe نیستند.
- اشیاء، بیش از زمان پیش بینی شدهی برایشان، درون حافظه باقی میمانند.
سرویسهای Transient میتوانند به سرویسهایی با طول حیات زیر وابستگی داشته باشند:
- Transient
- Scoped
- Singleton
سرویسهای Scoped میتوانند به سرویسهایی با طول حیات زیر وابستگی داشته باشند:
- Transient
- Scoped
سرویسهای Singleton میتوانند به سرویس هایی با طول حیات زیر وابستگی داشته باشند:
Singleton
میتوانید از جدول زیر به عنوان راهنمای خلاصه شدهی برای استفادهی امن از سرویسها درون یکدیگر بهره ببرید:
Scope Validation
این قابلیت که به صورت پیش
فرض در حالت توسعهی برنامههای ASP.NET Core فعال است، در زمان شروع برنامه و در Startup ، بررسی میکند که سرویسها، وابستگی
به سرویسهایی با طول حیاتی مناسب، داشته باشند.
تعریف متدها در برنامه نویسی:
متدها جزء اولین چیزهایی هستند که در هنگام شروع برنامه نویسی در هر یک از زبانهای برنامه نویسی، برنامه نویس با آنها آشنا میشود. بنابراین متدها به عنوان اصلیترین Building Block ها در زبانهای برنامه نویسی دارای اهمیت بسیار زیادی میباشند. متدها اولین جاهایی هستند که ما میتوانیم کار خودمان را از آنها شروع کنیم و به سوی هدف خود حرکت کنیم.
همان طور که میدانید شما در هنگام کد نویسی یکسری عملکردها را با ارسال یکسری پارمترها به متدها و فراخوانی آنها، بر عهدهی یک متد خاص میگذارید. بعد از فراخوانی انتظار دارید که متد، یک نوع دادهی خاص را برگرداند یا اینکه انتظار ندارید هیچ مقداری را برگرداند. همچنین احتمال دارد در هنگام اجرای متدی، یکسری خطاها رخ دهند و برنامه نویس باید سعی کند همهی آنها را به درستی مدیریت کند. اما آیا به نظر شما مواردی که در مورد متدها دارای اهمیت میباشند، فقط اینها هستند؟
بسیاری از برنامه نویسان انتظاری که از متدها دارند فقط در همین حد میباشد و بیشتر از این درگیر هیچ مسئلهی دیگری نمیشوند. اما آیا فقط در نظر گرفتن این مسایل در رسیدن به یک کد خوش ساخت، قابل توسعه و بدون پیچیدگی کافی است؟
متدها علاوه بر ویژگیهای ذکر شدهی در بالا که بیشتر بر ویژگیهای ذاتی و عملکردی آن تمرکز داشت، باید داری یکسری ویژگیهای دیگر نیز باشند، متدها باید Clean ، Testable و Predictable باشند. هر کدام از ویژگیهای مذکور توسط این پارامترها تشریح میشوند.
در ذیل ویژگیهای مذکور در شکل بالا را تشریح خواهیم کرد.
Clear Purpose :
· یک متد یک کار را انجام میدهد و همچنین آن کار را نیز به خوبی انجام میدهد.
· متد به راحتی قابل درک میباشد.
· میزان خطاها را به شدت کاهش میدهد.
· دیباگ کردن را در صورت وجود هر خطایی سادهتر میکند.
· قابلیت توسعه را افزایش میدهد.
· نوشتن تست برای این نوع متدها به دلیل اینکه فقط بر روی یک هدف خاص تمرکز دارند ساده است.
Good Name :
· نام متد عمکرد آن را به روشنی بیان میکند
Focused Code :
· تمام کد نوشته شدهی در متد فقط بر روی یک هدف تمرکز دارند.
· خوانایی کد بالا است و میزان توضیحاتی که برای کد نوشته میشود در کمترین حد ممکن است.
· متد دارای تاثیرات ناخواستهای بر سایر قسمتهای نرم افزار نمیباشد. این مسئله به معنی است که این نوع متدها شامل کدهایی که کارهای ناخواسته ای را انجام میدهند نمیباشد. برای مثال متدی که برای واکشی اطلاعات مشتریان استفاده میشود هیچگونه عملیاتی را که برای ثبت اطلاعات مشتریان انجام میشود، انجام نمیدهد.
Short Length :
· تعداد خطوط کد مربوط به متد کم میباشد. این مسئله خود باعث کاهش باگهای احتمالی در یک متد میشود.
Automated Code Test :
· متد این قابلیت را دارد که توسط زیر ساختهای تست، تست شود که این مسئله خود باعث افزایش کیفیت کد میشود.
Predictable Result :
· متد دارای یک نتیجهی قابل پیش بینی میباشد.
در ادامه سعی میکنیم با ذکر یک مثال، مواردی را که ذکر شد بیشتر توضیح دهیم و دیدگاه کاربردی آن را بررسی کنیم.
مثالی از دنیای واقعی:
مثال زیر فرآیند مربوط به دریافت سفارش از مشتری را به صورت کدی محاورهای نمایش میدهد. در این مثال سعی شده مشکلات و راه حلهای پیشنهادی Defensive Code با تمرکز بر مواردی که در قسمت قبل ذکر شد به صورت کامل تشریح شود. اکثر ما به عنوان برنامه نویس، با مواردی مانند شکل ذیل مواجه شدهایم. در این حالت در هنگام طراحی نرم افزار برنامه نویس مستقیما وارد رویداد مربوط به کلیک دکمهی ثبت اطلاعات سفارش میشود و به صورت کاملا ناباورانه و با پشتکاری مثال زدنی شروع به کد زدن میکند. برنامه نویس تمامی منطق دریافت اطلاعات از کاربر و ثبت مشترک، ایجاد درخواست برای مشترک، مراحل دریافت کالا از انبار، پرداخت، ارسال ایمیل به مشتری و سایر عملیاتهای دیگر را در این متد و پشت سر هم مینویسد:
این روش کد نویسی روشی است که اکثر برنامه نویسان با آن آشنایی دارند. اولین مشکلی که این روش دارد این است که این کد Clean نمیباشد. قابلیت توسعه و نگهداری این کد بسیار پایین میباشد و به اصطلاح یک کد کاملا باگ خیز میباشد. حال نوبت این رسیده که این کد را از نظر پارامترهایی که در بالا ذکر شد بررسی کنیم.
Clear Purpose :
آیا این متد دارای یک هدف مشخص و معین است؟ بیایید بررسی کنیم، ایجاد مشتری، ایجاد سفارش، ارسال درخواست به انبار، انجام عملیات پرداخت و ارسال رسید. همهی این کارها توسط این متد انجام میشود. خودتان در مورد تبعات این روش کد نویسی قضاوت کنید.
Clear Name :
به نظر شما چگونه میتوان یک اسم مناسب برای این متد انتخاب کرد که عملکرد آن را به درستی بیان کند. هر متد به یک نام مناسب نیاز دارد که این مسئله خود قابلیت توسعه و نگهداری کد را افزایش میدهد. این نام میتواند اطلاعات کاملی را در مورد متد ارائه دهد و عملکرد کلی آن را بیان نماید. هدف متد باید از طریق نام متد بیان شود و هنگامیکه شما نتوانید برای متد مد نظر یک نام را انتخاب کنید، بنابراین این متد دارای هدفی مشخص نمیباشد.
Focused Code :
متد باید کاری را انجام دهد که نام آن بیان میکند و تمام کدهای متد باید حتما بر روی آن هدف تمرکز کنند.
Short Length :
متد باید دارای طول کمی باشد. برای مثال طول کد نباید از اندازهی صفحه نمایش بیشتر باشد. به عبارتی دیگر، کد متد نباید اسکرول بخورد. بنابراین باید سعی شود کدهای اضافی از متد حذف شوند تا با این کار پیچیدگی کد هم به کمترین میزان برسد.
Automated Code Test :
آیا این متد به وسیلهی Automated Code Test می تواند تست شود؟ چند نکته در مورد این کد وجود دارد که توانایی Automated Code Test را از این کد میگیرد. اولین مسئله این است که در این مثال منطق یا همان Business برنامه با UI تلفیق شدهاست. برای رفع این مشکل باید منطق برنامه را در یک پروژهی مجزا از نوع Class Library قرار داد. مسئلهی دیگر این است که این متد برای تست شدن بسیار طولانی میباشد و باید به یکسری اجزای کوچکتر و منطقیتر شکسته شود و هر متد باید یک هدف و عملکرد روشن را داشته باشد.
در قسمت بعدی راهکارهایی برای Refactor کردن کد بر اساس اصول ذکر شده ارائه خواهد شد.
روش مرسوم طراحی Fluent interfaces، جهت ارائه روش ساخت اشیاء مسطح به کاربران بسیار مناسب هستند. اما اگر سعی در تهیه API عمومی برای کار با اشیاء چند سطحی مانند معرفی فایلهای XML توسط کلاسهای سی شارپ کنیم، اینبار Fluent interfaces آنچنان قابل استفاده نخواهند بود و نمیتوان این نوع اشیاء را به شکل روانی با کنار هم قرار دادن زنجیر وار متدها تولید کرد. برای حل این مشکل روش طراحی خاصی در نگارشهای اخیر NHibernate معرفی شده است به نام loquacious interface که این روزها در بسیاری از APIهای جدید شاهد استفاده از آن هستیم و در ادامه با پشت صحنه و طرز تفکری که در حین ساخت این نوع API وجود دارد آشنا خواهیم شد.
در ابتدا کلاسهای مدل زیر را در نظر بگیرید که قرار است توسط آنها ساختار یک جدول از کاربر دریافت شود:
using System; using System.Collections.Generic; namespace Test { public class Table { public Header Header { set; get; } public IList<Cell> Cells { set; get; } public float Width { set; get; } } public class Header { public string Title { set; get; } public DateTime Date { set; get; } public IList<Cell> Cells { set; get; } } public class Cell { public string Caption { set; get; } public float Width { set; get; } } }
using System; using System.Collections.Generic; namespace Test { public class TableApi { public Table CreateTable(Action<TableCreator> action) { var creator = new TableCreator(); action(creator); return creator.TheTable; } } public class TableCreator { readonly Table _theTable = new Table(); internal Table TheTable { get { return _theTable; } } public void Width(float value) { _theTable.Width = value; } public void AddHeader(Action<HeaderCreator> action) { _theTable.Header = ... } public void AddCells(Action<CellsCreator> action) { _theTable.Cells = ... } } }
همچنین در بدنه متد CreateTable، نکته نحوه دریافت خروجی از Action ایی که به ظاهر خروجی خاصی را بر نمیگرداند نیز قابل مشاهده است.
همانطور که عنوان شد کلاسهای xyzCreator تا رسیدن به خواص معمولی و ابتدایی پیش میروند. برای مثال در سطح اول چون خاصیت عرض از نوع float است، صرفا با یک متد معمولی دریافت میشود. دو خاصیت دیگر نیاز به Creator دارند تا در سطحی دیگر برای آنها سازندههای سادهتری را طراحی کنیم.
همچنین باید دقت داشت که در این طراحی تمام متدها از نوع void هستند. اگر قرار است خاصیتی را بین خود رد و بدل کنند، این خاصیت به صورت internal تعریف میشود تا در خارج از کتابخانه قابل دسترسی نباشد و در intellisense ظاهر نشود.
مرحله بعد، ایجاد دو کلاس HeaderCreator و CellsCreator است تا کلاس TableCreator تکمیل گردد:
using System; using System.Collections.Generic; namespace Test { public class CellsCreator { readonly IList<Cell> _cells = new List<Cell>(); internal IList<Cell> Cells { get { return _cells; } } public void AddCell(string caption, float width) { _cells.Add(new Cell { Caption = caption, Width = width }); } } public class HeaderCreator { readonly Header _header = new Header(); internal Header Header { get { return _header; } } public void Title(string title) { _header.Title = title; } public void Date(DateTime value) { _header.Date = value; } public void AddCells(Action<CellsCreator> action) { var creator = new CellsCreator(); action(creator); _header.Cells = creator.Cells; } } }
مقدار هر خاصیت معمولی توسط یک متد ساده void دریافت خواهد شد.
هر خاصیتی که اندکی پیچیدگی داشته باشد، نیاز به یک Creator جدید خواهد داشت.
کار هر Creator بازگشت دادن مقدار یک شیء است یا نهایتا ساخت یک لیست از یک شیء. این مقدار از طریق یک خاصیت internal بازگشت داده میشود.
البته عموما بجای معرفی مستقیم کلاسهای Creator از یک اینترفیس معادل آنها استفاده میشود. سپس کلاس Creator را internal تعریف میکنند تا خارج از کتابخانه قابل دسترسی نباشد و استفاده کننده نهایی فقط با توجه به متدهای void تعریف شده در interface کار تعریف اشیاء را انجام خواهد داد.
در نهایت، مثال تکمیل شده ما به شکل زیر خواهد بود:
using System; using System.Collections.Generic; namespace Test { public class TableCreator { readonly Table _theTable = new Table(); internal Table TheTable { get { return _theTable; } } public void Width(float value) { _theTable.Width = value; } public void AddHeader(Action<HeaderCreator> action) { var creator = new HeaderCreator(); action(creator); _theTable.Header = creator.Header; } public void AddCells(Action<CellsCreator> action) { var creator = new CellsCreator(); action(creator); _theTable.Cells = creator.Cells; } } public class CellsCreator { readonly IList<Cell> _cells = new List<Cell>(); internal IList<Cell> Cells { get { return _cells; } } public void AddCell(string caption, float width) { _cells.Add(new Cell { Caption = caption, Width = width }); } } public class HeaderCreator { readonly Header _header = new Header(); internal Header Header { get { return _header; } } public void Title(string title) { _header.Title = title; } public void Date(DateTime value) { _header.Date = value; } public void AddCells(Action<CellsCreator> action) { var creator = new CellsCreator(); action(creator); _header.Cells = creator.Cells; } } }
var data = new TableApi().CreateTable(table => { table.Width(1); table.AddHeader(header=> { header.Title("new rpt"); header.Date(DateTime.Now); header.AddCells(cells=> { cells.AddCell("cell 1", 1); cells.AddCell("cell 2", 2); }); }); table.AddCells(tableCells=> { tableCells.AddCell("c 1", 1); tableCells.AddCell("c 2", 2); }); });
این نوع طراحی مزیتهای زیادی را به همراه دارد:
الف) ساده سازی طراحی اشیاء چند سطحی و تو در تو
ب) امکان درنظر گرفتن مقادیر پیش فرض برای خواص
ج) سادهتر سازی تعاریف لیستها
د) استفاده کنندگان در حین استفاده نهایی و تعریف اشیاء به سادگی میتوانند کدنویسی کنند (مثلا سلولها را با یک حلقه اضافه کنند).
ه) امکان بهتر استفاده از امکانات Intellisense. برای مثال فرض کنید یکی از خاصیتهایی که قرار است برای آن Creator درست کنید یک interface را میپذیرد. همچنین در برنامه خود چندین پیاده سازی کمکی از آن نیز وجود دارد. یک روش این است که مستندات قابل توجهی را تهیه کنید تا این امکانات توکار را گوشزد کند؛ روش دیگر استفاده از طراحی فوق است. در اینجا در کلاس Creator ایجاد شده چون امکان معرفی متد وجود دارد، میتوان امکانات توکار را توسط این متدها نیز معرفی کرد و به این ترتیب Intellisense تبدیل به راهنمای اصلی کتابخانه شما خواهد شد.
زبان برنامه نویسی Erlang
ابتدا شیء user، در بالاترین سطح، دریافت شده و به صفحهای خاص از طریق ویژگیهای props ارسال میشود:
<Page user={user} />
<PageLayout user={user} />
<NavigationBar user={user} />
ایجاد شیء Context در برنامههای React
React Context، راه حلی است جهت به اشتراک گذاری دادهها، در بین انواع و اقسام کامپوننتهای یک برنامه، بدون اینکه نیازی باشد این اطلاعات را توسط props، از یک سطح، به سطحی دیگر، به صورت دستی انتقال داد. برای ایجاد یک نمونهی از آن، ابتدا پوشهی جدید src\contexts را افزوده و سپس فایل src\contexts\userContext.js را درون آن، با محتوای زیر ایجاد میکنیم:
import React from "react"; export const UserContext = React.createContext({ user: {} }); export const UserProvider = UserContext.Provider; export const UserConsumer = UserContext.Consumer;
تامین یک شیء Context در برنامه، در یک کامپوننت کلاسی و یا تابعی
تا اینجا یک شیء Context را به همراه اجزای export شدهی Provider و Consumer آن ایجاد کردیم. اکنون نوبت به پیاده سازی قسمت Provider آن است:
import "../../App.css"; import React, { Component } from "react"; import { UserProvider } from "../../contexts/userContext"; import Main from "./Main"; class App extends Component { state = { user: { name: "User 1" } }; componentDidMount() { // get user from the server or local storage and then set the currently logged in user to the this.state } render() { return ( <> <h1>App Class</h1> <UserProvider value={this.state.user}> <Main /> </UserProvider> </> ); } } export default App;
در ادامه قصد داریم اطلاعات این شیء user موجود در state را با تمام کامپوننتهایی که در درخت رندر کامپوننت جاری قرار میگیرند و با کامپوننت Main شروع میشوند، به اشتراک بگذاریم. این به اشتراک گذاری با import شیء UserProvider از ماژول contexts/userContext به نحوی که مشاهده میکنید، انجام میشود. شیء UserProvider، کار محصور سازی کامپوننت Main را انجام میدهد. سپس این Provider میتواند مقداری را توسط ویژگی value خود دریافت کند که برای مثال در اینجا شیء user است. اکنون این value تا n سطح بعدی که از کامپوننت Main مشتق میشوند نیز در دسترس خواهد بود.
یک نکته: متد React.createContext به همراه یک آرگومان defaultValue اختیاری است که در اختیار Consumerهای آن قرار داده میشود؛ اگر Provider متناظر با آن، در درخت کامپوننتهای برنامه، یافت نشود. یعنی تعریف Provider الزامی نیست. اگر نیاز است مقدار ثابتی را بین چندین کامپوننت به اشتراک بگذارید، فقط کافی است آنها را توسط React.createContext مقدار دهی اولیه کرده و ... استفاده کنید:
export const DefaultRouteContext = React.createContext({ path: '/welcome' });
خواندن شیء Context در کامپوننتی دیگر
اکنون که یک تامین کنندهی Context را ایجاد کردیم، برای خواندن اطلاعات آن در درخت کامپوننتهای محصور شدهی توسط UserProvider، میتوان به صورت زیر عمل کرد:
import React from "react"; import { UserConsumer } from "../../contexts/userContext"; export default function Main(props) { return ( <> <UserConsumer> {value => <div>User name: {value.name}.</div>} </UserConsumer> </> ); }
خروجی برنامه پس از این تغییرات به صورت زیر است:
ساده سازی دسترسی به UserConsumer توسط useContext Hook
نحوهی تعریف یک Provider و محصور سازی فرزندانی که باید از آن ارثبری کنند، در بین کامپوننتهای کلاسی و تابعی، یکی است. اما در کامپوننتهای تابعی حداقل میتوان نحوهی دسترسی به UserConsumer را به نحو زیر توسط useContext Hook ساده کرد:
import React, { useContext } from "react"; import { UserContext } from "../../contexts/userContext"; export default function Main() { const value = useContext(UserContext); return ( <> <div>User name: {value.name}.</div> </> ); }
مزیت دیگر این روش، ساده سازی کار با چندین شیء Context است. برای مثال اگر دو شیء Context را تعریف کرده باشید، خواندن دو مقدار از آنها، پیشتر چنین شکل تو در تویی را توسط دو Consumer پیدا میکرد:
function HeaderBar() { return ( <CurrentUser.Consumer> {user => <Notifications.Consumer> {notifications => <header> Welcome back, {user.name}! You have {notifications.length} notifications. </header> } } </CurrentUser.Consumer> ); }
function HeaderBar() { const user = useContext(CurrentUser); const notifications = useContext(Notifications); return ( <header> Welcome back, {user.name}! You have {notifications.length} notifications. </header> ); }
ارسال اطلاعات به کامپوننت Context Provider، از طریق کامپوننتهای فرزند
تا اینجا با استفاده از React Context، اطلاعات یک Provider را با فرزندان آن به اشتراک گذاشتیم؛ عکس این عمل نیز میسر است. برای اینکار، همانند تمام کامپوننتهای دیگری که برای ارسال اطلاعات به فراخوان خود از طریق رخدادها عمل میکنند، میتوان یک متد رویدادگردان را در کامپوننت والد، به استفاده کنندهی از Context ارسال کرد:
import "../../App.css"; import React, { Component } from "react"; import { UserProvider } from "../../contexts/userContext"; import Main from "./Main2"; class App extends Component { state = { user: { name: "User 1" } }; componentDidMount() { // get user from the server or local storage and then set the currently logged in user to the this.state } logout = () => { console.log("logout"); this.setState({ user: {} }); }; render() { const contextValue = { user: this.state.user, logoutUser: this.logout }; return ( <> <h1>App Class</h1> <UserProvider value={contextValue}> <Main /> </UserProvider> </> ); } } export default App;
اکنون تمام استفاده کنندههای از این UserProvider میتوانند با فراخوانی متد منتسب به logout، سبب پاک شدن اطلاعات کاربر موجود در state کامپوننت App، به روز رسانی state و در نتیجهی آن، رندر مجدد کامپوننت و ارائهی یک UserProvider جدید، با اطلاعاتی جدید به فرزندان آن شوند:
import React, { useContext } from "react"; import { UserContext } from "../../contexts/userContext"; export default function Main() { const { user, logoutUser } = useContext(UserContext); return ( <> <div>User name: {user.name}.</div> <button type="button" className="btn btn-primary" onClick={logoutUser}> Logout user </button> </> ); }
روش انجام اینکار بدون استفاده از useContext را نیز در ادامه مشاهده میکنید که در ابتدا نیاز به تعریف تابعی را دارد که همان خواص استخراجی را دریافت میکند. سپس باید بر اساس آنها، المانهای مدنظر نمایش نام کاربر و دکمهی خروج او را بازگشت داد:
import React from "react"; import { UserConsumer } from "../../contexts/userContext"; export default function Main(props) { return ( <> <UserConsumer> {({ user, logoutUser }) => ( <> <div>User name: {user.name}.</div> <button type="button" className="btn btn-primary" onClick={logoutUser} > Logout user </button> </> )} </UserConsumer> </> ); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-30-part-04.zip
در ابتدا کلاسهای مدل و Context برنامه را به شکل زیر درنظر بگیرید:
using System; using System.Data.Entity; using System.Data.Entity.Migrations; namespace TestKeys { public class Bill { public int Id { get; set; } public decimal Amount { set; get; } public virtual Account Account { get; set; } } public class Account { public int Id { get; set; } public string Name { get; set; } } public class MyContext : DbContext { public DbSet<Bill> Bills { get; set; } public DbSet<Account> Accounts { get; set; } } public class Configuration : DbMigrationsConfiguration<MyContext> { public Configuration() { AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; } protected override void Seed(MyContext context) { var a1 = new Account { Name = "a1" }; var a2 = new Account { Name = "a2" }; var bill1 = new Bill { Amount = 100, Account = a1 }; var bill2 = new Bill { Amount = 200, Account = a2 }; context.Bills.Add(bill1); context.Bills.Add(bill2); base.Seed(context); } } public static class Test { public static void Start() { Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>()); using (var ctx = new MyContext()) { var bill1 = ctx.Bills.Find(1); Console.WriteLine(bill1.Amount); } } } }
در اینجا کلاس صورتحساب و حساب مرتبط به آن تعریف شدهاند. سپس به کمک DbContext این دو کلاس در معرض دید EF Code first قرار گرفتهاند و در کلاس Configuration نحوه آغاز بانک اطلاعاتی به همراه تعدادی رکورد اولیه مشخص شده است.
نحوه صحیح مقدار دهی کلید خارجی در EF Code first
تا اینجا یک روال متداول را مشاهده کردیم. اکنون سؤال این است که اگر بخواهیم اولین رکورد صورتحساب ثبت شده توسط متد Seed را ویرایش کرده و مثلا حساب دوم را به آن انتساب دهیم، بهینهترین روش چیست؟ بهینهترین در اینجا منظور روشی است که کمترین تعداد رفت و برگشت به بانک اطلاعاتی را داشته باشد. همچنین فرض کنید در صفحه ویرایش، اطلاعات حسابها در یک Drop down list شامل نام و id آنها نیز وجود دارد.
روش اول:
using (var ctx = new MyContext()) { var bill1 = ctx.Bills.Find(1); var a2 = new Account { Id = 2, Name = "a2" }; bill1.Account = a2; ctx.SaveChanges(); }
به کمک متد Find اولین رکورد یافت شده و سپس بر اساس اطلاعات drop down در دسترس، یک شیء جدید حساب را ایجاد و سپس تغییرات لازم را اعمال میکنیم. در نهایت اطلاعات را هم ذخیره خواهیم کرد.
این روش به ظاهر کار میکنه اما حاصل آن ذخیره رکورد حساب سومی با id=3 در بانک اطلاعاتی است و سپس انتساب آن به اولین صورتحساب ثبت شده.
نتیجه: Id را دستی مقدار دهی نکنید؛ تاثیری ندارد. زیرا اطلاعات شیء جدید حساب، در سیستم tracking مرتبط با Context جاری وجود ندارد. بنابراین EF آنرا به عنوان یک شیء کاملا جدید درنظر خواهد گرفت، صرفنظر از اینکه Id را به چه مقداری تنظیم کردهاید.
روش دوم:
using (var ctx = new MyContext()) { var bill1 = ctx.Bills.Find(1); var a2 = ctx.Accounts.Find(2); bill1.Account = a2; ctx.SaveChanges(); }
مگر نه این است که اطلاعات نهایی ذخیره شده در بانک اطلاعاتی متناظر با حساب صورتحساب جاری، فقط یک عدد بیشتر نیست. بنابراین آیا نمیشود ما تنها همین عدد متناظر را بجای دریافت کل شیء به صورتحساب نسبت دهیم؟
پاسخ: بله. میشود! ادامه آن در روش سوم.
روش سوم:
در اینجا بهترین کار و یکی از best practices طراحی مدلهای EF این است که طراحی کلاس صورتحساب را به نحو زیر تغییر دهیم:
public class Bill { public int Id { get; set; } public decimal Amount { set; get; } [ForeignKey("AccountId")] public virtual Account Account { get; set; } public int AccountId { set; get; } }
اینبار به کمک خاصیت متناظر با کلید خارجی جدول، مقدار دهی و ویرایش کلیدهای خارجی یک شیء به سادگی زیر خواهد بود؛ خصوصا بدون نیاز به رفت و برگشت اضافی به بانک اطلاعاتی جهت دریافت اطلاعات متناظر با اشیاء تعریف شده به صورت navigation property :
using (var ctx = new MyContext()) { var bill1 = ctx.Bills.Find(1); bill1.AccountId = 2; ctx.SaveChanges(); }
وارد کردن یک شیء به سیستم Tracking
در قسمت قبل عنوان شد که Id را دستی مقدار دهی نکنید، چون تاثیری ندارد. سؤال: آیا میشود این شیء ویژه تعریف شده را به سیستم Tracking وارد کرد؟
پاسخ: بلی. به نحو زیر:
using (var ctx = new MyContext()) { var a2 = new Account { Id = 2, Name = "a2_a2" }; ctx.Entry(a2).State = System.Data.EntityState.Modified; ctx.SaveChanges(); }
public class WrongSingleton { static WrongSingleton _instance; WrongSingleton() { } public static WrongSingleton Instance { get { return _instance ?? (_instance = new WrongSingleton()); } } }
راه حلهای زیادی برای رفع این مشکل با اعمال مباحث قفل گذاری تا نکات ریز مربوط به کامپایلر وجود دارند که لیست آنها را در اینجا میتوانید مطالعه کنید.
از دات نت 4 به بعد با معرفی الگوی Lazy، امکان پیاده سازی lazy thread safe singletons به صورت توکار در دسترس میباشد. نمونهای از آن در کدهای IoC Container معروفی به نام StructureMap بکار رفتهاست:
public class Container { // ... } public static class ObjectFactory { private static readonly Lazy<Container> _containerBuilder = new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication); public static Container Container { get { return _containerBuilder.Value; } } private static Container defaultContainer() { return new Container(); } }
- چون این وهله توسط کلاس Lazy محصور شدهاست، صرفا در اولین بار دسترسی به آن، نمونه سازی خواهد شد. این مورد سبب کاهش مصرف حافظهی برنامه و همچنین بالا رفتن سرعت برپایی اولیهی آن میشود. بسیاری از اشیایی که در یک برنامه تعریف میشوند، شاید الزاما جهت ارائه راه حلی برای مسالهای خاص، مورد استفاده قرار نگیرند. تعریف آنها به صورت Lazy، سربار نمونه سازی الزامی آنها را حذف خواهد کرد و آنرا به اولین بار استفاده از شیء مورد نظر، به تعویق خواهد انداخت.
- با استفاده از LazyThreadSafetyMode.ExecutionAndPublication، چندین ترد میتوانند به خاصیت Container دسترسی پیدا کنند، اما تنها یکی از آنها موفق به وهله سازی این کلاس خواهد شد. البته حالت ExecutionAndPublication، حالت پیش فرض است و الزاما نیازی به ذکر آن نیست.
اینبار بازنویسی کلاس ابتدای بحث با توجه به نکات ذکر شده به صورت زیر خواهد بود:
public sealed class LazySingleton { private static readonly Lazy<LazySingleton> _instance = new Lazy<LazySingleton>(() => new LazySingleton(), LazyThreadSafetyMode.ExecutionAndPublication); private LazySingleton() { } public static LazySingleton Instance { get { return _instance.Value; } } }
- تنها وهلهی در دسترس کلاس، به صورت استاتیک و نمونه سازی کلاس، توسط کلاس Lazy با پارامتر LazyThreadSafetyMode.ExecutionAndPublication انجام میشود.
- علت استفاده از lambda در سازندهی کلاس Lazy، امکان دسترسی به اعضای private کلاس، از طریق یک خاصیت static است.
اگر در یک محیط کاری به برنامه نویسها دقت کنید دو گروه را به وضوح میتوان تمایز داد. کسانی که برنامه نویسی میکنند تا اموراتشان بگذرد و کسانی که واقعا علاقمند به کارشان و دنیای برنامه نویسی هستند. به گروه اول میتوان IT worker نام داد و گروه دوم را Software developer نامید.
جدول ذیل تفاوتهای این دو گروه را بر میشمارد:
IT Workers | Software developers |
عموما 5 تا 9 ساعت در یک شرکت کار میکنند. | عموما 5 تا 9 ساعت در یک شرکت کار کرده و پس از مراجعت به منزل بر روی پروژههای شخصی کار میکنند. |
با اینکه هنوز در همان شرکت مشغول به کار است همیشه مشغول نق زدن است. احتمالا شاید بتواند همان موقعیت کاری را در یک شرکت دیگر نیز کسب کند. | تا زمانیکه شغل فعلی برای او جذابیت دارد به آن ادامه خواهد داد و ترسی از حضور در شرکتهای دیگر ندارد. |
تنها محل یادگیری او همان پروژههایی است که در شرکت وجود دارند یا مشغول به کار بر روی آنها است. دید کاری و آموزشی او تنها به همین موارد خلاصه میشود. | به صورت مداوم مشغول خواندن بلاگها، کتابهای جدید و فراگیری نحوهی استفاده از برنامههای جدید میباشد. |
عموما و اکثریت آنها فقط به خاطر کلاس کاری به این رشته روی آوردهاند و نه اصل کار مربوطه. | به شدت علاقمند به بهبود روشهای توسعه کاری و همچنین بهبود وضعیت خویش هستند. |
اگر احتمالا بلاگی داشته باشند تنها به توضیح همان نق زدنهای رایج در محیط کار میپردازند. | از بلاگ خود در جهت توضیح تجارب کاری و کمک به ارتقای سایر همکاران خود استفاده میکنند. |
اگر دانشی را کسب میکنند تنها محل عرضهی آن جهت پز دادن پیش مدیر پروژه خواهد بود. | بسیار با معلومات اما افتاده حال هستند. |
از تغییرات مداوم دنیای IT که در آن قرار دارند هراسان هستند. مدام نق میزنند که مگر فاکس پروی 2.6 چه مشکلی دارد که باید از NHibernate استفاده کنند؟! این نوع افراد همیشه میگویند که وقت ندارند مطالب جدید را بیاموزند و میل به تحجر و مقاومت در برابر تغییرات در آنها بسیار زیاد است. | در تغییرات روی داده در دنیای IT سهیم بوده و جزئی از آن هستند. |
زمانیکه قرار است یک قطعه کد اس کیوال را نمایش دهند از یک برچسب ساده یا یک تکست باکس استفاده میکنند. در حدی که فقط به قولی برنامه "کار کند". در همان حدی کار میکنند که به آنها حقوق میدهند. نه بیشتر. | چند روز وقت میگذارند و با روشهای مختلف syntax highlighting و نمایش زیبای کد آشنا میشوند تا کاری را که ارائه میدهند مزهی غذای ماندهی چند روز قبل را ندهد. |
برای مطالعه بیشتر
+ و + و +