public interface IShoppingCartService { ShoppingCart GetContents(); ShoppingCart AddItemToCart(int itemId, int quantity); } public class ShoppingCartService : IShoppingCartService { public ShoppingCart GetContents() { throw new NotImplementedException("Get cart from Persistence Layer"); } public ShoppingCart AddItemToCart(int itemId, int quantity) { throw new NotImplementedException("Add Item to cart then return updated cart"); } } public class ShoppingCart { public List<product> Items { get; set; } } public class Product { public int ItemId { get; set; } public string ItemName { get; set; } } public class ShoppingCartController : Controller { //Concrete object below points to actual service //private ShoppingCartService _shoppingCartService; //loosely coupled code below uses the interface rather than the //concrete object private IShoppingCartService _shoppingCartService; public ShoppingCartController() { _shoppingCartService = new ShoppingCartService(); } public ActionResult GetCart() { //now using the shared instance of the shoppingCartService dependency ShoppingCart cart = _shoppingCartService.GetContents(); return View("Cart", cart); } public ActionResult AddItemToCart(int itemId, int quantity) { //now using the shared instance of the shoppingCartService dependency ShoppingCart cart = _shoppingCartService.AddItemToCart(itemId, quantity); return View("Cart", cart); } }
//loosely coupled code below uses the interface rather //than the concrete object private IShoppingCartService _shoppingCartService; //MVC uses this constructor public ShoppingCartController() { _shoppingCartService = new ShoppingCartService(); } //You can use this constructor when testing to inject the //ShoppingCartService dependency public ShoppingCartController(IShoppingCartService shoppingCartService) { _shoppingCartService = shoppingCartService; }
[TestClass] public class ShoppingCartControllerTests { [TestMethod] public void GetCartSmokeTest() { //arrange ShoppingCartController controller = new ShoppingCartController(new ShoppingCartServiceStub()); // Act ActionResult result = controller.GetCart(); // Assert Assert.IsInstanceOfType(result, typeof(ViewResult)); } } /// <summary> /// This is is a stub of the ShoppingCartService /// </summary> public class ShoppingCartServiceStub : IShoppingCartService { public ShoppingCart GetContents() { return new ShoppingCart { Items = new List<product> { new Product {ItemId = 1, ItemName = "Widget"} } }; } public ShoppingCart AddItemToCart(int itemId, int quantity) { throw new NotImplementedException(); } }
آشنایی با مدل برنامه نویسی TAP
دات نت فریم ورک، از زمان ارائه نگارش یک آن، از اعمال غیرهمزمان و API خاص آن پشتیبانی میکردهاست. همچنین این مورد یکی از ویژگیهای Win32 نیز میباشد. نوشتن کدهای همزمان متداول بسیار ساده است. در این نوع کدها هر عملیات خاص، پس از پایان عملیات قبلی انجام میشود.
public string TestNoneAsync() { var webClient = new WebClient(); return webClient.DownloadString("http://www.google.com"); }
این مورد همچنین در برنامههای سمت سرور نیز حائز اهمیت است. با قفل شدن تعداد زیادی ترد در حال اجرا، عملا قدرت پاسخدهی سرور نیز کاهش مییابد. بنابراین در این نوع موارد، برنامههای چند ریسمانی هرچند در سمت کلاینت ممکن است مفید واقع شوند و برای مثال ترد UI را آزاد کنند، اما اثر آنچنانی بر روی برنامههای سمت سرور ندارند. زیرا در آنها میتوان هزاران ترد را ایجاد کرد که همگی دارای کدهای اصطلاحا blocking باشند. برای حل این مساله استفاده از API غیرهمزمان توصیه میشود.
برای نمونه کلاس WebClient توکار دات نت، دارای متدی به نام DownloadStringAsync نیز میباشد. این متد به محض فراخوانی، ترد جاری را آزاد میکند. به این معنا که فراخوانی آن سبب توقف ترد جاری برای دریافت نتیجهی دریافت اطلاعات از وب نمیشود. به این نوع API، یک Asynchronous API گفته میشود؛ زیرا با سایر کدهای نوشته شده، هماهنگ و همزمان اجرا نمیشود.
هر چند این کد جدید مشکل عدم پاسخ دهی برنامه را برطرف میکند، اما مشکل دیگری را به همراه دارد؛ چگونه باید حاصل عملیات آنرا پس از پایان کار دریافت کرد؟ چگونه باید خطاها و مشکلات احتمالی را مدیریت کرد؟
برای مدیریت این مساله، رخدادی به نام DownloadStringCompleted تعریف شدهاست. روال رویدادگردان آن پس از پایان کار دریافت اطلاعات از وب، فراخوانی میگردد.
public void TestAsync() { var webClient = new WebClient(); webClient.DownloadStringAsync(new Uri("http://www.google.com")); webClient.DownloadStringCompleted += webClientDownloadStringCompleted; } void webClientDownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e) { // use e.Result }
مشکل! ما سادگی یک عملیات همزمان را از دست دادیم. متد TestNoneAsync از لحاظ پیاده سازی و همچنین خواندن و نگهداری آن در طول زمان، بسیار سادهتر است از نمونهی TestAsync نوشته شده. در کدهای غیرهمزمان فوق، یک متد ساده، به دو متد مجزا خرد شدهاست و نتیجهی نهایی، درون یک روال رخدادگردان بدست میآید.
به این مدل، EAP یا Event based asynchronous pattern نیز گفته میشود. EAP در دات نت 2 معرفی شد. روالهای رخدادگردان در این حالت، در ترد اصلی برنامه اجرا میشوند. اما اگر به حالت اصلی اعمال غیرهمزمان موجود از دات نت یک کوچ کنیم، اینطور نیست. در WinForms و WPF برای به روز رسانی رابط کاربری نیاز است اطلاعات دریافت شده در همان تردی که رابط کاربری ایجاد شده است، تحویل گرفته شده و استفاده شوند. در غیراینصورت استثنایی صادر شده و برنامه خاتمه مییابد.
آشنایی با Synchronization Context
ابتدا یک برنامهی WinForms ساده را آغاز کرده و یک دکمهی جدید را به نام btnGetInfo و یک تکست باکس را به نام txtResults، به آن اضافه کنید. سپس کدهای فرم اصلی آنرا به نحو ذیل تغییر دهید:
using System; using System.Linq; using System.Net; using System.Windows.Forms; namespace Async02 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void btnGetInfo_Click(object sender, EventArgs e) { var req = (HttpWebRequest)WebRequest.Create("http://www.google.com"); req.Method = "HEAD"; req.BeginGetResponse( asyncResult => { var resp = (HttpWebResponse)req.EndGetResponse(asyncResult); var headersText = formatHeaders(resp.Headers); txtResults.Text = headersText; }, null); } private string formatHeaders(WebHeaderCollection headers) { var headerString = headers.Keys.Cast<string>() .Select(header => string.Format("{0}:{1}", header, headers[header])); return string.Join(Environment.NewLine, headerString.ToArray()); } } }
همچنین در این مثال از متد غیرهمزمان BeginGetResponse نیز استفاده شدهاست. در این نوع API خاص، کار با BeginGetResponse آغاز شده و سپس در callback نهایی توسط EndGetResponse، نتیجهی عملیات به دست میآید.
اگر برنامه را اجرا کنید، با استثنای زیر مواجه خواهید شد:
An exception of type 'System.InvalidOperationException' occurred in System.Windows.Forms.dll but was not handled in user code Additional information: Cross-thread operation not valid: Control 'txtResults' accessed from a thread other than the thread it was created on.
private void btnGetInfo_Click(object sender, EventArgs e) { var sync = SynchronizationContext.Current; var req = (HttpWebRequest)WebRequest.Create("http://www.google.com"); req.Method = "HEAD"; req.BeginGetResponse( asyncResult => { var resp = (HttpWebResponse)req.EndGetResponse(asyncResult); var headersText = formatHeaders(resp.Headers); sync.Post(delegate { txtResults.Text = headersText; }, null); }, null); }
برای درک بهتر آن، سه break point را پیش از متد BeginGetResponse، داخل Async calback و داخل delegate متد Post قرار دهید. پس از اجرای برنامه، از منوی دیباگ در VS.NET گزینهی Windows و سپس Threads را انتخاب کنید.
در اینجا همانطور که مشخص است، کد داخل delegate تعریف شده، در ترد اصلی برنامه اجرا میشود و نه یکی از Worker threadهای ثانویه.
هر چند استفاده از متدهای تو در تو و lambda syntax، نیاز به تعریف چندین متد جداگانه را برطرف کردهاست، اما باز هم کد سادهای به نظر نمیرسد. در سی شارپ 5، برای مدیریت بهتر تمام مشکلات یاد شده، پشتیبانی توکاری از اعمال غیرهمزمان، به هستهی زبان اضافه شدهاست.
Syntax ابتدایی یک متد Async
در ابتدا کلاس و متد Async زیر را در نظر بگیرید:
using System; using System.Threading.Tasks; namespace Async01 { public class AsyncExample { public async Task DoWorkAsync(int parameter) { await Task.Delay(parameter); Console.WriteLine(parameter); } } }
- در مدل برنامه نویسی TAP، متدهای غیرهمزمان باید یک Task را بازگشت دهند؛ یا نمونهی جنریک آنرا. البته کامپایلر، async void را نیز پشتیبانی میکند ولی در قسمتهای بعدی بررسی خواهیم کرد که چرا استفاده از آن مشکلزا است و باید از آن پرهیز شود.
- همچنین مطابق TAP، اینگونه متدها باید به پسوند Async ختم شوند تا استفاده کننده در حین کار با Intellisense، بتواند آنها را از متدهای معمولی سریعتر تشخیص دهد.
- از واژهی کلیدی async نیز استفاده میگردد تا کامپایلر از وجود اعمال غیر همزمان مطلع گردد.
- await به کامپایلر میگوید، عبارت پس از من، یک وظیفهی غیرهمزمان است و ادامهی کدهای نوشته شده، تنها زمانی باید اجرا شوند که عملیات غیرهمزمان معرفی شده، تکمیل گردد.
در متد DoWorkAsync، ابتدا به اندازهای مشخص توقف حاصل شده و سپس سطر بعدی یعنی Console.WriteLine اجرا میشود.
یک اشتباه عمومی! استفاده از واژههای کلیدی async و await متد شما را async نمیکنند.
برخلاف تصور ابتدایی از بکارگیری واژههای کلیدی async و await، این کلمات نحوهی اجرای متد شما را async نمیکنند. این کلمات صرفا برای تشکیل متدهایی که هم اکنون غیرهمزمان هستند، مفید میباشند. برای توضیح بیشتر آن به مثال ذیل دقت کنید:
public async Task<double> GetNumberAsync() { var generator = new Random(); await Task.Delay(generator.Next(1000)); return generator.NextDouble(); }
در ادامه برای استفاده از آن خواهیم داشت:
public async Task<double> GetSumAsync() { var leftOperand = await GetNumberAsync(); var rightOperand = await GetNumberAsync(); return leftOperand + rightOperand; }
در کدهای همزمان متداول، سطر اول ابتدا انجام میشود و بعد سطر دوم و الی آخر. با استفاده از واژهی کلیدی await یک چنین عملکردی را با اعمال غیرهمزمان خواهیم داشت. پیش از این برای مدیریت اینگونه اعمال از یک سری callback و یا رخداد استفاده میشد. برای مثال ابتدا عملیات همزمانی شروع شده و سپس نتیجهی آن در یک روال رخداد گردان جایی در کدهای برنامه دریافت میشد (مانند مثال ابتدای بحث). اکنون تصور کنید که قصد داشتید جمع نهایی حاصل دو عملیات غیرهمزمان را از دو روال رخدادگردان جدا از هم، جمع آوری کرده و بازگشت دهید. هرچند اینکار غیرممکن نیست، اما حاصل کار به طور قطع آنچنان زیبا نبوده و قابلیت نگهداری پایینی دارد. واژهی کلیدی await، انجام اینگونه امور غیرهمزمان را طبیعی و همزمان جلوه میدهد. به این ترتیب بهتر میتوان بر روی منطق و الگوریتمهای مورد استفاده تمرکز داشت، تا اینکه مدام درگیر مکانیک اعمال غیرهمزمان بود.
امکان استفاده از واژهی کلیدی await در هر جایی از کدها وجود دارد. برای نمونه در مثال زیر، برای ترکیب دو عملیات غیرهمزمان، از await در حین تشکیل عملیات ضرب نهایی، دقیقا در جایی که مقدار متد باید بازگشت داده شود، استفاده شدهاست:
public async Task<double> GetProductOfSumAsync() { var leftOperand = GetSumAsync(); var rightOperand = GetSumAsync(); return await leftOperand * await rightOperand; }
Operator '*' cannot be applied to operands of type 'System.Threading.Tasks.Task<double>' and 'System.Threading.Tasks.Task<double>'
اگر متد DownloadString همزمان ابتدای بحث را نیز بخواهیم تبدیل به نمونهی async سیشارپ 5 کنیم، میتوان از متد الحاقی جدید آن به نام DownloadStringTaskAsync کمک گرفت:
public async Task<string> DownloadAsync() { var webClient = new WebClient(); return await webClient.DownloadStringTaskAsync("http://www.google.com"); }
سؤال: آیا استفاده از await نیز ترد جاری را قفل میکند؟
اگر به کدها دقت کنید، استفاده از await به معنای صبر کردن تا پایان عملیات async است. پس اینطور به نظر میرسد که در اینجا نیز ترد اصلی، همانند قبل قفل شدهاست.
public void TestDownloadAsync() { Debug.WriteLine("Before DownloadAsync"); DownloadAsync(); Debug.WriteLine("After DownloadAsync"); }
Before DownloadAsync After DownloadAsync
برنامههای Async و نگارشهای مختلف دات نت
شاید در ابتدا به نظر برسد که قابلیتهای جدید async و await صرفا متعلق هستند به دات نت 4.5 به بعد؛ اما خیر. اگر کامپایلری را داشته باشید که از این واژههای کلیدی را پشتیبانی کند، امکان استفاده از آنها را با دات نت 4 نیز خواهید داشت. برای این منظور تنها کافی است از VS 2012 به بعد استفاده نمائید. سپس در کنسول پاورشل نیوگت دستور ذیل را اجرا نمائید (فقط برای برنامههای دات نت 4 البته):
PM> Install-Package Microsoft.Bcl.Async
یکی دیگر از تغییرات عمدهی ASP.NET Core با نگارشهای قبلی آن، نحوهی مدیریت مسیریابیهای سیستم است. در نگارشهای قبلی مبتنی بر HTTP Moduleها، مسیریابیها توسط یک HTTP Module مخصوص، با pipeline اصلی ASP.NET یکپارچه شدهاند و زمانیکه مسیر درخواستی با تنظیمات سیستم تطابق داشته باشد، پردازش کار به HTTP Handler مخصوص ASP.NET MVC منتقل میشود:
اما در ASP.NET Core مبتنی بر میان افزارها، زیر ساخت مسیریابی به صورت زیر تغییر کردهاست:
میان افزار ASP.NET MVC را که در قسمت قبل فعال کردیم، باید بتواند کنترلر و اکشن متد متناظر با URL درخواستی را مشخص کند. این تصمیم گیری نیز بر اساس تنظیماتی به نام Routing انجام میشود. در قسمت قبل، حالت ساده و پیش فرض این تنظیمات را مورد استفاده قرار دادیم
app.UseMvcWithDefaultRoute();
public static IApplicationBuilder UseMvcWithDefaultRoute(this IApplicationBuilder app) { if (app == null) throw new ArgumentNullException("app"); return app.UseMvc((Action<IRouteBuilder>) (routes => routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}"))); }
روش دیگر معرفی این تنظیمات، استفاده از Attribute routing است:
[Route("[controller]/[action]")]
مسیریابیهای قراردادی
در قسمت قبل، یک POCO Controller را به صورت ذیل تعریف کردیم و این کنترلر، بدون تعریف هیچ نوع مسیریابی خاصی در دسترس بود:
namespace Core1RtmEmptyTest.Controllers { public class HomeController { public string Index() { return "Running a POCO controller!"; } } }
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); });
در این تعاریف، هر کدام از قسمتهای قرارگرفتهی داخل {}، مشخص کنندهی قسمتی از URL دریافتی بوده و نامهای controller و action در اینجا جزو نامهای از پیش مشخص شده هستند و برای نگاشت اطلاعات مورد استفاده قرار میگیرند. برای مثال اگر آدرس home/index/ درخواست شد، برنامه به کلاس HomeController و متد عمومی Index آن هدایت میشود. همچنین قسمت آخر این پردازش به ?id ختم شدهاست. وجود ?، به معنای اختیاری بودن این پارامتر است و اگر در URL ذکر شود، به پارامتر id این اکشن متد، نگاشت خواهد شد. مواردی که پس از = ذکر شدهاند، مقادیر پیش فرض مسیریابی هستند. برای مثال اگر صرفا آدرس home/ درخواست شود، مقدار اکشن متد آن با مقدار پیش فرض index جایگزین خواهد شد و اگر تنها مسیر / درخواست شود، کنترل Home و اکشن متد Index آن پردازش میشوند.
در اینجا به هر تعدادی که نیاز است میتوان متدهای routes.MapRoute را فراخوانی و استفاده کرد؛ اما ترتیب تعریف آنها حائز اهمیت است. هر مسیریابی که در ابتدای لیست اضافه شود، حق تقدم بالاتری خواهد داشت و هر تطابقی با یکی از مسیریابیهای تعریف شده، در همان سطح سبب خاتمهی پردازش سایر مسیریابیها میشود.
استفاده از Attributes برای تعریف مسیریابیها
بجای تعریف قرار دادهای پیش فرض مسیریابی در کلاس آغازین برنامه، میتوان از ویژگی Route نیز استفاده کرد. هرچند روش تعریف مسیریابیهای قراردادی، از نگارشهای آغازین ASP.NET MVC به همراه آن بودهاند، اما با زیاد شدن تعداد کنترلرها و مسیریابیهای سفارشی هر کدام، اینبار با نگاه کردن به یک کنترلر، سریع نمیتوان تشخیص داد که چه مسیریابیهای خاصی به آن مرتبط هستند. برای ساده سازی مدیریت برنامههای بزرگ و ساده سازی تعاریف مسیریابیهای خاص آنها، استفاده از ویژگی Route نیز به ASP.NET MVC اضافه شدهاست.
یک مثال: کنترلر About را درنظر بگیرید:
using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { public class AboutController : Controller { public ActionResult Hello() { return Content("Hello from DNT!"); } public ActionResult SiteName() { return Content("DNT"); } } }
اما اگر آدرس /About/ را درخواست دهیم چطور؟ چون در مسیریابی پیش فرض، تعریف {action=Index} را داریم، یعنی هر زمانیکه در URL درخواستی، قسمت action آن ذکر نشد، آنرا با index جایگزین کن و این کنترلر دارای متد Index نیست. در ادامه اگر بخواهیم متد Hello را تبدیل به متد پیش فرض این کنترلر کنیم، میتوان با استفاده از ویژگی Route به صورت ذیل عمل کرد:
using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { [Route("About")] public class AboutController : Controller { [Route("")] public ActionResult Hello() { return Content("Hello from DNT!"); } [Route("SiteName")] public ActionResult SiteName() { return Content("DNT"); } } }
AmbiguousActionException: Multiple actions matched. The following actions matched route data and had all constraints satisfied: Core1RtmEmptyTest.Controllers.AboutController.Hello (Core1RtmEmptyTest) Core1RtmEmptyTest.Controllers.AboutController.SiteName (Core1RtmEmptyTest)
روش بهتر و refactoring friendly آن نیز به صورت ذیل است:
using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { [Route("[controller]")] public class AboutController : Controller { [Route("")] public ActionResult Hello() { return Content("Hello from DNT!"); } [Route("[action]")] public ActionResult SiteName() { return Content("DNT"); } } }
یک نکته: در حین تعریف مسیریابی یک کنترلر میتوان پیشوندهایی را نیز ذکر کرد؛ برای مثال:
[Route("api/[controller]")]
تعریف قیود، برای مسیریابیهای تعریف شده
فرض کنید به کنترلر About فوق، اکشن متد ذیل را که یک خروجی JSON را بازگشت میدهد، اضافه کردهایم:
//[Route("/Users/{userid}")] [Route("Users/{userid}")] public IActionResult GetUsers(int userId) { return Json(new { userId = userId }); }
و اگر مسیریابی Users/{userid} را تعریف کنیم، یعنی این مسیریابی پس از ذکر کنترلر about، به عنوان یک اکشن متد آن مفهوم پیدا میکند:
http://localhost:7742/about/users/1
در هر دو حالت، ذکر پارامتر userid الزامی است (چون با ? مشخص نشدهاست)؛ مانند:
[Route("/Users/{userid:int?}")]
[Route("Users/{userid:int}")]
قیودی را که در اینجا میتوان ذکر کرد به شرح زیر هستند:
• alpha - معادل است با (a-z, A-Z).
• bool - برای تطابق با مقادیر بولی.
• datetime - برای تطابق با تاریخ میلادی.
• decimal - برای تطابق با ورودیهای اعشاری.
• double - برای تطابق با اعداد اعشاری 64 بیتی.
• float - برای تطابق با اعداد اعشاری 32 بیتی.
• guid - برای تطابق با GUID ها
• int - برای تطابق با اعداد صحیح 32 بیتی.
• length - برای تعیین طول رشته.
• long - برای تطابق با اعداد صحیح 64 بیتی.
• max - برای ذکر حداکثر مقدار یک عدد صحیح.
• maxlength - جهت ذکر حداکثر طول رشتهی مجاز ورودی.
• min - برای ذکر حداقل مقدار یک عدد صحیح.
• minlength - جهت ذکر حداقل طول رشتهی مجاز ورودی.
• range - ذکر بازهی اعداد صحیح مجاز.
• regex - ذکر یک عبارت با قاعده جهت مشخص سازی الگوی قابل پذیرش.
برای ترکیب چندین قید مختلف نیز میتوان از : استفاده کرد:
[Route("/Users/{userid:int:max(1000):min(10)}")]
ذکر نام Route برای ساده سازی تعریف آدرسی به آن
در حین تعریف یک Route میتوان نام دلخواهی را نیز به آن انتساب داد (همانند نام default مسیریابی ثبت شدهی در کلاس آغازین برنامه):
[Route("/Users/{userid:int}", Name="GetUserById")]
string uri = Url.Link("GetUserById", new { userid = 1 });
مشخص سازی ترتیب پردازش مسیریابیها
ترتیب مسیریابیهای ثبت شدهی در کلاس آغازین برنامه، همان ترتیب افزوده شدن و ذکر آنها است.
در اینجا میتوان از خاصیت order نیز استفاده کرد و اعداد کوچکتر، ابتدا پردازش میشوند (مقدار پیش فرض آن نیز صفر است):
[Route("/Users/{userid:int}", Name = "GetUserById", Order = 1)]
امکان تعریف قیود سفارشی
اگر قیودی که تا اینجا ذکر شدند، برای کار شما مناسب نبودند و نیاز بود تا الگوریتم خاصی را جهت محدود سازی دسترسی به یک مسیریابی خاص پیاده سازی کنید، میتوان به صورت ذیل عمل کرد:
using System; using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; namespace Core1RtmEmptyTest { public class CustomRouteConstraint : IRouteConstraint { public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) { object value; if (!values.TryGetValue(routeKey, out value) || value == null) { return false; } long longValue; if (value is long) { longValue = (long)value; return longValue != 10; } var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); if (long.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out longValue)) { return longValue != 10; } return false; } } }
public class CustomRouteConstraint : IRouteConstraint
در آخر برای ثبت و معرفی آن باید به متد ConfigureServices کلاس آغازین برنامه مراجعه کرد:
public void ConfigureServices(IServiceCollection services) { services.AddRouting(options =>options.ConstraintMap.Add("Custom", typeof(CustomRouteConstraint)));
[Route("/Users/{userid:int:custom}")]
یک نکته: اگر به سورس ASP.NET Core مراجعه کنید ، تمام قیودی را که پیشتر نام بردیم (مانند int، guid و امثال آن) نیز به همین روش تعریف و پیشتر ثبت شدهاند.
معرفی بستهی نیوگت Microsoft.AspNetCore.SpaServices
مسیریابیهای پیش فرض ASP.NET Core با مسیریابیهای برنامههای SPA مانند AngularJS (و امثال آن) تداخل دارند؛ از این جهت که درخواستهای رسیدهی به سرور، ابتدا به موتور پردازشی ASP.NET وارد میشوند و اگر یافت نشدند، کاربر با پیام 404 مواجه خواهد شد و دیگر در اینجا برنامه به مسیریابی خاص مثلا AngularJS 2.0 هدایت نمیشود.
برای این موارد مرسوم است که یک fallback route را در انتهای مسیریابیهای موجود اضافه کنند (به آن catch all هم میگویند)
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); routes.MapRoute( name: "spa-fallback", template: "{*url}", defaults: new { controller = "Home", action = "Index" }); });
برای حل این مشکل مایکروسافت بستهای را به نام Microsoft.AspNetCore.SpaServices ارائه داده است.
برای افزودن آن بر روی گره references کلیک راست کرده و گزینهی manage nuget packages را انتخاب کنید. سپس در برگهی browse آن Microsoft.AspNetCore.SpaServices را جستجو کرده و نصب نمائید:
انجام این مراحل معادل هستند با افزودن یک سطر ذیل به فایل project.json برنامه:
{ "dependencies": { //same as before "Microsoft.AspNetCore.SpaServices": "1.0.0-beta-000007" },
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); routes.MapSpaFallbackRoute("spa-fallback", new { controller = "Home", action = "Index" }); });
// Serve wwwroot as root app.UseFileServer(); // Serve /node_modules as a separate root (for packages that use other npm modules client side) app.UseFileServer(new FileServerOptions { // Set root of file server FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "node_modules")), // Only react to requests that match this path RequestPath = "/node_modules", // Don't expose file system EnableDirectoryBrowsing = false });
داستانهای کاربر
توسعهدهندگان، ویژگیهای مورد نظر پروژه را با جمعآوری نیازمندیها، در قالب داستانهای کاربر احصاء میکنند و به هرکدام متناسب با پیچیدگیاش امتیازی اختصاص میدهند. با لیستی از داستانهای دارای ابعادی مشخص و بودجه و زمان مورد نیاز برای هرکدام، مشتریان قادر به این انتخابند که کدام ویژگیها در تکرار (iteration) بعدی باقی بماند. مشخصکردن بودجه و زمان، یعنی تعیین حجم کاری که تیم توسعه برای انجام آن ویژگی، نیاز میداند. برآورد بودجۀ مورد نیاز تکرار اول به صورت تجربی خواهد بود و ممکن است این تخمین در ابتدا نادرست باشد؛ اما با شروع تکرار بعدی درست خواهد شد. در پایان هر تکرار، امتیازات به دست آمده از داستانهای کامل شده را جمع کنید. مجموع این امتیازات، نشانگر سرعت شما خواهد بود. این سرعت شاخص خوبی جهت چگونگی بودجهبندی مرحلۀ بعد است. هنگامیکه امتیازات جمعآوری شده به حد مطلوبی رسید، «سرعت پیشرَوی»، شاخص مناسب دیگری برای بودجهبندی است که عبارت است از متوسط سرعت سه تکرار آخر.
با این کار شما به دیدگاه مناسبی از فاز برنامهریزی دست پیدا میکنید. حال اجاز دهید نگاه دقیقتری به شیوههای برنامهریزی داشته باشیم.
برنامهریزی (planning game) دو فاز دارد: فاز شناسایی و فاز برنامهریزی. در فاز شناسایی، توسعهدهندگان و مشتریان را دور هم جمع میکنند تا دربارۀ نیازمندیهای سیستم در حال طراحی، گفتگو کنند. به خاطر داشته باشید که این کار تا وقتی انجام میشود که به ویژگیهایی (features) کافی برای شروع انجام کار برسیم و البته واضح است که چنین لیستی از ویژگیهای احصاء شده، هرچقدر هم که تلاش شود، کامل نخواهد بود. مشتریان اغلب اوقات، خواستهی خود را یا نمیدانند یا نمیتوانند به خوبی توضیح دهند. بنابراین معمولاً این لیست به مرور تغییر میکند. در ضمن آنکه برخی ویژگیها دقیقتر میشود، مواردی نیز ممکن است به لیست افزوده شوند یا حتی میتوان برخی ویژگیهای نامربوط را از لیست حذف کرد. در مرحلۀ شناسایی، ویژگیها به داستانهای کاربر تجزیه شد و ثبت میشوند.
یک داستان کاربر عبارت است از توصیفی کوتاه از یک ویژگی که نمایانگر یک واحد ارزش کسب و کار برای مشتری است. داستانهای کاربر از زبان کاربر بیان شدهاند و قالب نوشتاری زیر را دارند:
به عنوان «نوع کاربر»، من میخواهم «یک فعل» تا «منفعتی برای کسب و کار»
یا به صورت:
به منظور «یک دلیل» به عنوان «نقش کاربر» من میخواهم «یک فعل»
داستانهای کاربر معمولاً در جلسهی گفتگو با مشتری بر روی کارتهای راهنما نوشته شده و در آن از واژگان و ادبیاتی استفاده میشود که برای مشتری قابل فهم باشد. ممکن است چنین بیاندیشید که ثبت نیازمندیها، خلاف مزیتهای چابکسازی است؛ چرا که تولید نرمافزار کارآمد و چابک مبتنی بر مستندسازی گسترده و فراگیر خواهد بود. در واقع، داستانهای کاربر به طور ساده فقط یادآورندۀ جزئیات بیشتری از گفتگوی انجام شدهاند که به عمد بهصورت کوتاه و دقیق نوشته شدهاند. فهم دقیقتر جزئیات کار، مستلزم ارتباط بیشتر میان توسعهدهندگان و مشتری است. در واقع همسو با این اصل چابک که میگوید: «مؤثرترین و کارآمدترین شیوۀ انتقال اطلاعات در میان تیم توسعه و به خارج از آن، گفتگوی چهره به چهره است.»
هنگام احصاء ویژگیهای پروژه تحت عنوان داستانهای کاربری، از اصول INVEST (که پیشتر گفته شد) جهت کنترل مناسب بودن این داستانها استفاده کنید. شکل 2-3 مثالی از یک داستان کاربر را که توصیفکنندۀ ویژگی «افزودن یک بن تخفیف به سبد خرید» است، نشان میدهد. «تخفیف گرفتن»، یک منفعت کسب و کار است برای عامل (actor) اصلی، یعنی مشتری. «یک بن تخفیف به سبد بیفزا» نام فرآیند یا «use case» مربوط است.
معیار پذیرش همچنین به تشخیص جزئیات بیشتر یا شناسایی وابستگیها کمک میکند. مثلاً در شکل 3-3 تعریف «in date» چیست و چه چیزی حدود یک بن تخفیف را مشخص میکند؟ معمولاً باید حداقل سه معیار پذیرش وجود داشته باشد. در فصل بعد در یک مطالعۀ موردی، مطالب بیشتری را دربارۀ داستانهای کاربر خواهید آموخت.
هنگامیکه تیم و مشتریان حسکنند که حدود 75 درصد از ویژگیهای اصلی احصاء شده است، توسعهدهندگان ابعاد داستانها را تخمین زده و آنها را برای اولویتبندی توسط مشتری آماده میکنند.
تخمین
شکی در آن نیست که تخمینزدن کار سختی است. تخمینزدن هم دانش است هم هنر. تخمینزدن در یک پروژۀ تازه شروع شده، بسیار سخت است زیرا مجهولات بسیاری در آن وجود دارد.
یکی از روشهای تخمین گروهی، روش «Planning Poker» نام دارد. در این روش همهی اعضای فنی تیم، متشکل از توسعهدهندگان نرمافزار، تحلیلگران، متخصصان امنیت و زیرساخت، مشارکت میکنند. نقش مشتری در این حالت پاسخگویی به سؤالات احتمالی اعضای تیم است تا ایشان بهتر بتوانند تخمین بزنند.
شیوۀ انجام کار به این صورت است که عضوی از تیم، یک داستان کاربر را برداشته و آن را برای تیم توضیح میدهد. تیم دربارۀ آن ویژگی با مشتری گفتگو کرده تا جزئیات بیشتری را دریابد. وقتی که تیم به درک خوبی از آن رسید، رأیگیری آغاز میشود. هر عضو تیم با یک کارت، از مجموعهای ازکارتهایی با شمارههای 0، 1 ، 2، 3، 5، 8، 13، 20، 40 و 100 رأی خود را اعلام میکند.
تیم باید از داستانی شروع کند که نسبتاً کوچک و ساده باشد. این داستان به عنوان مبنا انتخاب میشود. هر تخمین داستان کاربر، باید به نسبت این داستان کوچک انجام شود. اگر داستان مبنا به خوبی انتخاب نشود، بقیۀ تخمینها نادرست خواهد بود.
اگر همهی اعضای تیم به یک صورت رأی دهند، آن رأی، تخمین آن داستان خواهد شد. اگر اختلاف آراء وجود داشت، ناظر یعنی کسی که رأی نمیدهد، از افرادی که بالاترین و پایینترین امتیاز را دادهاند، میخواهد که علل خود را توضیح دهند. سپس تیم مجدداً گفتگو کرده و دوباره رأیگیری میکند. طبق تجربه، خوب است که زمان معقولی، برای هر گفتگو در نظر گرفته شود.
اگر تخمین یک داستان به دلیل فقدان دانش فنی، بسیار سخت بود، مناسب است که این داستان کنار گذاشته شود و داستان دیگری برای برطرف کردن مشکل ناآشنایی با دانش فنی مورد نظر فراهم شود. بدین ترتیب تیم توسعه در موقعیت بهتری میتواند نسبت به داستان جدید تخمین بزند.
داستانهایی که بیش از یک هفته کار نیاز داشته باشند با عنوان داستانهای حماسی (epic stories) شناخته میشوند و معمولاً برای تخمین بسیار بزرگ هستند. در واقع، این داستانها به چند داستان کوچکتر که قابل فهمتر و به آسانی قابل تخمین باشند، تجزیه میشوند. این بدان معناست که ایجاد یک داستان کاربر از تعداد انبوهی ویژگی موجب کاهش کارآیی خواهد شد.
تخمین در تیمی که افراد آن تاکنون با همدیگر سابقۀ همکاری نداشته باشند، خیلی پایین یا خیلی بالاست. اما با استمرار هر تکرار و تجربه و دانش بیشتر افراد، تخمین داستانها بهتر میشود.
استفاده از ابزار Planning Poker مزایای بسیاری دربردارد. دقت تخمین بالا میرود؛ زیرا مسأله از منظر تخصصهای گوناگون مورد بررسی قرار گرفته است. همچنین به تیم کمک میکند که هم رأی شوند و گفتگو میان اعضاء را تسهیل میکند. پس از آنکه داستانها تخمین زده شدند، مشتری و صاحب محصول با تیم توسعه در تولید چگونگی انتشار نسخهها، همکاری میکنند.
برنامه انتشار
اگرچه کدهای قابل ارسال، قابلیت انتشار در پایان هر تکرار را دارند، اما یک پروژه XP در چند سری منتشر شده است. یک نسخۀ منتشرشده، متشکل از تعداد مناسبی داستان برای عرضۀ ارزش کسب وکاری است که به کوچک نگه داشتن آن کمک میکند. بسیار مناسب است که یک موضوع یا هدف خاص را در ضمن هرنسخۀ انتشار، مد نظر قرار داد تا کمک کند که هر نسخۀ انتشار بر برخی ارزشهای کسب و کاری متمرکز شده و آن را هدایت کند. معمولاً یک نسخۀ انتشار، متشکل از چهار تکرار است؛ همانطور که در شکل 4-3 نشان داده شده است.
در برنامهریزی نسخههای انتشار، طول یک تکرار نیز تعیین میشود که معمولاً بین دو تا چهار هفته است. مطابق تجربه، اگر محیط کار شما دچار بینظمی و اختلالات دائمی است، میتوانید دورۀ تکرار را به یک هفته محدود کنید.
یکی از پروژههایی که ما بر روی آن کار میکردیم، برنامهای بود که نگهداری آن بسیار سخت و فوقالعاده ناپایدار بود. مشتری مکررا با تیم تماس گرفته و اشکالات بحرانساز و ایراداتی را که مخل برنامه بودند، گزارش میکرد. در ابتدای کار دوره، تکرار ما هفتگی بود. به همین دلیل چون حلقۀ بازخوردگیریمان کوچک بود، میتوانستیم بر پایدارسازی پروژه در هر دوره کاری تمرکز کنیم. هنگامی که محصول به پایداری مناسبتری رسید و تماسهای مشتری کم شد، قادر شدیم تا در هر دوره، دقت بیشتری بر روی مسائل به خرج دهیم.
اگر قصد دارید به صورت دقیق بر روی حلقۀ بازخورد متمرکز شوید، دورهی تکرار یک هفتهای، مدل خوبی است. اما این مدل سربار زیادی را به دلیل ضرورت تقسیم داستانهای کاربر باید به بخشهای کوچکتری تا آن اندازه که در یک دوره تکمیل شوند، بر پروژه تحمیل میکند. در ادامه خواهیم گفت که هر تکرار شامل برنامۀ ملاقات و بازبینی نیز هست.
بعد از مدتی که تیم با فرآیند کار آشناتر شد و نوبت به مشکلات با اولویت کمتر رسید، میتوان دورۀ تکرار را دو هفتهای در نظر گرفت. اما اگر پروژه به گونهای است که ویژگیهای بزرگتر را نمیتوان به موارد کوچکتری که قابل انجام در دورههای یک هفتهای باشد، تجزیه کرد و تیم هنوز در حال یادگیری است، دورههای بلندمدتتر قابل پذیرش است.
مشتری با توجه به طول دورۀ تکرار و بودجۀ داستان آغازین، انتخاب میکند که کدام داستان در هنگام انتشار نسخۀ اوّل، در تکرار اوّل کامل شود.
این مشتری است که داستانها را به گونهای اولویتبندی میکند تا مشخص شود که کدامیک بیشترین ارزش کسب و کار را فراهم میکند. از آنجایی که مشتری مسؤول داستانهای کاربر است، تیم باید به وی توضیح دهد که داستانهایی وجود دارند که صرفاً باید به جهت دلایل فنی ایجاد شوند.
معمولاً باید به داستانهای کاربریای که مستلزم ریسک بالا بوده یا دربرگیرندۀ مجهولات زیادی باشند، بیش از یک یا دو تکرار اختصاص داد.
برنامۀ تکرار
مشتری داستانهایی را که میخواهد در تکرار باشند، انتخاب میکند. برای هر داستان کاربر، مجموعهای از معیارهای پذیرش، تعریف شده است. همان طور که متوجه شدهاید ما در هر فاز، وقت بیشتر و بیشتری را صرف جمعآوری جزئیات هر داستان کاربر کرده و بصورت عمیقتری در آن غور میکنیم. این کار مفید است، زیرا اگر یک داستان کاربر ایجاد شده در ابتدای پروژه، ممکن است بعداً به عنوان داستانی کم اهمیت یا غیر مهم دیدهشود و بدون آنکه وقت خاصی برای آن صرف شده باشد، کنار گذاشته شود. اما اگر در ابتدای کار وقت زیادی صرف دقیقتر کردن داستانهای کاربر شود و بعداً بعضی از آنها کنار گذاشته شوند، در واقع وقت تلف شده است. بنابراین دقیقتر کردن یک داستان در جایی که مورد نیاز است، باید اتفاق بیفتد. در سطح برنامۀ تکرار، مجموعهای از معیارهای پذیرش را برای هر داستان کاربر تعریف میکنیم. معیار پذیرش به توسعهدهنده کمک میکند تا بداند که یک داستان کاربر به طور کامل انجام میشود. این معیارها به صورت مؤلفههایی از بافرض/هنگامی که/درنتیجه، نوشته میشود.
مثالهای زیر چگونگی انجام این کار را توصیف میکند:
عنوان ویژگی: افزودن کالایی به سبد
به عنوان یک مشتری میخواهم بتوانم کالایی را به سبدم اضافه کنم؛ به نحوی که قادر باشم به خرید خود ادامه دهم.
سناریو: سبد خالی
با فرض اینکه یک سبد خالی دارم، در نتیجه جمع تعداد کالایی که برای سفارش در سبد من وجود دارد، صفر است.
سناریو: افزودن یک کالا به سبد
با فرض اینکه یک سبد خالی دارم هنگامی که کالایی با شناسۀ 1 به سبدم اضافه میکنم، در نتیجه جمع کالاهای قابل سفارش در سبدم 1 میشود.
سناریو: افزودن کالاهایی به سبد
با فرض اینکه یک سبد خالی دارم، هنگامی که کالایی با شناسۀ 1 و کالایی با شناسۀ 2 به سبدم اضافه میکنم، در نتیجه جمع کالاهای قابل سفارش در سبدم 2 میشود.
سناریو: دو بار افزودن یک کالا
با فرض اینکه یک سبد خالی دارم هنگامی که کالایی با شناسۀ 1 به سبدم اضافه میکنم و هنگامی که کالایی با شناسۀ 1 را مجدداً به سبدم اضافه میکنم، در نتیجه تعداد کالاهای با شناسۀ 1 در سبد من باید 2 باشد.
سناریو: افزودن یک کالای تمام شده به سبد
با فرض اینکه یک سبد خالی دارم و کالایی با شناسۀ 2 در انبار وجود نداشته باشد، هنگامی که من کالایی با شناسۀ 2 را به سبد خودم اضافه میکنم، در نتیجه جمع تعداد کالای قابل سفارش در سبد من باید 0 باشد و به کاربر، موجود نبودن آن کالا را هشدار دهد.
یک آزمون پذیرش (acceptance) به زبان متعارف در قوانین کسب و کار نوشته میشود. در مثال سبد خرید، این سؤال پیش میآید که چگونه میتوان یک محصول را از سبد کالا، حذف کرد و اگر یک جنس اکنون در انبار نیست و کاربر پیام هشدار دریافت کرده است، در ادامه چه اتفاقی باید بیفتد؟ سناریوها به تیم در کشف ملزومات کسب و کار و تصریح آنها کمک میکند.
این سناریوها توسط توسعهدهنده به عنوان نقطۀ شروع آزمونهای واحد در توسعۀ آزمون محور و رفتار محور استفاده میشود. سناریوها همچنین در آزمودن معیارهای پذیرش به توسعهدهنده کمک کرده و توسعهدهنده و تستکننده را قادر میسازند که بر روی اتمام داستان اتفاق نظر داشته باشند.
بعد از آنکه سناریوهای معیار پذیرش تعیین شد، تیم توسعه، هر داستان را به تعدادی وظیفه تقسیم میکند و وظایف مرتبط به یک داستان، در تابلوی وظایف قرارگرفته و تیم توسعه تخمینهای خود را در قالب یکی از واحدهای اندازهگیری، مثلاً نفرساعت اعلام میکند. شکل 5-3 یک تابلوی وظیفه را نمایش میدهد.
به عنوان مثال وظایف میتوانند شامل ایجاد طرح یک بانک اطلاعاتی برای یک داستان یا یکپارچهسازی آن با بخشی موجود در سیستم باشند. وظایف شامل مؤلفههای فنی مانند تهیۀ گزارش از زیرسیستمها یا چارچوب مدیریت استثنائات نیز میباشد. اغلب اینگونه وظایف نادیدهگرفته میشود. یک داستان کاربر با وظایف گوناگونی گره خورده است. مثلاً:
داستان کاربر : به عنوان یک کاربر میخواهم بتوانیم یک کاربر را مدیریت کنم.
وظایف زیر از این داستان قابل استخراج است:
- طرحی برای بانک اطلاعات جهت ذخیرهسازی اطلاعات کاربر ایجاد کن.
- یک کلاس کاربر، برای مدیریت کاربر از درون برنامه ایجاد کن.
هر عضو تیم میتواند بر روی هر وظیفهای که بر روی تخته است، کار کند. هنگامیکه یک عضو گروه، وظیفهای را برمیدارد، باید نشانی از خود روی کارت آن وظیفه قراردهد ( مثلاً حروف اوّل اسمش) تا بقیۀ افراد بدانند که وی بر روی آن وظیفه، مشغول به کار است. معمولاً اما نه همیشه، یک توسعهدهنده همۀ وظایف مربوط به یک داستان را برمیدارد. این کار بدین معناست که آن توسعهدهنده با پشتیبانی تیم، مسؤول اتمام آن کار است.
در طول یک تکرار، هر روز باید گفتگوهایی سرپایی با حضور همۀ اعضای تیم انجام شود و مشکلاتی که ممکن است باعث به تأخیر افتادن ارائه کار شود، مورد بحث و بررسی قرار گیرد و همچنین تیم، لیست وظایف و تخته آن را بهروز کرده تا پیشرفت یا موانع آن به وضوح قابل رؤیت باشند.
با تشکر از آقای سید مجتبی حسینی
در این مجموعه مقالات، به بررسی و آموزش برنامه نویسی شیء گرا در جاوا اسکریپت میپردازیم. در طول آموزش، فرض را بر این قرار دادیم که شما به عنوان خوانندهی این مقاله، با مبانی جاوا اسکریپت آشنا میباشید و حداقل چند قطعه کد مفید را با جاوا اسکریپت نوشتهاید. همچنین کمی هم با مباحث شیء گرایی آشنا میباشید.
روال آموزش در این مجموعه به گونه است که در ابتدا به معرفی مباحث پیش نیاز جهت ورود به دنیای شیء گرایی در جاوا اسکریپت، پرداخته خواهد شد. سپس مباحث شیء گرایی را آغاز خواهیم نمود و تمامی نکات ریز و درشت آن را بررسی خواهیم کرد. پس از پایان این مقالات قادر خواهید بود تا تمامی کتابخانهها و Frameworkهای جاوا اسکریپتی را مطالعه نموده و به راحتی تکنیکهای کد نویسی آن را درک کنید. همچنین خود شما نیز برای نوشتن یک کتابخانه یا Framework جاوا اسکریپتی استاندارد و حرفهای، دست به کار شوید و یک کتابخانه سودمند را ارائه نمایید.
با چند مثال ساده و بصورت تقریبا مبتدی مسائل پایه را تشریح نموده و آرام آرام مباحث حرفه ای را آغاز خواهیم کرد. قرار است در پایان این آموزشها به بررسی الگوهای موجود در جاوا اسکریپت نیز بپردازم که مجموعه مقالات مربوط به الگوها را در قالب مجموعه مقالاتی مجزا ارائه مینمایم.
در تهیهی این مجموعه مقالات از منابع زیر استفاده شده است:
1. Pro JavaScript Techniques (John Resig ، خالق JQuery – فصول 2 و 3)
2. Professional JavaScript for Web Developers (Third Edition) (Nicholas C. Zakas – فصول 3، 4، 5، 6 و 7)
3. Object-Oriented JavaScript (Stoyan Stefanov – فصول 3، 4، 5، 6 و 8)
4. و تجربهی ناچیز اینجانب
توابع (Functions)
همیشه در برنامه، مجموعه دستوراتی وجود دارند که عمل خاصی را انجام میدهند و به دفعات نیز مورد استفاده قرار میگیرند. جهت جلوگیری از تکرار این دستورات، افزایش خوانایی و کاهش پیچیدگی برنامه، این دستورات را در بستهای به نام تابع قرار میدهیم تا در زمان نیاز، آن را فراخوانی یا اجرا نماییم. جهت تعریف تابع در جاوا اسکریپت به صورت زیر عمل مینماییم:
function <functionName> ([arg0, arg1, …, argN]) { <statements> }
به عنوان مثال:
function sayHello(name, message) { alert("Hello " + name + ", " + message); }
این تابع شامل دو آرگومان ورودی است که میتواند به صورت زیر فراخوانی شود:
sayHello("Meysam", "welcome to site");
خروجی این تابع “Hello Meysam, welcome to site” می باشد که به صورت یک پنجرهی پیغام، نمایش مییابد. تابع فوق هیچ مقداری را به عنوان خروجی به برنامهی اصلی یا محل فراخوانی خود بر نمیگرداند. اگر بخواهیم توسط تابع مقداری را برگردانیم میتوانیم از دستور return برای این منظور استفاده نماییم. به مثال زیر توجه کنید:
function sum(num1, num2) { return num1 + num2; }
تابع sum دو عدد را به عنوان آرگومان ورودی دریافت نموده و حاصل جمع آنها را توسط دستور return به عنوان خروجی بر میگرداند. تابع فوق را میتوان به صورت زیر فراخوانی نمود:
alert(sum(10, 20));
خروجی :
30
اینک به بررسی چند نکته در مورد دستور return میپردازیم. دستور return فقط میتواند یک مقدار را برگرداند و نمیتوان، چند مقدار را مقابل این دستور نوشت. همچنین روال اجرای تابع با رسیدن به دستور return خاتمه مییابد و دستورات بعد از آن اجرا نخواهند شد. به مثال زیر توجه کنید:
function sum(num1, num2) { return num1 + num2; alert("Hello"); }
در مثال فوق دستور alert به هیچ عنوان اجرا نخواهد شد؛ زیرا تابع با رسیدن به دستور return خاتمه مییابد. یک تابع میتواند شامل بیش از یک دستور return باشد.
function diff(num1,num2) { if (num1 > num2) return num1 - num2; else return num2 - num1; }
تابع فوق اختلاف دو عدد را بدست میآورد و اگر عدد اول بزرگتر باشد، عدد دوم را از عدد اول تفریق میکند؛ در غیر اینصورت عدد اول را از عدد دوم تفریق میکند و به عنوان خروجی بر میگرداند. نکتهی دیگری که لازم است بدانید این است که دستور return می تواند هیچ مقداری را بر نگرداند و به تنهایی بکار گرفته شود. در این صورت دقیقا بعد از دستور return سمی کالن (;) قرار میدهیم.
function checkNumber(num) { if (isNaN(num)) { alert("Not a number"); return; } alert(num+" is a number"); }
تابع فوق یک ورودی را دریافت مینماید و در صورتی که آرگومان ورودی عدد نباشد پیغام “Not a number” را نمایش میدهد و از تابع خارج میشود. در صورتی که آرگومان ورودی یک عدد باشد، پیغام دوم را نمایش میدهد.
توجه:
یک تابع بهتر است همیشه یک مقداری را به عنوان خروجی برگرداند و یا کلا هیچ مقداری را بر نگرداند. نوشتن توابعی که در برخی شرایط مقداری را به عنوان خروجی بر میگردانند و در برخی شرایط مقداری را برنمی گردانند موجب ایجاد پیچیدگی در اشکال زدایی میگردند. نوشتن تابعی به صورت زیر کمی گیج کننده و نامتعارف میباشد:
function sum(num1, num2) { if (isNaN(num1) || isNaN(num2)) return; // بهتر است حداقل مقدار 0 برگردانده شود return num1 + num2; }
کار با آرگومان ها
رفتار آرگومانها در جاوا اسکریپت نسبت به سایر زبانهای برنامه نویسی کاملا متفاوت میباشد. در جاوا اسکریپت تعداد و نوع آرگومانهای ارسالی بررسی نمیشوند و خطایی هم رخ نخواهد داد. به عنوان مثال اگر تابعی با 3 آرگومان ورودی دارید، میتوانید با 0 تا 3 آرگومان ورودی، آن تابع را فراخوانی نمایید. زیرا آرگومانها در سیستم داخلی جاوا اسکریپت به صورت یک آرایه ارسال میگردند و تابع توجه نمیکند که کدام آرگومانها به این آرایه ارسال شدهاند.
با توجه به قابلیتی که در مورد آرگومانها ذکر شد و به دلیل عدم مدیریت نوع و تعداد آرگومانهای ارسالی، مطمئنا جهت جلوگیری از بروز خطا در توابع، باید تعداد و نوع آرگومانهای ارسالی بررسی و مدیریت شوند. همچنین در هر تابع، آرایهای به نام arguments به صورت توکار تعبیه شده است که مدیریت آرگومانها را تسهیل میبخشد. به مثال زیر توجه کنید:
function sayHello() { alert("Hello " + arguments[0] + ", " + arguments[1]); } sayHello("Meysam", "welcome to site");
خروجی :
"Hello Meysam, welcome to site"
تابع فوق هیچ آرگومان ورودی ندارد ولی با دو آرگومان ورودی فراخوانی شده است. در داخل تابع توسط آرایه arguments به آرگومانهای ارسالی دسترسی پیدا کردیم. حال به مثال زیر توجه کنید:
function sum() { var s = 0; for (var i = 0; i < arguments.length; i++) s += arguments[i]; return s; } alert(sum()); alert(sum(10, 20, 30, 40, 50)); alert(sum(10));
خروجی :
0
150
10
در تابع فوق هیچ آرگومان ورودی وجود ندارد ولی این تابع را با 0، 5 و 1 آرگومان ورودی فراخوانی نمودیم. این تابع مجموع چند عدد را محاسبه و بر میگرداند و میتواند به تعداد نامحدودی عدد دریافت نماید. البته بهتر است نوع آرگومانهای ارسالی نیز بررسی شوند تا خطایی در محاسبات رخ ندهد. همچنین بجای حلقه for از حلقه for…in استفاده خواهم کرد.
function sum() { var s = 0; for (var i in arguments) { if (isNaN(arguments[i])) continue; s += arguments[i]; } return s; } alert(sum()); alert(sum(10, 20, "a", 40, 50)); alert(sum(10));
خروجی :
0
120
10
اگر دقت کرده باشید در فراخوانی دوم، رشته “a” به تابع ارسال شده است و چون آرگومانهای نامعتبر را مدیریت نمودهایم، خطایی در خروجی رخ نمیدهد. به مثال زیر نیز توجه نمایید:
function sum(a,b,c) { return a + b + c; } alert(sum(10, 20, 30)); alert(sum(10, 20)); alert(sum());
خروجی :
60
NaN
NaN
تابع فوق دارای 3 آرگومان ورودی میباشد؛ ولی ما این تابع را با 2 و 0 آرگومان ورودی فراخوانی نمودیم که خروجی نامناسبی را تولید نموده است. برای رفع این مشکل و معتبر سازی آرگومانهای ارسالی میتوانیم به صورت زیر عمل نماییم:
function sum(a, b, c) { if (isNaN(a)) a = 0; if (isNaN(b)) b = 0; if (isNaN(c)) c = 0; return a + b + c; } alert(sum(10, 20, 30)); alert(sum(10, 20)); alert(sum());
خروجی :
60
30
0
در تابع، قبل از انجام عمل محاسبه، بررسی کردیم که آرگومانهای ارسالی مقدار دهی شده باشند. در صورت عدم مقداردهی و یا مقداردهی نامناسب، آرگومان ارسالی را با صفر مقداردهی مینماید. اگر آرگومان مورد نظر به تابع ارسال نشود، مقدار پیش فرض آن undefined میباشد.
با توجه به مسائل مطرح شده در مورد توابع، این روش استفاده و کاربرد توابع، جزء معایب توابع در جاوا اسکریپت محسوب نمیشود. این تکنیک استفاده از توابع، موجب افزایش انعطاف پذیری توابع و آزادی عمل برنامه نویس میگردد که لذت بیشتری را به برنامه نویسی میدهد.
توجه:
به آرگومانهایی که در تابع دارای نام میباشند و یا به عبارتی، آرگومانهایی که نام آنها در تابع ذکر میشود، Named Arguments یا آرگومانهای نامی (اسمی و یا نامدار) میگویند. مثل آرگومان های a ، b وc در تابع sum .
عدم پشتیبانی از سربارگذاری یا Overloading
در زبانهای برنامه نویسی شیء گرا، امکان تعریف توابع هم نام وجود دارد؛ به شرطی که امضای این توابع با هم متفاوت باشند. منظور از امضاء، تعداد و نوع آرگومانهای ورودی میباشد. از آنجاییکه در سیستم داخلی جاوا اسکریپت، آرگومانها بصورت یک آرایه ارسال میشوند، بنابراین امضاء برای توابع مفهومی ندارد؛ پس نمیتوانیم توابع هم نام یا overloading داشته باشیم.
اگر دو تابع هم نام داشته باشیم، تابعی که دیرتر تعریف میشود، جایگزین تابع قبلی میگردد. به مثال زیر توجه کنید:
function calc(num1,num2) { return num1 + num2; } function calc(num1,num2) { return num1 - num2; } alert(calc(200,100));
خروجی :
100
همانطور که در مثال فوق مشاهده مینمایید، تابع دوم فراخوانی شدهاست و حاصل تفریق به عنوان خروجی نمایش یافته است.
{ // Use IntelliSense to find out which attributes exist for C# debugging // Use hover for the description of the existing attributes // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md "version": "0.2.0", "configurations": [ { "name": ".NET Core Launch (console)", "type": "coreclr", "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. "program": "${workspaceFolder}/bin/Debug/netcoreapp3.1/EFCoreDbFunctionsSample.dll", "args": [], "cwd": "${workspaceFolder}", // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console "console": "internalConsole", "stopAtEntry": false, "justMyCode": false, "symbolOptions": { "searchMicrosoftSymbolServer": true }, "suppressJITOptimizations": true, "env": { "COMPlus_ZapDisable": "1" } }, { "name": ".NET Core Attach", "type": "coreclr", "request": "attach", "processId": "${command:pickProcess}" } ] }
Loaded 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.1.8\System.Private.CoreLib.dll'. Symbols loaded. Loaded 'D:\Prog\1399\EFCoreDbFunctionsSample\bin\Debug\netcoreapp3.1\EFCoreDbFunctionsSample.dll'. Symbols loaded. . . . Loaded 'D:\Prog\1399\EFCoreDbFunctionsSample\bin\Debug\netcoreapp3.1\EFCoreSecondLevelCacheInterceptor.dll'. Symbols loaded. . . .
منابع مطالعاتی مرتبط
این موضوع اینقدر مهم است که تابحال چندین کتاب در مورد آن تالیف شده است:
Temporal Data & the Relational Model
Trees and Hierarchies in SQL
Developing Time-Oriented Database Applications in SQL
Temporal Data: Time and Relational Databases
Temporal Database Entries for the Springer Encyclopedia of Database Systems
Temporal Database Management
نکته مهمی که در این مآخذ وجود دارند، واژه کلیدی «Temporal data » است که میتواند در جستجوهای اینترنتی بسیار مفید واقع شود.
بررسی ابعاد زمان
فرض کنید کارمندی را استخدام کردهاید که ساعتی 2000 تومان از ابتدای فروردین ماه حقوق دریافت میکند. حقوق این شخص از ابتدای مهرماه قرار است به ساعتی 2400 تومان افزایش یابد. اگر مامور مالیات در بهمن ماه در مورد حقوق این شخص سؤال پرسید، ما چه پاسخی را باید ارائه دهیم؟ قطعا در بهمن ماه عنوان میکنیم که حقوقش ساعتی 2400 تومان است؛ اما واقعیت این است که این عدد از ابتدای استخدام او ثابت نبوده است و باید تاریخچه تغییرات آن، در نحوه محاسبه مالیات سال جاری لحاظ شود.
بنابراین در مدل سازی این سیستم به دو زمان نیاز داریم:
الف) actual time یا زمان رخ دادن واقعهای. برای مثال حقوق شخصی در تاریخ ابتدای مهر ماه تغییر کرده است. به این تاریخ در منابع مختلف Valid time نیز گفته میشود.
ب) record time یا زمان ثبت یک واقعه؛ مثلا زمان پرداخت حقوق. به آن Transaction time هم گفته شده است.
یک مثال:
record date actual date حقوق دریافتی 1392/01/01 1392/01/01 2000/روز 1392/02/01 1392/01/01 2000/روز ... 1392/07/01 1392/07/01 2400/روز ... 1392/17/01 1392/07/01 2400/روز
به ترکیب Valid Time و Transaction Time، اصطلاحا Bitemporal data میگویند.
مشکلات طراحیهای متداول اطلاعات وابسته به زمان
در طراحیهای متداول، عموما یک جدول کارمندان وجود دارد و یک جدول لیست حقوقهای پرداختی. رکوردهای لیست حقوقهای پرداختی نیز توسط یک کلید خارجی به اطلاعات هر کارمند متصل است؛ از این جهت که نمیخواهیم اطلاعاتی تکراری را در جدول لیست حقوقی ثبت کنیم و طراحی نرمال سازی شدهای مدنظر میباشد.
خوب؛ اول مهرماه حقوق شخصی تغییر کرده است. بنابراین کارمند بخش مالی اطلاعات شخص را به روز میکند. با این کار، کل سابقه حقوقهای پرداختی شخص نیز از بین خواهد رفت. چون وجود این کلید خارجی به معنای استفاده از آخرین اطلاعات به روز شده یک کارمند در جدول لیست حقوقی است. الان اگر از جدول لیست حقوقی گزارش بگیریم، کارمندان همواره از آخرین حقوق به روز شده خودشان استفاده خواهند کرد.
راه حلهای متفاوت مدل سازی اطلاعات وابسته به زمان
برای رفع این مشکل مهم، راه حلهای متفاوتی وجود دارند که در ادامه آنها را بررسی خواهیم کرد.
الف) نگهداری اطلاعات وابسته به زمان در جداول نهایی مرتبط
اگر حقوق پایه شخص در زمانهای مختلف تغییر میکند، بهتر است عدد نهایی این حقوق پرداختی نیز در یک فیلد مشخص، در همان جدول لیست حقوقی ثبت شود. این مورد به معنای داشتن «دادهای تکراری» نیست. از این جهت که دادهای تکراری است که اطلاعات آن در تمام زمانها، دارای یک مقدار و مفهوم باشد و اطلاعات حقوق یک شخص اینچنین نیست.
ب) نگهداری اطلاعات تغییرات حقوقی در یک جداول جداگانه
یک جدول ثانویه حقوق جاری کارمندان مرتبط با جدول اصلی کارمندان باید ایجاد شود. در این جدول هر رکورد آن باید دارای بازه زمانی (valid_start_time و valid_end_time) مشخصی باشد. مثلا از تاریخ X تا تاریخ Y، حقوق کارمند شماره 11 ، 2000 تومان در ساعت بوده است. از تاریخ H تا تاریخ Z اطلاعات دیگری ثبت خواهند شد. به این ترتیب با گزارشگیری از جدول لیست حقوقهای پرداخت شده، سابقه گذشته اشخاص محو نشده و هر رکورد بر اساس قرارگیری در یک بازه زمانی ثبت شده در جدول ثانویه حقوق جاری کارمندان تفسیر میشود.
در این حالت باید دقت داشت که بازههای زمانی تعریف شده، با هم تداخل نکنند و برنامه ثبت کننده اطلاعات باید این مساله را به ازای هر کارمند کنترل کند و یا با ثبت record_date، اجازه ثبت بازههای تکراری را نیز بدهد (توضیحات در قسمت بعد).
به این جدول، یک Temporal table نیز گفته میشود. نمونه دیگر آن، نگهداری قیمت یک کالا است از یک تاریخ تا تاریخی مشخص. به این ترتیب میتوان کوئری گرفت که بستنی مگنوم فروخته شده در ماه آبان سال قبل، بر مبنای قیمت آن زمان، دقیقا چقدر فروش کرده است و نه اینکه صرفا بر اساس آخرین قیمت روز این کالا گزارشگیری کنیم که در این حالت اطلاعات نهایی استخراج شده صحیح نیستند.
حال اگر به این طراحی در جدولی دیگر Transaction time یا زمان ثبت یک رکورد یا زمان ثبت یک فروش را هم اضافه کنیم، به جداول حاصل Bitemporal Tables میگویند.
مدیریت به روز رسانیها در جداول Temporal
در جداول Temporal، حذف فیزیکی اطلاعات مطلقا ممنوع است؛ چون سابقه سیستم را تخریب میکند. اگر اطلاعاتی در این جداول دیگر معتبر نیست باید تنها تاریخ پایان دوره آن به روز شوند یا یک رکورد جدید بر اساس بازهای جدید ثبت گردد.
همچنین به روز رسانیها در این جداول نیز معادل هستند با یک Insert جدید به همراه فیلد record_date و نه به روز رسانی واقعی یک رکورد قبلی (شبیه به سیستمهای حسابداری باید عمل کرد).
یک مثال:
فرض کنید حقوق کارمندی که مثال زده شد، در مهرماه به ساعتی 2400 تومان افزایش یافته است و حقوق نهایی نیز پرداخته شده است. بعد از یک ماه مشخص میشود که مدیر عامل سیستم گفته بوده است که ساعتی 2500 تومان و نه ساعتی 2400 تومان! (از این نوع مسایل در دنیای واقعی زیاد رخ میدهند!) خوب؛ اکنون چه باید کرد؟ آیا باید رفت و رکورد ساعتی 2400 تومان را به روز کرد؟ خیر. چون سابقه پرداخت واقعی صورت گرفته را تخریب میکند. به روز رسانی شما ابدا به این معنا نخواهد بود که دریافتی واقعی شخص در آن تاریخ خاص، ساعتی 2500 بوده است.
بنابراین در جداول Temporal، تنها «تغییرات افزودنی» مجاز هستند و این تغییرات همواره به عنوان آخرین رکورد جدول ثبت میشوند. به این ترتیب میتوان اصطلاحا «مابه التفاوت» حقوق پرداخت نشده را به شخص خاصی، محاسبه و پرداخت کرد (میدانیم در یک بازه زمانی خاص به او چقد حقوق دادهایم. همچنین میدانیم که این بازه در یک record_date دیگر لغو و با عددی دیگر، جایگزین شدهاست).
برای مطالعه بیشتر
Bitemporal Database Table Design - The Basics
Temporal Data Techniques in SQL
Database Design: A Point in Time Architecture
Temporal database
Temporal Patterns
راه حلی دیگر؛ استفاده از بانکهای اطلاعاتی NoSQL
بانکهای اطلاعاتی NoSQL برخلاف بانکهای اطلاعاتی رابطهای برای اعمال Read بهینه سازی میشوند و نه برای Write. در چند دهه قبل که بانکهای اطلاعاتی رابطهای پدیدار شدند، یک سخت دیسک 10 مگابایتی حدود 4000 دلار قیمت داشته است. به همین جهت مباحث نرمال سازی اطلاعات و ذخیره نکردن اطلاعات تکراری تا این حد در این نوع بانکهای اطلاعاتی مهم بوده است. اما در بانکهای اطلاعاتی NoSQL امروزی، اگر قرار است فیش حقوقی شخصی ثبت شود، میتوان کل اطلاعات جاری او را یکجا داخل یک سند ثبت کرد (از اطلاعات شخص در آن تاریخ تا اطلاعات تمام اجزای فیش حقوقی در قالب یک شیء تو در توی JSON). به همین جهت بسیار سریع هستند برای اعمال Read و گزارشگیری. همچنین این نوع سیستمها برای نگهداری نگارشهای مختلف یک سند بهینه سازی شدهاند و جزو ساختار توکار آنها است. بنابراین در این نوع سیستمها اگر نیاز است از یک سند خاصی گزارش بگیریم، دقیقا اطلاعات همان تاریخ خاص را دارا است و اگر اطلاعات پایه سیستم را به روز کنیم، از امروز به بعد در سندهای جدید ثبت خواهد شد. این نوع سیستمها رابطهای نیستند و بسیاری از مباحث نرمال سازی اطلاعات در آنها ضرورتی ندارد. قرار است یک فیش حقوقی شخص را نمایش دهیم؟ خوب، چرا تمام اطلاعات مورد نیاز او را در قالب یک شیء JSON تو در توی حاضر و آماده نداشته باشیم؟
- جاوا اسکریپت توانایی خواندن، نوشتن، کپی و اجرای فایلها را بر روی هارد دیسک ندارد و همچنین دسترسی مستقیمی به سیستم عامل ندارد.
- مرورگرهای مدرن اجازه کار با فایلها را به جاوا اسکریپت میدهند؛ بشرطی که آن فایل از طریق input درون صفحه وب و با اجازه کاربر انتخاب شود و فقط محدود به دسترسی به همان فایل است.
- وقتی یک تَب جدید را در مرورگر باز میکنید، جاوااسکریپتِ این دو تَب (دو صفحهی جداگانه) هیچ دسترسی و اطلاعاتی بهم ندارند.
- جاوااسکریپت به کوکیها دسترسی دارد (کوکی، فایلهای متنی کوچکی برای ذخیره اطلاعات کاربر از یک سایت است؛ مانند نام کاربری و کلمه عبور). از کوکیها برای ذخیرهی توکن نشستها استفاده میشود. لذا اگه هکر بتواند کوکی نشست یک کاربر را بدست بیاورد، میتواند از هویت او استفاده کند و خود را بجای کاربر مورد نظر قرار دهد.
- جاوااسکریپت امکان تغییرات در DOM مرورگر را دارد (داخل صفحهای که جاوااسکریپت در آن اجرا شده است).
- جاوا اسکریپت با استفاده از شیء XMLHttpRequest میتواند درخواستهای HTTP را با محتوای دلخواه، به مقصد مورد نظر ارسال نماید.
- در مرورگرهای مدرن امروزی امکان دسترسی به دوربین، میکروفن، GPS وفایلهای خاصی از طریق APIهای HTML5 امکانپذیر هست.
انواع روشهای XSS
- روش بازتابی Reflected XSS Attack
- روش ذخیره شده Stored XSS Attack
- روش مبتنی بر DOM-based XSS
روش بازتابی Reflected XSS Attack
CoffeeScript #12
بخشهای بد
جاوااسکریپت یک زبان پیچیده است که شما برای کار با آن، نیاز است قسمتهایی را که باید از آنها دوری کنید و قسمتهای مهمی را که باید استفاده کنید، بشناسید. همانطور که Sun Tzu گفته "دشمن خود را بشناس"، ما نیز در این قسمت میخواهیم برای شناخت بیشتر قسمتهای تاریک و روشن جاوااسکریپت به آن بپردازیم.
همانطور که در قسمتهای قبل گفته شد، CoffeeScript تنها به یک syntax محدود نمیشود و توانایی برطرف کردن برخی از مشکلات جاوااسکریپت را نیز دارد. با این حال، با توجه به این واقعیت که کدهای CoffeeScript به صورت مستقیم به جاوااسکریپت تبدیل میشوند و نمیتوانند تمامی مشکلاتی را که در جاوااسکریپت وجود دارند، حل کنند، پس برخی از مسائل وجود دارند که شما باید از آنها آگاهی داشته باشید.
اول از قسمتهایی که توسط CoffeeScript حل شدهاند شروع میکنیم.
A JavaScript Subset
with یک دستور بسیار زمانبر است و مضر شناخته شده است و نباید از آن استفاده کنید. with با ایجاد یک ساختار خلاصه نویسی، برای جستجو بر روی خصوصیات اشیاء در نظر گرفته شده بود. برای نمونه به جای نوشتن:
dataObj.users.vahid.email = "info@vmt.ir";
with(dataObj.users.vahid) { email = "info@vmt.ir"; }
همه چیز برای عدم استفاده از with در نظر گرفته شده است. CoffeeScript یک قدم جلوتر از همه برداشته و with را از syntax خود حذف کرده است. به عبارت دیگر در صورتیکه شما از آن استفاده کنید، کامپایلر CoffeeScript خطا صادر میکند.
Global variables
به طور پیش فرض تمامی برنامههای جاوااسکریپت در دامنه global اجرا میشوند و تمامی متغیرهایی که ساخته میشوند به طور پیش فرض در ناحیهی global قرار میگیرند. اگر شما بخواهید متغیری را در ناحیهی local ایجاد کنید، باید از کلمه کلیدی var استفاده کنید.
usersCount = 1; // Global var groupsCount = 2; // Global (function(){ pagesCount = 3; // Global var postsCount = 4; // Local })()
خوشبختانه CoffeeScript به کمک شما میآید و به طور کامل انتساب متغیرهای global را به طور ضمنی از بین میبرد. به عبارت دیگر کلمه کلیدی var در CoffeeScript رزرو شده است و در صورت استفاده خطا صادر میشود.
به صورت پیش فرض به طور ضمنی متغیرها local ایجاد میشوند و خیلی سخت میشود متغیر global ایی را بدون انتساب آن به عنوان خصوصیتی از شیء window ایجاد کرد.
outerScope = true do -> innerScope = true
var outerScope; outerScope = true; (function() { var innerScope; return innerScope = true; })();
package = require('./package') class Test build: -> # Overwrites outer variable! package = @testPackage.compile() testPackage: -> package.create()
class window.Asset constructor: ->
Semicolons
جاوااسکریپت اجباری برای نوشتن ";" ندارد، بنابراین ممکن است یک سری از دستورات از قلم بیافتند. با این حال در پشت صحنهی کامپایلر جاوااسکریپت به ";" احتیاج دارد. به طوری که parser جاوااسکریپت به صورت خودکار هر زمانی که نتواند ارزیابی از دستورات داشته باشد، یک بار دیگر با ";" این کار را انجام میدهد و درصورت موفقیت، پیام خطایی مبنی بر نبود ";" را صادر میکند.متاسفانه این یک ایده بد است. چرا که ممکن است تغییر رفتاری در کد نوشته شده به وجود آید. به مثال زیر توجه کنید. به نظر کد نوشته شده صحیح است؛ درسته؟
function() {} (window.options || {}).property
function() {}(window.options || {}).property