public class Client { public delegate string Invoker(); public static Invoker Execute;//اضافه کردن یک آیتم جدید public static Invoker Redo;//حرکت به جلو public static Invoker Undo;//حرکت به عقب }
public class Command { public Command(Receiver receiver) { Client.Execute = receiver.Action; Client.Redo = receiver.Foreward; Client.Undo = receiver.Reverse; } }
public class Receiver { private readonly List<string> build = new List<string>(); private readonly List<string> oldBuild = new List<string>(); public string Action() { if (build.Count > 0) oldBuild.Add(build.LastOrDefault()); build.Add(build.Count.ToString(CultureInfo.InvariantCulture)); return build.LastOrDefault(); } public string Reverse() { string last = oldBuild.LastOrDefault(); if (last == null) return "EMPTY"; oldBuild.Remove(last); return last; } public string Foreward() { string oldIndex = oldBuild.LastOrDefault(); int index = oldIndex == null ? -1 : build.IndexOf(oldIndex); if ((index + 1) == build.Count) return "END"; oldBuild.Add(build.ElementAt(index + 1)); return oldBuild.LastOrDefault(); } }
new Command(new Receiver()); Console.WriteLine(Client.Execute()); Console.WriteLine(Client.Execute()); Console.WriteLine(Client.Undo()); Console.WriteLine(Client.Undo()); Console.WriteLine(Client.Undo()); Console.WriteLine(Client.Redo()); Console.WriteLine(Client.Redo()); Console.WriteLine(Client.Redo()); Console.WriteLine(Client.Execute());
EF Code First #1
در ادامه بحث ASP.NET MVC میشود به ابزاری به نام MVC Scaffolding اشاره کرد. کار این ابزار که توسط یکی از اعضای تیم ASP.NET MVC به نام استیو اندرسون تهیه شده، تولید کدهای اولیه یک برنامه کامل ASP.NET MVC از روی مدلهای شما میباشد. حجم بالایی از کدهای تکراری آغازین برنامه را میشود توسط این ابزار تولید و بعد سفارشی کرد. MVC Scaffolding حتی قابلیت تولید کد بر اساس الگوی Repository و یا نوشتن Unit tests مرتبط را نیز دارد. بدیهی است این ابزار جای یک برنامه نویس را نمیتواند پر کند اما کدهای آغازین یک سری کارهای متداول و تکراری را به خوبی میتواند پیاده سازی و ارائه دهد. زیر ساخت این ابزار، علاوه بر ASP.NET MVC، آشنایی با Entity framework code first است.
در طی سری ASP.NET MVC که در این سایت تا به اینجا مطالعه کردید من به شدت سعی کردم از ابزارگرایی پرهیز کنم. چون شخصی که نمیداند مسیریابی چیست، اطلاعات چگونه به یک کنترلر منتقل یا به یک View ارسال میشوند، قراردادهای پیش فرض فریم ورک چیست یا زیر ساخت امنیتی یا فیلترهای ASP.NET MVC کدامند، چطور میتواند از ابزار پیشرفته Code generator ایی استفاده کند، یا حتی در ادامه کدهای تولیدی آنرا سفارشی سازی کند؟ بنابراین برای استفاده از این ابزار و درک کدهای تولیدی آن، نیاز به یک پیشنیاز دیگر هم وجود دارد: «Entity framework code first»
امسال دو کتاب خوب در این زمینه منتشر شدهاند به نامهای:
Programming Entity Framework: DbContext, ISBN: 978-1-449-31296-1
Programming Entity Framework: Code First, ISBN: 978-1-449-31294-7
که هر دو به صورت اختصاصی به مقوله EF Code first پرداختهاند.
در طی روزهای بعدی EF Code first را با هم مرور خواهیم کرد و البته این مرور مستقل است از نوع فناوری میزبان آن؛ میخواهد یک برنامه کنسول باشد یا WPF یا یک سرویس ویندوز NT و یا ... یک برنامه وب.
البته از دیدگاه مایکروسافت، M در MVC به معنای EF Code first است. به همین جهت MVC3 به صورت پیش فرض ارجاعی را به اسمبلیهای آن دارد و یا حتی به روز رسانی که برای آن ارائه داده نیز در جهت تکمیل همین بحث است.
مروری سریع بر تاریخچه Entity framework code first
ویژوال استودیو 2010 و دات نت 4، به همراه EF 4.0 ارائه شدند. با این نگارش امکان استفاده از حالتهای طراحی database first و model first مهیا است. پس از آن، به روز رسانیهای EF خارج از نوبت و به صورت منظم، هر از چندگاهی ارائه میشوند و در زمان نگارش این مطلب، آخرین نگارش پایدار در دسترس آن 4.3.1 میباشد. از زمان EF 4.1 به بعد، نوع جدیدی از مدل سازی به نام Code first به این فریم ورک اضافه شد و در نگارشهای بعدی آن، مباحث DB migration جهت ساده سازی تطابق اطلاعات مدلها با بانک اطلاعاتی، اضافه گردیدند. در روش Code first، کار با طراحی کلاسها که در اینجا مدل دادهها نامیده میشوند، شروع گردیده و سپس بر اساس این اطلاعات، تولید یک بانک اطلاعاتی جدید و یا استفاده از نمونهای موجود میسر میگردد.
پیشتر در روش database first ابتدا یک بانک اطلاعاتی موجود، مهندسی معکوس میشد و از روی آن فایل XML ایی با پسوند EDMX تولید میگشت. سپس به کمک entity data model designer ویژوال استودیو، این فایل نمایش داده شده و یا امکان اعمال تغییرات بر روی آن میسر میشد. همچنین در روش دیگری به نام model first نیز کار از entity data model designer جهت طراحی موجودیتها آغاز میگشت.
اما با روش Code first دیگر در ابتدای امر مدل فیزیکی و یک بانک اطلاعاتی وجود خارجی ندارد. در اینجا EF تعاریف کلاسهای شما را بررسی کرده و بر اساس آن، اطلاعات نگاشتهای خواص کلاسها به جداول و فیلدهای بانک اطلاعاتی را تشکیل میدهد. البته عموما تعاریف ساده کلاسها بر این منظور کافی نیستند. به همین جهت از یک سری متادیتا به نام ویژگیها یا اصطلاحا data annotations مهیا در فضای نام System.ComponentModel.DataAnnotations برای افزودن اطلاعات لازم مانند نام فیلدها، جداول و یا تعاریف روابط ویژه نیز استفاده میگردد. به علاوه در روش Code first یک API جدید به نام Fluent API نیز جهت تعاریف این ویژگیها و روابط، با کدنویسی مستقیم نیز درنظر گرفته شده است. نهایتا از این اطلاعات جهت نگاشت کلاسها به بانک اطلاعاتی و یا برای تولید ساختار یک بانک اطلاعاتی خالی جدید نیز میتوان کمک گرفت.
مزایای EF Code first
- مطلوب برنامه نویسها! : برنامه نویسهایی که مدتی تجربه کار با ابزارهای طراح را داشته باشند به خوبی میدانند این نوع ابزارها عموما demo-ware هستند. چندجا کلیک میکنید، دوبار Next، سه بار OK و ... به نظر میرسد کار تمام شده. اما واقعیت این است که عمری را باید صرف نگهداری و یا پیاده سازی جزئیاتی کرد که انجام آنها با کدنویسی مستقیم بسیار سریعتر، سادهتر و با کنترل بیشتری قابل انجام است.
- سرعت: برای کار با EF Code first نیازی نیست در ابتدای کار بانک اطلاعاتی خاصی وجود داشته باشد. کلاسهای خود را طراحی و شروع به کدنویسی کنید.
- سادگی: در اینجا دیگر از فایلهای EDMX خبری نیست و نیازی نیست مرتبا آنها را به روز کرده یا نگهداری کرد. تمام کارها را با کدنویسی و کنترل بیشتری میتوان انجام داد. به علاوه کنترل کاملی بر روی کد نهایی تهیه شده نیز وجود دارد و توسط ابزارهای تولید کد، ایجاد نمیشوند.
- طراحی بهتر بانک اطلاعاتی نهایی: اگر طرح دقیقی از مدلهای برنامه داشته باشیم، میتوان آنها را به المانهای کوچک و مشخصی، تقسیم و refactor کرد. همین مساله در نهایت مباحث database normalization را به نحوی مطلوب و با سرعت بیشتری میسر میکند.
- امکان استفاده مجدد از طراحی کلاسهای انجام شده در سایر ORMهای دیگر. چون طراحی مدلهای برنامه به بانک اطلاعاتی خاصی گره نمیخورند و همچنین الزاما هم قرار نیست جزئیات کاری EF در آنها لحاظ شود، این کلاسها در صورت نیاز در سایر پروژهها نیز به سادگی قابل استفاده هستند.
- ردیابی سادهتر تغییرات: روش اصولی کار با پروژههای نرم افزاری همواره شامل استفاده از یک ابزار سورس کنترل مانند SVN، Git، مرکوریال و امثال آن است. به این ترتیب ردیابی تغییرات انجام شده به سادگی توسط این ابزارها میسر میشوند.
- سادهتر شدن طراحیهای پیچیدهتر: برای مثال پیاده سازی ارث بری، ایجاد کلاسهای خود ارجاع دهنده و امثال آن با کدنویسی سادهتر است.
دریافت آخرین نگارش EF
برای دریافت و نصب آخرین نگارش EF نیاز است از NuGet استفاده شود و این مزایا را به همراه دارد:
به کمک NuGet امکان با خبر شدن از به روز رسانی جدید صورت گرفته به صورت خودکار درنظر گرفته شده است و همچنین کار دریافت بستههای مرتبط و به روز رسانی ارجاعات نیز در این حالت خودکار است. به علاوه توسط NuGet امکان دسترسی به کتابخانههایی که مثلا در گوگلکد قرار دارند و به صورت معمول امکان دریافت آنها برای ما میسر نیست، نیز بدون مشکل فراهم است (برای نمونه ELMAH، که اصل آن از گوگلکد قابل دریافت است؛ اما بسته نیوگت آن نیز در دسترس میباشد).
پس از نصب NuGet، تنها کافی است بر روی گره References در Solution explorer ویژوال استودیو، کلیک راست کرده و به کمک NuGet آخرین نگارش EF را نصب کرد. در گالری آنلاین آن، عموما EF اولین گزینه است (به علت تعداد بالای دریافت آن).
حین استفاده از NuGet جهت نصب Ef، ابتدا ارجاعاتی به اسمبلیهای زیر به برنامه اضافه خواهند شد:
System.ComponentModel.DataAnnotations.dll
System.Data.Entity.dll
EntityFramework.dll
بدیهی است بدون استفاده از NuGet، تمام این کارها را باید دستی انجام داد.
سپس در پوشهای به نام packages، فایلهای مرتبط با EF قرار خواهند گرفت که شامل اسمبلی آن به همراه ابزارهای DB Migration است. همچنین فایل packages.config که شامل تعاریف اسمبلیهای نصب شده است به پروژه اضافه میشود. NuGet به کمک این فایل و شماره نگارش درج شده در آن، شما را از به روز رسانیهای بعدی مطلع خواهد ساخت:
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="EntityFramework" version="4.3.1" />
</packages>
همچنین اگر به فایل app.config یا web.config برنامه نیز مراجعه کنید، یک سری تنظیمات ابتدایی اتصال به بانک اطلاعاتی در آن ذکر شده است:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=4.3.1.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</configSections>
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework">
<parameters>
<parameter value="Data Source=(localdb)\v11.0; Integrated Security=True; MultipleActiveResultSets=True" />
</parameters>
</defaultConnectionFactory>
</entityFramework>
</configuration>
همانطور که ملاحظه میکنید بانک اطلاعاتی پیش فرضی که در اینجا ذکر شده است، LocalDB میباشد. این بانک اطلاعاتی را از این آدرس نیز میتوانید دریافت کنید.
البته ضرورتی هم به استفاده از آن نیست و از سایر نگارشهای SQL Server نیز میتوان استفاده کرد ولی خوب ... مزیت استفاده از آن برای کاربر نهایی این است که «نیازی به یک مهندس برای نصب، راه اندازی و نگهداری ندارد». تنها مشکل آن این است که از ویندوز XP پشتیبانی نمیکند. البته SQL Server CE 4.0 این محدودیت را ندارد.
ضمن اینکه باید درنظر داشت EF به فناوری میزبان خاصی گره نخورده است و مثالهایی که در اینجا بررسی میشوند صرفا تعدادی برنامه کنسول معمولی هستند و نکات عنوان شده در آنها در تمام فناوریهای میزبان موجود به یک نحو کاربرد دارند.
قراردادهای پیش فرض EF Code first
عنوان شد که اطلاعات کلاسهای ساده تشکیل دهنده مدلهای برنامه، برای تعریف جداول و فیلدهای یک بانک اطلاعات و همچنین مشخص سازی روابط بین آنها کافی نیستند و مرسوم است برای پر کردن این خلاء از یک سری متادیتا و یا Fluent API مهیا نیز استفاده گردد. اما در EF Code first یک سری قرار داد توکار نیز وجود دارند که مطلع بودن از آنها سبب خواهد شد تا حجم کدنویسی و تنظیمات جانبی این فریم ورک به حداقل برسند. برای نمونه مدلهای معروف بلاگ و مطالب آنرا درنظر بگیرید:
namespace EF_Sample01.Models
{
public class Post
{
public int Id { set; get; }
public string Title { set; get; }
public string Content { set; get; }
public virtual Blog Blog { set; get; }
}
}
using System.Collections.Generic;
namespace EF_Sample01.Models
{
public class Blog
{
public int Id { set; get; }
public string Title { set; get; }
public string AuthorName { set; get; }
public IList<Post> Posts { set; get; }
}
}
یکی از قراردادهای EF Code first این است که کلاسهای مدل شما را جهت یافتن خاصیتی به نام Id یا ClassId مانند BlogId، جستجو میکند و از آن به عنوان primary key و فیلد identity جدول بانک اطلاعاتی استفاده خواهد کرد.
همچنین در کلاس Blog، خاصیت لیستی از Posts و در کلاس Post خاصیت virtual ایی به نام Blog وجود دارند. به این ترتیب روابط بین دو کلاس و ایجاد کلید خارجی متناظر با آنرا به صورت خودکار انجام خواهد داد.
نهایتا از این اطلاعات جهت تشکیل database schema یا ساختار بانک اطلاعاتی استفاده میگردد.
اگر به فضاهای نام دو کلاس فوق دقت کرده باشید، به کلمه Models ختم شدهاند. به این معنا که در پوشهای به همین نام در پروژه جاری قرار دارند. یا مرسوم است کلاسهای مدل را در یک پروژه class library مجزا به نام DomainClasses نیز قرار دهند. این پروژه نیازی به ارجاعات اسمبلیهای EF ندارد و تنها به اسمبلی System.ComponentModel.DataAnnotations.dll نیاز خواهد داشت.
EF Code first چگونه کلاسهای مورد نظر را انتخاب میکند؟
ممکن است دهها و صدها کلاس در یک پروژه وجود داشته باشند. EF Code first چگونه از بین این کلاسها تشخیص خواهد داد که باید از کدامیک استفاده کند؟ اینجا است که مفهوم جدیدی به نام DbContext معرفی شده است. برای تعریف آن یک کلاس دیگر را به پروژه برای مثال به نام Context اضافه کنید. همچنین مرسوم است که این کلاس را در پروژه class library دیگری به نام DataLayer اضافه میکنند. این پروژه نیاز به ارجاعی به اسمبلیهای EF خواهد داشت. در ادامه کلاس جدید اضافه شده باید از کلاس DbContext مشتق شود:
using System.Data.Entity;
using EF_Sample01.Models;
namespace EF_Sample01
{
public class Context : DbContext
{
public DbSet<Blog> Blogs { set; get; }
public DbSet<Post> Posts { set; get; }
}
}
سپس در اینجا به کمک نوع جنریکی به نام DbSet، کلاسهای دومین برنامه را معرفی میکنیم. به این ترتیب، EF Code first ابتدا به دنبال کلاسی مشتق شده از DbContext خواهد گشت. پس از یافتن آن، خواصی از نوع DbSet را بررسی کرده و نوعهای متناظر با آنرا به عنوان کلاسهای دومین درنظر میگیرد و از سایر کلاسهای برنامه صرفنظر خواهد کرد. این کل کاری است که باید انجام شود.
اگر دقت کرده باشید، نام کلاسهای موجودیتها، مفرد هستند و نام خواص تعریف شده به کمک DbSet، جمع میباشند که نهایتا متناظر خواهند بود با نام جداول بانک اطلاعاتی تشکیل شده.
تشکیل خودکار بانک اطلاعاتی و افزودن اطلاعات به جداول
تا اینجا بدون تهیه یک بانک اطلاعاتی نیز میتوان از کلاس Context تهیه شده استفاده کرد و کار کدنویسی را آغاز نمود. بدیهی است جهت اجرای نهایی کدها، نیاز به یک بانک اطلاعاتی خواهد بود. اگر تنظیمات پیش فرض فایل کانفیگ برنامه را تغییر ندهیم، از همان defaultConnectionFactory یاده شده استفاده خواهد کرد. در این حالت نام بانک اطلاعاتی به صورت خودکار تنظیم شده و مساوی «EF_Sample01.Context» خواهد بود.
برای سفارشی سازی آن نیاز است فایل app.config یا web.config برنامه را اندکی ویرایش نمود:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
...
</configSections>
<connectionStrings>
<clear/>
<add name="Context"
connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true"
providerName="System.Data.SqlClient"
/>
</connectionStrings>
...
</configuration>
در اینجا به بانک اطلاعاتی testdb2012 در وهله پیش فرض SQL Server نصب شده، اشاره شده است. فقط باید دقت داشت که تگ configSections باید در ابتدای فایل قرار گیرد و مابقی تنظیمات پس از آن.
یا اگر علاقمند باشید که از SQL Server CE استفاده کنید، تنظیمات رشته اتصالی را به نحو زیر مقدار دهی نمائید:
<connectionStrings>
<add name="MyContextName"
connectionString="Data Source=|DataDirectory|\Store.sdf"
providerName="System.Data.SqlServerCe.4.0" />
</connectionStrings>
در هر دو حالت، name باید به نام کلاس مشتق شده از DbContext اشاره کند که در مثال جاری همان Context است.
یا اگر علاقمند بودید که این قرارداد توکار را تغییر داده و نام رشته اتصالی را با کدنویسی تعیین کنید، میتوان به نحو زیر عمل کرد:
public class Context : DbContext
{
public Context()
: base("ConnectionStringName")
{
}
البته ضرورتی ندارد این بانک اطلاعاتی از پیش موجود باشد. در اولین بار اجرای کدهای زیر، به صورت خودکار بانک اطلاعاتی و جداول Blogs و Posts و روابط بین آنها تشکیل میگردد:
using EF_Sample01.Models;
namespace EF_Sample01
{
class Program
{
static void Main(string[] args)
{
using (var db = new Context())
{
db.Blogs.Add(new Blog { AuthorName = "Vahid", Title = ".NET Tips" });
db.SaveChanges();
}
}
}
}
در این تصویر چند نکته حائز اهمیت هستند:
الف) نام پیش فرض بانک اطلاعاتی که به آن اشاره شد (اگر تنظیمات رشته اتصالی قید نگردد).
ب) تشکیل خودکار primary key از روی خواصی به نام Id
ج) تشکیل خودکار روابط بین جداول و ایجاد کلید خارجی (به کمک خاصیت virtual تعریف شده)
د) تشکیل جدول سیستمی به نام dbo.__MigrationHistory که از آن برای نگهداری سابقه به روز رسانیهای ساختار جداول کمک گرفته خواهد شد.
ه) نوع و طول فیلدهای متنی، nvarchar از نوع max است.
تمام اینها بر اساس پیش فرضها و قراردادهای توکار EF Code first انجام شده است.
در کدهای تعریف شده نیز، ابتدا یک وهله از شیء Context ایجاد شده و سپس به کمک آن میتوان به جدول Blogs اطلاعاتی را افزود و در آخر ذخیره نمود. استفاده از using هم دراینجا نباید فراموش شود، زیرا اگر استثنایی در این بین رخ دهد، کار پاکسازی منابع و بستن اتصال گشوده شده به بانک اطلاعاتی به صورت خودکار انجام خواهد شد.
در ادامه اگر بخواهیم مطلبی را به Blog ثبت شده اضافه کنیم، خواهیم داشت:
using EF_Sample01.Models;
namespace EF_Sample01
{
class Program
{
static void Main(string[] args)
{
//addBlog();
addPost();
}
private static void addPost()
{
using (var db = new Context())
{
var blog = db.Blogs.Find(1);
db.Posts.Add(new Post
{
Blog = blog,
Content = "data",
Title = "EF"
});
db.SaveChanges();
}
}
private static void addBlog()
{
using (var db = new Context())
{
db.Blogs.Add(new Blog { AuthorName = "Vahid", Title = ".NET Tips" });
db.SaveChanges();
}
}
}
}
متد db.Blogs.Find، بر اساس primary key بلاگ ثبت شده، یک وهله از آنرا یافته و سپس از آن جهت تشکیل شیء Post و افزودن آن به جدول Posts استفاده میشود. متد Find ابتدا Contxet جاری را جهت یافتن شیءایی با id مساوی یک جستجو میکند (اصطلاحا به آن first level cache هم گفته میشود). اگر موفق به یافتن آن شد، بدون صدور کوئری اضافهای به بانک اطلاعاتی از اطلاعات همان شیء استفاده خواهد کرد. در غیراینصورت نیاز خواهد داشت تا ابتدا کوئری لازم را به بانک اطلاعاتی ارسال کرده و اطلاعات شیء Blog متناظر با id=1 را دریافت کند. همچنین اگر نیاز داشتیم تا تنها با سطح اول کش کار کنیم، در EF Code first میتوان از خاصیتی به نام Local نیز استفاده کرد. برای مثال خاصیت db.Blogs.Local بیانگر اطلاعات موجود در سطح اول کش میباشد.
نهایتا کوئری Insert تولید شده توسط آن به شکل زیر خواهد بود (لاگ شده توسط برنامه SQL Server Profiler):
exec sp_executesql N'insert [dbo].[Posts]([Title], [Content], [Blog_Id])
values (@0, @1, @2)
select [Id]
from [dbo].[Posts]
where @@ROWCOUNT > 0 and [Id] = scope_identity()',
N'@0 nvarchar(max) ,@1 nvarchar(max) ,@2 int',
@0=N'EF',
@1=N'data',
@2=1
این نوع کوئرهای پارامتری چندین مزیت مهم را به همراه دارند:
الف) به صورت خودکار تشکیل میشوند. تمام کوئریهای پشت صحنه EF پارامتری هستند و نیازی نیست مرتبا مزایای این امر را گوشزد کرد و باز هم عدهای با جمع زدن رشتهها نسبت به نوشتن کوئریهای نا امن SQL اقدام کنند.
ب) کوئرهای پارامتری در مقابل حملات تزریق اس کیوال مقاوم هستند.
ج) SQL Server با کوئریهای پارامتری همانند رویههای ذخیره شده رفتار میکند. یعنی query execution plan محاسبه شده آنها را کش خواهد کرد. همین امر سبب بالا رفتن کارآیی برنامه در فراخوانیهای بعدی میگردد. الگوی کلی مشخص است. فقط پارامترهای آن تغییر میکنند.
د) مصرف حافظه SQL Server کاهش مییابد. چون SQL Server مجبور نیست به ازای هر کوئری اصطلاحا Ad Hoc رسیده یکبار execution plan متفاوت آنها را محاسبه و سپس کش کند. این مورد مشکل مهم تمام برنامههایی است که از کوئریهای پارامتری استفاده نمیکنند؛ تا حدی که گاهی تصور میکنند شاید SQL Server دچار نشتی حافظه شده، اما مشکل جای دیگری است.
مشکل! ساختار بانک اطلاعاتی تشکیل شده مطلوب کار ما نیست.
تا همینجا با حداقل کدنویسی و تنظیمات مرتبط با آن، پیشرفت خوبی داشتهایم؛ اما نتیجه حاصل آنچنان مطلوب نیست و نیاز به سفارشی سازی دارد. برای مثال طول فیلدها را نیاز داریم به مقدار دیگری تنظیم کنیم، تعدادی از فیلدها باید به صورت not null تعریف شوند یا نام پیش فرض بانک اطلاعاتی باید مشخص گردد و مواردی از این دست. با این موارد در قسمتهای بعدی بیشتر آشنا خواهیم شد.
EF Code First #12
البته مشکل اصلی من با تعدد کلاس ها و زمانبر بودن پیاده سازی آنها است.
مثلاً برای کلاس Product یک بار باید خود آن را ساخت، یک بار EfProductService و IProductService
آیا راه ساده تری وجود دارد؟ یا من کلاً قضیه را اشتباه متوجه شدم؟
استخراج جدول فیلمها
در طراحی فعلی کامپوننت movies، مشکل کوچکی وجود دارد: این کامپوننت تا اینجا، ترکیبی شدهاست از دو کامپوننت صفحه بندی و نمایش لیست گروهها، به همراه جزئیات کامل یک جدول بسیار طولانی. به این مشکل، mixed levels of abstractions میگویند. در اینجا دو کامپوننت سطح بالا را داریم، به همراه یک جدول سطح پایین که تمام مشخصات آن در معرض دید هستند و با هم مخلوط شدهاند. یک چنین کدی، یکدست به نظر نمیرسد. به همین جهت اولین کاری را که در ادامه انجام خواهیم داد، تعریف یک کامپوننت جدید و انتقال تمام جزئیات جدول نمایش ردیفهای فیلمها، به آن است. برای این منظور فایل جدید src\components\moviesTable.jsx را ایجاد کرده و توسط میانبرهای imrc و cc در VSCode، ساختار ابتدایی کامپوننت MoviesTable را تولید میکنیم. این کامپوننت را در پوشهی common قرار ندادیم؛ از این جهت که قابلیت استفادهی مجدد در سایر برنامهها را ندارد. کار آن تنها مرتبط و مختص به اشیاء فیلمی است که در سرویسهای برنامه داریم. البته در ادامه، این جدول را نیز به چندین کامپوننت با قابلیت استفادهی مجدد، خواهیم شکست؛ اما فعلا در اینجا با اصل کدهای سطح پایین جدول نمایش داده شدهی در کامپوننت movies، شروع میکنیم، آنها را cut کرده و به متد رندر کامپوننت جدید MoviesTable منتقل میکنیم.
پس از انتقال کامل تگ table از کامپوننت movies به داخل متد رندر کامپوننت MoviesTable، در ابتدای آن توسط Object Destructuring، یک آرایه و دو رخدادی را که برای مقدار دهی قسمتهای مختلف آن نیاز داریم، از props فرضی، استخراج میکنیم. اینکار کمک میکند تا بتوان اینترفیس این کامپوننت را به خوبی مشخص و طراحی کرد:
class MoviesTable extends Component { render() { const { movies, onDelete, onLike } = this.props;
import Like from "./common/like";
// ... <Like liked={movie.liked} onClick={() => onLike(movie)} />
<button onClick={() => onDelete(movie)} className="btn btn-danger btn-sm" > Delete </button>
import MoviesTable from "./moviesTable";
<MoviesTable movies={movies} onDelete={this.handleDelete} onLike={this.handleLike} />
پس از این تغییرات، متد رندر کامپوننت movies چنین شکلی را پیدا کردهاست که در آن سه کامپوننت سطح بالا درج شدهاند و در یک سطح از abstraction قرار دارند و دیگر مخلوطی از المانهای سطح بالا و سطح پایین را نداریم:
return ( <div className="row"> <div className="col-3"> <ListGroup items={this.state.genres} onItemSelect={this.handleGenreSelect} selectedItem={this.state.selectedGenre} /> </div> <div className="col"> <p>Showing {totalCount} movies in the database.</p> <MoviesTable movies={movies} onDelete={this.handleDelete} onLike={this.handleLike} /> <Pagination itemsCount={totalCount} pageSize={this.state.pageSize} onPageChange={this.handlePageChange} currentPage={this.state.currentPage} /> </div> </div> );
اکنون نوبت فعالسازی کلیک بر روی سرستونهای جدول نمایش داده شده و مرتب سازی اطلاعات جدول بر اساس ستون انتخابی است. به همین جهت در کامپوننت MoviesTable، رویداد onSort را هم به لیستی از خواصی که از props انتظار داریم، اضافه میکنیم که در نهایت در کامپوننت movies، به یک متد رویدادگردان متصل میشود:
class MoviesTable extends Component { render() { const { movies, onDelete, onLike, onSort } = this.props;
return ( <table className="table"> <thead> <tr> <th style={{ cursor: "pointer" }} onClick={() => onSort("title")}>Title</th> <th style={{ cursor: "pointer" }} onClick={() => onSort("genre.name")}>Genre</th> <th style={{ cursor: "pointer" }} onClick={() => onSort("numberInStock")}>Stock</th> <th style={{ cursor: "pointer" }} onClick={() => onSort("dailyRentalRate")}>Rate</th> <th /> <th /> </tr> </thead>
<MoviesTable movies={movies} onDelete={this.handleDelete} onLike={this.handleLike} onSort={this.handleSort} />
handleSort = column => { console.log("handleSort", column); };
پیاده سازی مرتب سازی اطلاعات
تا اینجا اگر دقت کرده باشید، هر زمانیکه شماره صفحهای تغییر میکند یا گروه فیلم خاصی انتخاب میشود، ابتدا state را به روز رسانی میکنیم که در نتیجهی آن، کار رندر مجدد کامپوننت در DOM مجازی React صورت میگیرد. سپس در متد رندر، کار تغییر اطلاعات آرایهی فیلمها را جهت نمایش به کاربر، انجام میدهیم.
بنابراین ابتدا در متد رویدادگران handleSort، با فراخوانی متد setState، مقدار path دریافتی حاصل از کلیک بر روی یک سرستون را به همراه صعودی و یا نزولی بودن مرتب سازی، در state کامپوننت جاری تغییر میدهیم:
handleSort = path => { console.log("handleSort", path); this.setState({ sortColumn: { path, order: "asc" } }); };
class Movies extends Component { state = { // ... sortColumn: { path:"title", order: "asc" } };
getPagedData() { const { pageSize, currentPage, selectedGenre, movies: allMovies, sortColumn } = this.state; let filteredMovies = selectedGenre && selectedGenre._id ? allMovies.filter(m => m.genre._id === selectedGenre._id) : allMovies; filteredMovies = filteredMovies.sort((movie1, movie2) => movie1[sortColumn.path] > movie2[sortColumn.path] ? sortColumn.order === "asc" ? 1 : -1 : movie2[sortColumn.path] > movie1[sortColumn.path] ? sortColumn.order === "asc" ? -1 : 1 : 0 ); const first = (currentPage - 1) * pageSize; const last = first + pageSize; const pagedMovies = filteredMovies.slice(first, last); return { totalCount: filteredMovies.length, data: pagedMovies }; }
همچنین میخواهیم اگر با کلیک بر روی ستونی، روش و جهت مرتب سازی آن صعودی بود، نزولی شود و یا برعکس که یک روش پیاده سازی آنرا در اینجا مشاهده میکنید:
handleSort = path => { console.log("handleSort", path); const sortColumn = { ...this.state.sortColumn }; if (sortColumn.path === path) { sortColumn.order = sortColumn.order === "asc" ? "desc" : "asc"; } else { sortColumn.path = path; sortColumn.order = "asc"; } this.setState({ sortColumn }); };
بهبود کیفیت کدهای مرتب سازی اطلاعات
اگر قرار باشد کامپوننت MoviesTable را در جای دیگری مورد استفادهی مجدد قرار دهیم، زمانیکه این جدول سبب صدور رخدادی میشود، باید منطقی را که در متد handleSort فوق مشاهده میکنید، مجددا به همین شکل تکرار کنیم. بنابراین این منطق متعلق به کامپوننت MoviesTable است و زمانیکه onSort را فراخوانی میکند، بهتر است بجای ارسال path یا همان نام فیلدی که قرار است مرتب سازی بر اساس آن انجام شود، شیء sortColumn را به عنوان خروجی بازگشت دهد. به همین جهت، این منطق را به کلاس MoviesTable منتقل میکنیم:
class MoviesTable extends Component { raiseSort = path => { console.log("raiseSort", path); const sortColumn = { ...this.props.sortColumn }; if (sortColumn.path === path) { sortColumn.order = sortColumn.order === "asc" ? "desc" : "asc"; } else { sortColumn.path = path; sortColumn.order = "asc"; } this.props.onSort(sortColumn); };
<MoviesTable movies={movies} onDelete={this.handleDelete} onLike={this.handleLike} onSort={this.handleSort} sortColumn={this.state.sortColumn} />
return ( <table className="table"> <thead> <tr> <th style={{ cursor: "pointer" }} onClick={() => this.raiseSort("title")}>Title</th> <th style={{ cursor: "pointer" }} onClick={() => this.raiseSort("genre.name")}>Genre</th> <th style={{ cursor: "pointer" }} onClick={() => this.raiseSort("numberInStock")}>Stock</th> <th style={{ cursor: "pointer" }} onClick={() => this.raiseSort("dailyRentalRate")}>Rate</th> <th /> <th /> </tr> </thead>
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-13.zip
این برنامه از چهار کامپوننت تشکیل شدهاست:
- کامپوننت App که در برگیرندهی سه کامپوننت زیر است:
- کامپوننت BasketItemsCounter: جمع تعداد آیتمهای انتخابی توسط کاربر را نمایش میدهد؛ به همراه دکمهای برای خالی کردن لیست انتخابی.
- کامپوننت ShopItemsList: لیست محصولات موجود در فروشگاه را نمایش میدهد. با کلیک بر روی هر آیتم آن، آیتم انتخابی به لیست انتخابهای او اضافه خواهد شد.
- کامپوننت BasketItemsList: لیستی را نمایش میدهد که حاصل انتخابهای کاربر در کامپوننت ShopItemsList است (یا همان سبد خرید). در ذیل این لیست، جمع نهایی قیمت قابل پرداخت نیز درج میشود. همچنین اگر کاربر بر روی دکمهی remove هر ردیف کلیک کند، یک واحد از چند واحد انتخابی، حذف خواهد شد.
بنابراین در اینجا سه کامپوننت مجزا را داریم که با هم تبادل اطلاعات میکنند. یکی جمع تعداد محصولات خریداری شده را، دیگری لیست محصولات موجود را و آخری لیست خرید نهایی را نمایش میدهد. همچنین این سه کامپوننت، فرزند یک دیگر هم محسوب نمیشوند و انتقال اطلاعات بین اینها نیاز به بالا بردن state هر کدام و قرار دادن آنها در کامپوننت App را دارد تا بتوان پس از آن از طریق props آنها را بین سه کامپوننت فوق که اکنون فرزند کامپوننت App محسوب میشوند، به اشتراک گذاشت. روش بهتر اینکار، استفاده از یک مخزن حالت سراسری است تا حالتهای این کامپوننتها را نگهداری کرده و دادهها را بین آنها به اشتراک بگذارد که در اینجا برای حل این مساله از کتابخانههای mobx و mobx-react استفاده خواهیم کرد.
برپایی پیشنیازها
برای پیاده سازی برنامهی فوق، یک پروژهی جدید React را ایجاد میکنیم:
> create-react-app state-management-with-mobx-part4 > cd state-management-with-mobx-part4
> npm install --save bootstrap mobx mobx-react mobx-react-devtools mobx-state-tree
- برای استفاده از شیوهنامههای بوت استرپ، بستهی bootstrap نیز در اینجا نصب میشود.
- اصل کار برنامه توسط دو کتابخانهی mobx و کتابخانهی متصل کنندهی آن به برنامههای react که mobx-react نام دارد، انجام خواهد شد.
- چون میخواهیم از افزونهی mobx-devtools نیز استفاده کنیم، نیاز است دو بستهی mobx-react-devtools و همچنین mobx-state-tree را که جزو وابستگیهای آن است، نصب کنیم.
سپس بستههای زیر را که در قسمت devDependencies فایل package.json درج خواهند شد، باید نصب شوند:
> npm install --save-dev babel-eslint customize-cra eslint eslint-config-react-app eslint-loader eslint-plugin-babel eslint-plugin-css-modules eslint-plugin-filenames eslint-plugin-flowtype eslint-plugin-import eslint-plugin-no-async-without-await eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-react-redux eslint-plugin-redux-saga eslint-plugin-simple-import-sort react-app-rewired typescript
- افزودن فایل جدید config-overrides.js به ریشهی پروژه، تا پشتیبانی ازlegacy" decorators spec" فعال شود.
- اصلاح فایل package.json و ویرایش قسمت scripts آن برای استفادهی از react-app-rewired، تا امکان تغییر تنظیمات webpack به صورت پویا در زمان اجرای برنامه، میسر شود.
- همچنین فایل غنی شدهی eslintrc.json. را نیز به ریشهی پروژه اضافه میکنیم.
تهیه سرویس لیست محصولات موجود در فروشگاه
این برنامه از یک لیست درون حافظهای، برای تهیهی لیست محصولات موجود در فروشگاه استفاده میکند. به همین جهت پوشهی service را افزوده و سپس فایل جدید src\services\productsService.js را با محتوای زیر، ایجاد میکنیم:
const products = [ { id: 1, name: "Item 1", price: 850 }, { id: 2, name: "Item 2", price: 900 }, { id: 3, name: "Item 3", price: 1500 }, { id: 4, name: "Item 4", price: 1000 } ]; export default products;
ایجاد کامپوننت نمایش لیست محصولات
پس از مشخص شدن لیست محصولات قابل فروش، کامپوننت جدید src\components\ShopItemsList.jsx را به صورت زیر ایجاد میکنیم:
import React from "react"; import products from "../services/productsService"; const ShopItemsList = ({ onAdd }) => { return ( <table className="table table-hover"> <thead className="thead-light"> <tr> <th>Name</th> <th>Price</th> <th>Action</th> </tr> </thead> <tbody> {products.map(product => ( <tr key={product.id}> <td>{product.name}</td> <td>{product.price}</td> <td> <button className="btn btn-sm btn-info" onClick={() => onAdd(product)} > Add </button> </td> </tr> ))} </tbody> </table> ); }; export default ShopItemsList;
- در اینجا همچنین هر ردیف، به همراه یک دکمهی Add نیز هست که قرار است با کلیک بر روی آن، متد رویدادگردان onAdd فراخوانی شود. این متد نیز از طریق props این کامپوننت دریافت میشود. کتابخانههای مدیریت حالت، تمام خواص و رویدادگردانهای مورد نیاز یک کامپوننت را از طریق props، تامین میکنند.
- فعلا این کامپوننت به هیچ مخزن دادهای متصل نیست و فقط طراحی ابتدایی آن آماده شدهاست.
ایجاد کامپوننت نمایش لیست خرید کاربر (سبد خرید)
اکنون که میتوان توسط کامپوننت لیست محصولات، تعدادی از آنها را خریداری کرد، کامپوننت جدید src\components\BasketItemsList.jsx را برای نمایش لیست نهایی خرید کاربر، به صورت زیر پیاده سازی میکنیم:
import React from "react"; const BasketItemsList = ({ items, totalPrice, onRemove }) => { return ( <> <table className="table table-hover"> <thead className="thead-light"> <tr> <th>Name</th> <th>Price</th> <th>Count</th> <th>Action</th> </tr> </thead> <tbody> {items.map(item => ( <tr key={item.id}> <td>{item.name}</td> <td>{item.price}</td> <td>{item.count}</td> <td> <button className="btn btn-sm btn-danger" onClick={() => onRemove(item.id)} > Remove </button> </td> </tr> ))} <tr> <td align="right"> <strong>Total: </strong> </td> <td> <strong>{totalPrice}</strong> </td> <td></td> <td></td> </tr> </tbody> </table> </> ); }; export default BasketItemsList;
const BasketItemsList = ({ items, totalPrice, onRemove }) => {
- همچنین هر ردیف نمایش داده شده، به همراه یک دکمهی Remove آیتم انتخابی نیز هست که به متد رویدادگردان onRemove متصل شدهاست.
- در ردیف انتهایی این لیست، مقدار totalPrice که یک خاصیت محاسباتی است، درج میشود.
- فعلا این کامپوننت نیز به هیچ مخزن دادهای متصل نیست و فقط طراحی ابتدایی آن آماده شدهاست.
ایجاد کامپوننت نمایش تعداد آیتمهای خریداری شده
کاربر اگر آیتمی را از لیست محصولات انتخاب کند و یا محصول انتخاب شده را از لیست خرید حذف کند، تعداد نهایی باقی مانده را میتوان در کامپوننت src\components\BasketItemsCounter.jsx مشاهده کرد:
import React, { Component } from "react"; class BasketItemsCounter extends Component { render() { const { count, onRemoveAll } = this.props; return ( <div> <h1>Total items: {count}</h1> <button type="button" className="btn btn-sm btn-danger" onClick={() => onRemoveAll()} > Empty Basket </button> </div> ); } } export default BasketItemsCounter;
- فعلا این کامپوننت نیز به هیچ مخزن دادهای متصل نیست و فقط طراحی ابتدایی آن آماده شدهاست.
نمایش ابتدایی سه کامپوننت توسط کامپوننت App
اکنون که این سه کامپوننت تکمیل شدهاند، میتوان المانهای آنها را در فایل src\App.js درج کرد تا در صفحه نمایش داده شوند:
import React, { Component } from "react"; import BasketItemsCounter from "./components/BasketItemsCounter"; import BasketItemsList from "./components/BasketItemsList"; import ShopItemsList from "./components/ShopItemsList"; class App extends Component { render() { return ( <main className="container"> <div className="row"> <BasketItemsCounter /> </div> <hr /> <div className="row"> <h2>Products</h2> <ShopItemsList /> </div> <div className="row"> <h2>Basket</h2> <BasketItemsList /> </div> </main> ); } } export default App;
میتوان همانند Redux کل state برنامه را داخل یک شیء store ذخیره کرد و یا چون در اینجا میتوان طراحی مخزن حالت MobX را به دلخواه انجام داد، میتوان چندین مخزن حالت را تهیه و به هم متصل کرد؛ مانند تصویری که مشاهده میکنید. در اینجا:
- src\stores\counter.js: مخزن دادهی حالت کامپوننت شمارشگر است.
- src\stores\market.js: مخزن دادهی کامپوننتهای لیست محصولات و سبد خرید است.
- src\stores\index.js: کار ترکیب دو مخزن قبل را انجام میدهد.
در ادامه کدهای کامل این مخازن را مشاهده میکنید:
مخزن حالت src\stores\counter.js
import { action, observable } from "mobx"; export default class CounterStore { @observable totalNumbersInBasket = 0; constructor(rootStore) { this.rootStore = rootStore; } @action increase = () => { this.totalNumbersInBasket++; }; @action decrease = () => { this.totalNumbersInBasket--; }; }
- در اینجا خاصیت totalNumbersInBasket به صورت observable تعریف شدهاست و با تغییر آن چه به صورت مستقیم، با مقدار دهی آن و یا توسط دو action تعریف شده، سبب به روز رسانی UI خواهد شد.
- میشد این مخزن را با مخزن src\stores\market.js یکی کرد؛ اما جهت ارائهی مثالی در مورد نحوهی تعریف چند مخزن و روش برقراری ارتباط بین آنها، به صورت مجزایی تعریف شد.
مخزن حالت src\stores\market.js
import { action, computed, observable } from "mobx"; export default class MarketStore { @observable basketItems = []; constructor(rootStore) { this.rootStore = rootStore; } @action add = product => { const selectedItem = this.basketItems.find(item => item.id === product.id); if (selectedItem) { selectedItem.count++; } else { this.basketItems.push({ ...product, count: 1 }); } this.rootStore.counterStore.increase(); }; @action remove = id => { const selectedItem = this.basketItems.find(item => item.id === id); selectedItem.count--; if (selectedItem.count === 0) { this.basketItems.remove(selectedItem); } this.rootStore.counterStore.decrease(); }; @action removeAll = () => { this.basketItems = []; this.rootStore.counterStore.totalNumbersInBasket = 0; }; @computed get totalPrice() { return this.basketItems.reduce((previous, current) => { return previous + current.price * current.count; }, 0); } }
- توسط متد add آن در کامپوننت نمایش لیست محصولات، میتوان آیتمی را به این آرایه اضافه کرد. در اینجا چون شیء product مورد استفاده دارای خاصیت count نیست، روش افزودن آنرا توسط spread operator برای درج خواص شیء product اصلی و سپس تعریف آنرا مشاهده میکنید. این فراخوانی، سبب افزایش یک واحد به عدد شمارشگر نیز میشود.
- متد remove آن در کامپوننت سبد خرید، مورد استفاده قرار میگیرد تا کاربر بتواند اطلاعاتی را از این لیست حذف کند. این فراخوانی، سبب کاهش یک واحد از عدد شمارشگر نیز میشود.
- متد removeAll آن در کامپوننت شمارشگر بالای صفحه استفاده میشود تا سبب خالی شدن آرایهی آیتمهای انتخابی گردد و همچنین عدد آنرا نیز صفر کند.
- خاصیت محاسباتی totalPrice آن در پایین جدول سبد خرید، جمع کل هزینهی قابل پرداخت را مشخص میکند.
مخزن حالت src\stores\index.js
در اینجا روش یکی کردن دو مخزن حالت یاد شده را به صورت خاصیتهای عمومی یک مخزن کد ریشه، مشاهده میکنید:
import CounterStore from "./counter"; import MarketStore from "./market"; class RootStore { counterStore = new CounterStore(this); marketStore = new MarketStore(this); } export default RootStore;
export default class MarketStore { @observable basketItems = []; constructor(rootStore) { this.rootStore = rootStore; } @action removeAll = () => { this.basketItems = []; this.rootStore.counterStore.totalNumbersInBasket = 0; }; }
پس از ایجاد مخازن حالت، اکنون نیاز است آنها را در اختیار سلسه مراتب کامپوننتهای برنامه قرار دهیم. به همین جهت به فایل src\index.js مراجعه کرده و آنرا به صورت زیر تغییر میدهیم:
import "./index.css"; import "bootstrap/dist/css/bootstrap.css"; import makeInspectable from "mobx-devtools-mst"; import { Provider } from "mobx-react"; import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; import * as serviceWorker from "./serviceWorker"; import RootStore from "./stores"; const rootStore = new RootStore(); if (process.env.NODE_ENV === "development") { makeInspectable(rootStore); // https://github.com/mobxjs/mobx-devtools } ReactDOM.render( <Provider {...rootStore}> <App /> </Provider>, document.getElementById("root") ); serviceWorker.unregister();
- سپس یک وهلهی جدید از RootStore را که حاوی خاصیتهای عمومی counterStore و marketStore است، ایجاد میکنیم.
- اگر علاقمند باشید تا حین کار با MobX، جزئیات پشت صحنهی آنرا توسط افزونهی mobx-devtools ردیابی کنید، روش آنرا در اینجا با فراخوانی متد makeInspectable مشاهده میکنید. مقدار process.env.NODE_ENV نیز بر اساس پروسهی جاری node.js اجرا کنندهی برنامهی React تامین میشود. اطلاعات بیشتر
- قسمت آخر این تنظیمات، محصور کردن کامپوننت App که بالاترین کامپوننت در سلسله مراتب کامپوننتهای برنامه است، با شیء Provider میباشد. در این شیء توسط spread operator، سبب درج خواص عمومی rootStore، به عنوان مخازن قابل استفاده شدهایم. تنظیم {rootStore...} معادل عبارت زیر است:
<Provider counterStore={rootStore.counterStore} marketStore={rootStore.marketStore}>
اتصال کامپوننت ShopItemsList به مخزن حالت marketStore
پس از ایجاد rootStore و محصور کردن کامپوننت App توسط شیء Provider در فایل src\index.js، اکنون باید قسمت export default کامپوننتهای برنامه را جهت استفادهی از مخازن حالت، یکی یکی ویرایش کرد:
import { inject, observer } from "mobx-react"; import React from "react"; import products from "../services/productsService"; const ShopItemsList = ({ onAdd }) => { return ( // ... ); }; export default inject(({ marketStore }) => ({ onAdd: marketStore.add }))(observer(ShopItemsList));
اتصال کامپوننت BasketItemsList به مخزن حالت marketStore
در اینجا نیز سطر export default را جهت دریافت خاصیت marketStore، از شیء Provider تامین شدهی در فایل src\index.js، ویرایش میکنیم. به این ترتیب سه props مورد انتظار این کامپوننت، توسط خاصیتهای basketItems (آرایهی اشیاء انتخابی توسط کاربر)، totalPrice (خاصیت محاسباتی جمع کل هزینه) و متد رویدادگردان onRemove (برای حذف یک آیتم) تامین میشوند. در آخر کامپوننت را به صورت observer محصور کرده و بازگشت میدهیم تا تغییرات در مخزن حالت آن، سبب به روز رسانی UI آن شوند:
import { inject, observer } from "mobx-react"; import React from "react"; const BasketItemsList = ({ items, totalPrice, onRemove }) => { return ( // ... ); }; export default inject(({ marketStore }) => ({ items: marketStore.basketItems, totalPrice: marketStore.totalPrice, onRemove: marketStore.remove }))(observer(BasketItemsList));
اتصال کامپوننت BasketItemsCounter به دو مخزن حالت counterStore و marketStore
در اینجا روش استفادهی از decorator syntax کتابخانهی mobx-react را بر روی یک کامپوننت کلاسی مشاهده میکنید. تزئین کنندهی inject، امکان دسترسی به مخازن حالت تزریقی به شیء Provider را میسر کرده و سپس توسط آن میتوان props مورد انتظار کامپوننت را از مخازن متناظر استخراج کرده و در اختیار کامپوننت قرار داد. همچنین این کامپوننت توسط تزئین کنندهی observer نیز علامت گذاری شدهاست. در این حالت نیازی به تغییر سطر export default نیست.
import { inject, observer } from "mobx-react"; import React, { Component } from "react"; @inject(rootStore => ({ count: rootStore.counterStore.totalNumbersInBasket, onRemoveAll: rootStore.marketStore.removeAll })) @observer class BasketItemsCounter extends Component { render() { const { count, onRemoveAll } = this.props; return ( // ... ); } } export default BasketItemsCounter;
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید: state-management-with-mobx-part4.zip
فواید استفاده از فریمورک Seneca
- فریمورک Seneca کدنویسی برای ایجاد درخواستها، ارسال پاسخ به درخواستهای رسیده و تبدیل دادهها را که از روتینهای هر سرویسی میتوانند باشند، سادهتر میکند.
- فریمورک Seneca با معرفی ایده Actionها و Pluginها که جلوتر توضیح داده خواهند شد، روند تبدیل کامپوننتهای یک برنامه Monolithic را به نوع سرویسی، تسهیل میکند. روندی که به صورت عادی میتواند کاری طاقت فرسا باشد و نیاز به Refactoring زیادی دارد.
- سرویسهای نوشته شده با زبانهای برنامهنویسی مختلف یا فریمورکهای متفاوت میتوانند با سرویسهای نوشته شده توسط فریمورک Seneca در ارتباط و تبادل باشند.
نصب فریمورک Seneca
{ "name": "seneca-example", "dependencies": { "seneca": "0.6.5", "express": "latest" } }
var seneca = require("seneca")();
Actionها
seneca.add({role: "accountManagement", cmd: "login"}, function(args, respond){ }); seneca.add({role: "accountManagement", cmd: "register"}, function(args, respond){ });
seneca.act({role: "accountManagement", cmd: "register", username: "parham", password: "12345!"}, function(error, response){ }); seneca.act({role: "accountManagement", cmd: "login", username: "parham", password: "12345!"}, function(error, response){ });
Pluginها
function account(options){ this.add({init: "account"}, function(pluginInfo, respond){ console.log(options.message); respond(); }) this.add({role: "accountManagement", cmd: "login"}, function(args, respond){ }); this.add({role: "accountManagement", cmd: "register"}, function(args, respond){ }); } seneca.use(account, {message: "Plugin Added"});
module.exports = function(options){ this.add({init: "account"}, function(pluginInfo, respond){ console.log(options.message); respond(); }) this.add({role: "accountManagement", cmd: "login"}, function(args, respond){ }); this.add({role: "accountManagement", cmd: "register"},function(args, respond){ }); return "account"; } seneca.use("./account.js", {message: "Plugin Added"});
Serviceها
var seneca = require("seneca")(); seneca.use("./account.js", {message: "Plugin Added"}); seneca.listen({port: "9090", pin: {role: "accountManagement"}});
seneca.client({port: "9090", pin: {role: "accountManagement"}});
در اینجا هم موارد port و pin اختیاری هستند. اگر سرویسی که میخواهیم ثبت کنیم در سرور دیگری باشد، بایستی خاصیت host و با مقدار آدرس IP سرور مورد نظر در زمان ثبت، اعمال شود. سوالی که باقی میماند این است که یک سرویس چطور Actionی از یک سرویس دیگر را فراخوانی میکند؟
در بخش Actionها آمد که برای فراخوانی یک Action از متد act، از نمونهی Seneca استفاده میکنیم. رفتار Seneca به این صورت است که ابتدا بر اساس امضای Action درخواست شده، Actionهای محلی را که به سرویس جاری اضافه شدهاند، جستجو میکند. اگر تطبیقی نیافت به سراغ Actionهای ثبت شده خارجی که دارای خاصیت pin هستند، خواهد رفت و در نهایت اگر آنجا هم موردی نیافت، برای تک تک سرویسهایی که آنها را ثبت کرده، اما خاصیت pin را ندارند، درخواستی را ارسال میکند.
برای اطلاعات بیشتر به بخش مستندات فریمورک Seneca رجوع کنید.
کتابخانه iconate
آشنایی اولیه با gRPC
syntax = "proto3"; package helloworld; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) {} } message HelloRequest { string name = 1; } message HelloReply { string message = 1; }
Google.Protobuf grpc Grpc.Tools
<ItemGroup> <Protobuf Include="helloworld.proto" /> </ItemGroup>
using System; using System.Collections.Generic; using System.Threading.Tasks; using Helloworld; using Grpc.Core; namespace ServerGrpc { class GreeterImpl : Greeter.GreeterBase { public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context) { System.Console.WriteLine("request made!"); return Task.FromResult(new HelloReply { Message = "Hello " + request.Name }); } } class Program { const int Port = 50051; public static void Main(string[] args) { Server server = new Server { Services = { Greeter.BindService(new GreeterImpl()) }, Ports = { new ServerPort("localhost", Port, ServerCredentials.Insecure) } }; server.Start(); Console.WriteLine("Greeter server listening on port " + Port); Console.WriteLine("Press any key to stop the server..."); Console.ReadKey(); server.ShutdownAsync().Wait(); } } }
obj/Debug/netcoreapp2.2(یا نسخهی دیگری که استفاده میکنید)
protoc helloworld.proto --go_out=plugins=grpc:.
package main import ( "fmt" "golang.org/x/net/context" "google.golang.org/grpc" "gosample/proto" ) func main() { initial() } func initial(){ conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure()) defer conn.Close() client := helloworld.NewGreeterClient(conn) data, _ := client.SayHello(context.Background(), &helloworld.HelloRequest{Name : "Ali"}) fmt.Println(data) }
Generators در ES 6
یک نمونه مثال ابتدایی از Generators را در کدهای زیر مشاهده میکنید:
function* generator () { yield 1; // pause yield 2; // pause yield 3; // pause yield 'done?'; // done }
let gen = generator(); // [object Generator] console.log(gen.next()); // Object {value: 1, done: false} console.log(gen.next()); // Object {value: 2, done: false} console.log(gen.next()); // Object {value: 3, done: false} console.log(gen.next()); // Object {value: 'done?', done: false} console.log(gen.next()); // Object {value: undefined, done: true} console.log(gen.next()); // Object {value: undefined, done: true}
و یا میتوان بجای فراخوانی دستی متد next، از حلقهی جدید for of نیز برای کار با آنها استفاده کرد:
for (let val of generator()) { console.log(val); // 1 // 2 // 3 // 'done?' }
function* random (max) { yield Math.floor(Math.random() * max) + 1; } function* random1_20 () { while (true) { yield* random(20); } } let rand = random1_20(); console.log(rand.next()); console.log(rand.next());
فقط در این حالت بجای yield معمولی از *yield استفاده میشود. از *yield برای کار با هر نوع Iterator ایی میتوان استفاده کرد:
function* multiplier (value) { yield value * 2; yield value * 3; yield value * 4; yield value * 5; } function* trailmix () { yield 0; yield* [1, 2]; yield* [...multiplier(2)]; yield* multiplier(3); }
کاهش مصرف حافظهی برنامه با استفاده از Generators
در مثال زیر، قرار است لیستی از rows بازگشت داده شود. در اینجا یک آرایه تشکیل شده و هربار اطلاعاتی به آن push میشود و در نهایت این آرایه بازگشت داده خواهد شد:
function splitIntoRows(icons, rowLength) { var rows = []; for (var i = 0; i < icons.length; i += rowLength) { rows.push(icons.slice(i, i + rowLength)); } return rows; }
function* splitIntoRows(icons, rowLength) { for (var i = 0; i < icons.length; i += rowLength) { yield icons.slice(i, i + rowLength); } }
روشهایی برای خاموش کردن Generators
Generators علاوه بر متد next، دارای متدهای return و throw نیز هستند.
فراخوانی (generator.throw(error همانند این است که در بین کار، به متدی برخوردهایم که استثنایی را صادر کردهاست و سبب خاتمهی کار Generator شدهاست.
اگر متد return آنها فراخوانی شود، کار Generator پایان مییابد (خاصیت done شیء بازگشتی بلافاصله true میشود) و فراخوانی next، پس از آن، دیگر اثری نخواهد داشت:
function* numbers () { yield 1 yield 2 yield 3 } var g = numbers() console.log(g.next()) // <- { done: false, value: 1 } console.log(g.return()) // <- { done: true } console.log(g.next()) // <- { done: true }, as we know
function* numbers () { yield 1 yield 2 return 3 yield 4 } console.log([...numbers()]) // <- [1, 2]
اگر به متد return خود Generator، پارامتری ارسال شود، این مقدار، مقدار نهایی بازگشت داده شده خواهد بود:
function* numbers () { yield 1 yield 2 return 3 yield 4 } var g = numbers() console.log(g.next()) // <- { done: false, value: 1 } console.log(g.return(5)) // <- { done: true, value: 5 } console.log(g.next()) // <- { done: true }
در حالتیکه به متد return پارامتری ارسال میشود، قسمت finally بدنهی try/finally نوشته شده حتما اجرا خواهد شد و سپس کار خاتمه پیدا میکند:
function* numbers () { yield 1 try { yield 2 } finally { yield 3 yield 4 } yield 5 } var g = numbers() console.log(g.next()) // <- { done: false, value: 1 } console.log(g.next()) // <- { done: false, value: 2 } console.log(g.return(6)) // <- { done: false, value: 3 } console.log(g.next()) // <- { done: false, value: 4 } console.log(g.next()) // <- { done: true, value: 6 }