شروع به کار با NBench
برای شروع به کار با NBench، ابتدا نیاز است دو بستهی نیوگت ذیل را نصب کرد:
PM> Install-Package NBench PM> Install-Package NBench.Runner
[PerfBenchmark(RunMode = RunMode.Iterations, TestMode = TestMode.Measurement)] [MemoryMeasurement(MemoryMetric.TotalBytesAllocated)] public void AddMemoryMeasurement() { const int numberOfAdds = 1000000; var dictionary = new Dictionary<int, int>(); for (var i = 0; i < numberOfAdds; i++) { dictionary.Add(i, i); } } [PerfBenchmark(RunMode = RunMode.Iterations, TestMode = TestMode.Measurement)] [GcMeasurement(GcMetric.TotalCollections, GcGeneration.AllGc)] public void MeasureGarbageCollections() { var dataCache = new List<int[]>(); for (var i = 0; i < 500; i++) { for (var j = 0; j < 10000; j++) { var data = new int[100]; dataCache.Add(data.ToArray()); } dataCache.Clear(); } }
سپس هر متد تست به ویژگی PerfBenchmark مزین میشود. در اینجا RunMode.Iterations به این معنا است که خودمان قصد داریم در طی یک حلقه، تعداد بار انجام را مشخص کنیم.
ویژگی MemoryMeasurement برای اندازه گیری حافظهی مصرفی یک قطعه کد و GcMeasurement برای اندازه گیری فشار بر روی Garbage collector بکار میرود.
اجرای آزمونهای NBench
پس از تهیهی دو متد فوق، به پوشهی packages\NBench.Runner.0.3.4\lib\net45 مراجعه کنید. یک فایل exe در آن موجود است که کار یافتن و اجرای آزمونهای NBench را انجام میدهد. به عنوان پارامتر آن تنها کافی است مسیر اسمبلی برنامه (فایل exe و یا dll) را به آن ارسال کنیم:
D:\Prog\NBenchSample\packages\NBench.Runner.0.3.4\lib\net45\NBench.Runner.exe "D:\Prog\NBenchSample\NBenchSample\bin\Release\NBenchSample.exe"
--------------- RESULTS: NBenchSample.Program+AddMemoryMeasurement --------------- TotalBytesAllocated: Max: 47,842,944.00 bytes, Average: 42,002,757.60 bytes, Min: 41,353,848.00 bytes, StdDev: 2,052,032.33 bytes TotalBytesAllocated: Max / s: 359,074,078.19 bytes, Average / s: 311,474,786.96 bytes, Min / s: 300,926,928.79 bytes, StdDev / s: 16,869,581.62 bytes --------------- RESULTS: NBenchSample.Program+MeasureGarbageCollections --------------- TotalCollections [Gen0]: Max: 708.00 collections, Average: 702.80 collections, Min: 697.00 collections, StdDev: 3.65 collections TotalCollections [Gen0]: Max / s: 111.55 collections, Average / s: 109.87 collections, Min / s: 107.88 collections, StdDev / s: 1.28 collections TotalCollections [Gen1]: Max: 338.00 collections, Average: 334.60 collections, Min: 330.00 collections, StdDev: 2.41 collections TotalCollections [Gen1]: Max / s: 53.61 collections, Average / s: 52.31 collections, Min / s: 51.10 collections, StdDev / s: 0.70 collections TotalCollections [Gen2]: Max: 32.00 collections, Average: 24.80 collections, Min: 18.00 collections, StdDev: 4.73 collections TotalCollections [Gen2]: Max / s: 4.91 collections, Average / s: 3.87 collections, Min / s: 2.86 collections, StdDev / s: 0.72 collections
نکتهای در مورد اندازه گیری فشار حافظه
حافظه توسط سیستم عامل، به صورت صفحات تخصیص داده میشود. برای مثال اگر شما به 12 بایت نیاز داشته باشید، سیستم عامل ممکن است 8 کیلوبایت را جهت کاهش تعداد بار تخصیصهای حافظه و بالا بردن سرعت کار، در اختیار برنامه قرار دهد. بنابراین جهت رسیدن به بهترین نتیجه، در اینجا بهتر است تعداد زیادی شیء را مورد آزمایش قرار داد. برای مثال در آزمایش فوق بجای افزودن یک آیتم به دیکشنری، افزودن میلیونها شیء، نویز استراتژی تخصیص حافظهی توسط سیستم عامل را به حداقل میرساند.
شبیه به همین استراتژی، در پیاده سازی Dictionary نیز بکارگرفته شدهاست:
[PerfBenchmark(RunMode = RunMode.Iterations, TestMode = TestMode.Measurement)] [MemoryMeasurement(MemoryMetric.TotalBytesAllocated)] public void AddMemoryMeasurement_With_initial_Size() { const int numberOfAdds = 1000000; var dictionary = new Dictionary<int, int>(numberOfAdds); for (var i = 0; i < numberOfAdds; i++) { dictionary.Add(i, i); } }
--------------- RESULTS: NBenchSample.Program+AddMemoryMeasurement_With_initial_Size --------------- TotalBytesAllocated: Max: 23,245,912.00 bytes, Average: 23,245,912.00 bytes, Min: 23,245,912.00 bytes, StdDev: 0.00 bytes TotalBytesAllocated: Max / s: 394,032,435.34 bytes, Average / s: 389,108,363.43 bytes, Min / s: 378,502,981.34 bytes, StdDev / s: 5,575,519.09 bytes
علت اینجا است که دیکشنری در پشت صحنه، از یک متد ReSize استفاده میکند که شبیه به سیستم عامل، بیشتر از مقدار مورد نیاز جهت ذخیرهی اشیاء، برای کاهش تعداد بار تخصیصهای حافظه، حافظه به خود اختصاص میدهد. به همین جهت زمانیکه اندازهی اولیه را مشخص کردهایم، کار تخصیص حافظهی بیش از اندازهی این شیء، به شدت کاهش یافتهاست.
بررسی متد MeasureGarbageCollections
در متد MeasureGarbageCollections، مقدار زیادی شیء بر روی heap ایجاد شده و GC را وادار به عکس العمل شدید میکند.
حلقهی داخلی ایجاد شده نیز تعداد زیادی شیء را در جهت پاکسازی GC تخصیص میدهد. این پاکسازی در مرحلهای به نام generation 0 صورت میگیرد.
اشیاء اضافه شدهی به لیست، طول عمر بیشتری دارند (تا پایان حلقه). بنابراین از garbage collection at generation 0 جان سالم به در خواهند برد و در garbage collection at generation 1 به عمر آنها پایان داده خواهد شد. هرچند ممکن است تعدادی از آنها پاکسازی نشده و تا پایان full garbage collection (generation 2) باقی بمانند.
در آزمایش انجام شده، با ذکر GcGeneration.AllGc، هر سه مورد Gen0 تا Gen2 اندازه گیری خواهند شد. عموما اندازه گیری Gen0 و Gen1 مهم نیستند و اینها خیلی زود به پایان خواهند رسید. اگر تعداد بار رخدادن Gen2 زیاد بود (یا اصلا وجود داشت)، میتواند سبب بروز مشکلات کارآیی شدیدی گردد.
بنابراین میتوان بجای تنظیم GcGeneration.AllGc، صرفا از GcGeneration.Gen2 استفاده کرد.
اندازهگیری Throughput یا تعداد بار اجرای یک متد در ثانیه
روش دیگر کار با فریم ورک NBench، ایجاد یک کلاس مخصوص و سپس افزودن متدهای Setup مزین به PerfSetup، متد Cleanup مزین به PerfCleanup و سپس تعدادی متد اندازه گیری کارآیی توسط ویژگی PerfBenchmark است. در اینجا برای اندازهگیری سرعت اجرای متدها، از ویژگی CounterThroughputAssertion استفاده خواهد شد که پارامتر اول آن نام یک شمارشگر است. این شمارشگر در متد Setup ایجاد میشود (با یک نام دلخواه).
public class DictionaryThroughputTests { private readonly Dictionary<int, int> _dictionary = new Dictionary<int, int>(); private const string AddCounterName = "AddCounter"; private Counter _addCounter; private int _key; private const int AverageOperationsPerSecond = 20000000; [PerfSetup] public void Setup(BenchmarkContext context) { _addCounter = context.GetCounter(AddCounterName); _key = 0; } [PerfBenchmark(RunMode = RunMode.Throughput, TestMode = TestMode.Test)] [CounterThroughputAssertion(AddCounterName, MustBe.GreaterThan, AverageOperationsPerSecond)] public void AddThroughput_ThroughputMode(BenchmarkContext context) { _dictionary.Add(_key++, _key); _addCounter.Increment(); } [PerfBenchmark(RunMode = RunMode.Iterations, TestMode = TestMode.Test)] [CounterThroughputAssertion(AddCounterName, MustBe.GreaterThan, AverageOperationsPerSecond)] public void AddThroughput_IterationsMode(BenchmarkContext context) { for (var i = 0; i < AverageOperationsPerSecond; i++) { _dictionary.Add(i, i); _addCounter.Increment(); } } [PerfCleanup] public void Cleanup(BenchmarkContext context) { _dictionary.Clear(); } }
--------------- RESULTS: NBenchSample.DictionaryThroughputTests+AddThroughput_ThroughputMode --------------- [Counter] AddCounter: Max: 575,654.00 operations, Average: 575,654.00 operations, Min: 575,654.00 operations, StdDev: 0.00 operations [Counter] AddCounter: Max / s: 7,205,997.59 operations, Average / s: 7,163,894.30 operations, Min / s: 7,075,316.79 operations, StdDev / s: 42,518.20 operations --------------- RESULTS: NBenchSample.DictionaryThroughputTests+AddThroughput_IterationsMode --------------- [Counter] AddCounter: Max: 20,000,000.00 operations, Average: 20,000,000.00 operations, Min: 20,000,000.00 operations, StdDev: 0.00 operations [Counter] AddCounter: Max / s: 7,409,380.61 operations, Average / s: 7,250,991.24 operations, Min / s: 6,880,938.73 operations, StdDev / s: 148,085.19 operations
در اینجا برای گزارش دادن، عددهای Average و Average / s باید مورد استفاده قرار گیرند.
ایجاد یک پروژه با استفاده Razor
در ادامه با هم یک مثال را با استفاده از Razor ایجاد میکنیم. یک پروژه جدید را با قالب Empty و با نام Razor ایجاد میکنیم.
مراحل:
1- ابتدا در کلاس startup قابلیت MVC را فعال میکنیم؛ با قرار دادن کد زیر در متد ConfigureServices:
services.AddMvc();
app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); });
namespace Razor { public class Startup { // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { services.AddMvc(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } //app.Run(async (context) => //{ // await context.Response.WriteAsync("Hello World!"); //}); } } }
ایجاد یک Model
یک پوشه جدید را به نام Models ایجاد و بعد در این پوشه یک کلاس را به نام Product ایجاد میکنیم و کدهای زیر را در آن قرار میدهیم:
namespace Razor.Models { public class Product { public int ProductID { get; set; } public string Name { get; set; } public string Description { get; set; } public decimal Price { get; set; } public string Category { set; get; } } }
ایجاد Controller
تنظیمات پیشفرض را در فایل Startup انجام دادهایم. درخواستهایی را که توسط کاربر ارسال میشوند، به controller پیشفرضی که نامش در اینجا Home است، ارسال میکند. حالا ما یک پوشه جدید را به نام Controllers ایجاد میکنیم و در آن یک کنترلر جدید را به نام HomeController ایجاد میکنیم و کدهای زیر را در آن قرار میدهیم:
namespace Razor.Controllers { public class HomeController : Controller { // GET: /<controller>/ public ViewResult Index() { Product myProduct = new Product { ProductID = 1, Name = "Kayak", Description = "A boat for one person", Category = "Watersports", Price = 275M }; return View(myProduct); } } }
ایجاد View
برای ایجاد یک View پیشفرض برای Action Method فوق در پوشه Views/Home یک MVC View Page (Razor View Page) را به نام Index.schtml ایجاد میکنیم.
- نکته1: پوشه View و داخل آن Home را ایجاد کنید.
- نکته2: معادل MVC View Page در نسخه جدید، Razor View میباشد. اگر در لیست این آیتم را انتخاب کنید، در توضیحات پنل سمت راست میتوانید این مطلب را مشاهده کنید.
- نکته3: دقت نمایید برای اینکه پروژه net Core2. باشد و تمام مشخصات موردنظر را داشته باشد، باید نگارش ویژوال استودیو VS 2017.15.6.6 و یا بیشتر باشد.
@model Razor.Models.Product @{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width"/> <title>Index</title> </head> <body> Content will go here </body> </html>
تا اینجا ما یک پروژه ساده را ایجاد نمودهایم که قابلیت استفادهی از Razor را هم دارد. در ادامه نحوهی استفاده از امکانات Razor شرح داده میشوند.
استفاده از Model در یک View
برای استفاده از شیء مدل در View، باید در View به آن شیء و مشخصات آن دسترسی داشته باشیم که این دسترسی را Razor با استفاده از کاراکتر @ برای ما ایجاد میکند. برای اتصال به Model از عبارت model@ (حتما باید حروف کوچک باشد) استفاده میکنیم و برای دسترسی به مشخصات مدل از عبارت Model@ (حتما باید حرف اول آن بزرگ باشد) استفاده میکنیم. به کد زیر دقت کنید:
@model Razor.Models.Product @{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width"/> <title>Index</title> </head> <body> @Model.Name </body> </html>
نتیجه خروجی بالا مانند زیر میباشد:
معرفی View Imports
زمانیکه بخواهیم به یک کلاس در View دسترسی داشته باشیم، باید فضای نام آن کلاس را مانند کد زیر در بالای View اضافه کنیم. حالا اگر بخواهیم به چند کلاس دسترسی داشته باشیم، باید این کار را به ازای هر کلاس در هر View انجام دهیم که سبب ایجاد کدهای اضافی در Viewها میشود. برای بهبود این وضعیت میتوانید یک کلاس View Import را در پوشهی Views ایجاد کنید و تمام فضاهای نام را در آن قرار دهید. با اینکار تمام فضاهای نامی که در این کلاس View Import قرار گرفتهاند، در تمام Viewهای موجود در پوشه Views قابل دسترسی خواهند بود.
در پوشه View راست کلیک کرده و گزینه Add و بعد New Item را انتخاب میکنیم و در کادر باز شده، آیتم MVC View Import Page (در نسخه جدید نام آن Razor View Imports است) انتخاب میکنیم. ویژوال استودیو به صورت پیش فرض نام ViewImports.cshtml_ را برای آن قرار میدهد.
نکته: استاندارد نام گذاری این View این میباشد که ابتدای آن کاراکتر (_) حتما وجود داشته باشد.
در کلاس تعریف شده با استفاده از عبارت using@ فضای نامهای خود را قرار میدهیم؛ مانند زیر:
@using Razor.Models
@model Product @{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width"/> <title>Index</title> </head> <body> @Model.Name </body> </html>
Layout ها
یکی دیگر از عبارتهای مهم Razor که در فایل Index وجود دارد، عبارت زیر است:
@{ Layout = null; }
از Layout برای طراحی الگوی Viewها استفاده میکنیم. اگر بخواهیم برای View ها یک قالب طراحی کنیم و این الگو بین تمام یا چندتای از آنها مشترک باشد، کدهای مربوط به الگو را با استفاده از Layout ایجاد میکنیم و از آن در View ها استفاده میکنیم. اینکار برای جلوگیری از درج کدهای تکراری قالب در برنامه انجام میشود. با اینکار اگر بخواهیم در الگو تغییری را انجام دهیم، این تغییر را در یک قسمت انجام میدهم و سپس به تمام Viewها اعمال میشود.
Layout
طرحبندی Viewهای برنامه بطور معمول بین چند View مشترک است و طبق استاندارد ویژوال استودیو در پوشهی Views/Shared قرار میگیرد. برای ایجاد Layout، روی پوشه Views/shared راست کلیک کرده و بعد گزینه Add وبعد NewItem و سپس گزینه MVC View Layout Page (نام آن در نسخه جدید Razor Layout است) را انتخاب میکنیم و ابتدای نام آن را به صورت پیشفرض کاراکتر (_) قرار میدهیم.
هنگام ایجاد این فایل توسط ویژوال استودیو، کدهای زیر به صورت پیش فرض در فایل ایجاد شده وجود دارند:
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> </head> <body> <div> @RenderBody() </div> </body> </html>
ViewBag ویژگی مفیدی است که اجازه میدهد تا مقادیر و دادهها در برنامه گردش داشته باشند و در این مورد بین یک View و Layout منتقل شوند. در ادامه خواهید دید وقتی Layout را به یک نمایه اعمال میکنیم، این مورد چگونه کار میکند.
عناصر HTML در یک Layout به هر View که از آن استفاده میکند، اعمال و توسط آن یک الگو برای تعریف محتوای معمولی ارائه میشود؛ مانند کدهای زیر. من برخی از نشانه گذاریهای ساده را به Layout اضافه کردم تا اثر قالب آن آشکارتر شود:
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> <style> #mainDiv { padding: 20px; border: solid medium black; font-size: 20pt } </style> </head> <body> <h1>Product Information</h1> <div id="mainDiv"> @RenderBody() </div> </body> </html>
اعمال Layout
برای اعمال کردن Layout به یک View، نیاز است مشخصه Layout آنرا مقدار دهی و سپس Htmlهای اضافی موجود در آنرا مانند المنتهای head و Body حذف کنید؛ همانند کدهای زیر:
@model Product @{ Layout = "_BasicLayout"; ViewBag.Title = "Product"; }
در اینجا عبارت ViewBag.Title را نیز مقدار دهی میکنیم. زمانیکه فایل فراخوانی میشود، عنوان آن صفحه با این مقدار، جایگزین خواهد شد.
تغییرات این View بسیار چشمگیر است؛ حتی برای چنین برنامه سادهای. طرحبندی شامل تمام ساختار مورد نیاز برای هر پاسخ HTML است که View را به صورت یک محتوای پویا ارائه میدهد و دادهها را به کاربر منتقل میکند. هنگامیکه MVC فایل Index.cshtmal را پردازش میکند، این طرحبندی برای ایجاد پاسخ HTML نهایی یکپارچه میشود؛ مانند عکس زیر:
View Start
بعضی موارد هنوز در برنامه وجود دارند که میتوان کنترل بیشتری بر روی آنها داشته باشید. مثلا اگر بخواهیم نام یک فایل layout را تغییر دهیم، مجبور هستیم تمام Viewهایی را که از آن Layout استفاده میکنند، پیدا کنید و نام Layout استفاده شده در آنها را تغییر دهیم. اینکار احتمال خطای بالایی دارد و امکان دارد بعضی View ها از قلم بیفتند و برنامه دچار خطا شود. بنابراین با استفاده از View Start میتوانیم این مشکل را برطرف کنیم. وقتی نام Layout تغییر کرد، تنها کافی است نام آنرا در View Start تغییر دهیم. اکنون زمانیکه برنامه را اجرا میکنیم، MVC به دنبال فایل View Start میگردد و اگر اطلاعاتی داشته باشد، آن را اجرا میکند و الویت این فایل از تمام فایلهای دیگر بیشتر است و ابتدا تمام آنها اجرا میشوند.
برای ایجاد یک فایل شروع مشاهده، روی پوشهی Views کلیک راست کرده و گزینه add->New Items را انتخاب میکنیم و از پنجره باز شده گزینه ( Razor View Start ) Mvc View Start Page را انتخاب میکنیم؛ مانند تصویر زیر:
ویژوال استودیو به صورت پیش فرض نام ViewStart.cshtml_ را به عنوان نام آن قرار میدهد؛ شما گزینهی Create را در این حالت انتخاب کنید. محتویات فایل ایجاد شده به صورت زیر میباشد:
@{ Layout = "_Layout"; }
@{ Layout = "_BasicLayout"; }
@model Product @{ ViewBag.Title = "Product"; }
شما همچنین میتوانید چندین فایل View Start را برای تنظیم مقادیر پیش فرض قسمتهای مختلف برنامه، استفاده کنید. یک فایل Razor همواره توسط نزدیکترین فایل View start، پردازش میشود. به این معنا که شما میتوانید تنظیمات پیش فرض را با افزودن یک فایل View Start به پوشه Views / Home و یا Views / Shared لغو کنید.
نکته: درک تفاوت میان حذف محتویات فایل View Start یا مساوی Null قرار دادن آن مهم است. اگر View شما مستقل است و شما نمیخواهید از آن استفاده کنید، بنابراین مقدار Layout آنرا صریحا برابر Null قرار دهید. اگر مقدار دهی صریح شما مشخصه Layout را نادیده بگیرید، Mvc فرض میکند که میخواهید layout را داشته باشید و مقدار آن را از فایل View Start تامین میکند.
استفاده از عبارتهای شرطی در Razor
حالا که من اصول و مبانی View و Layout را به شما نشان دادم، قصد دارم به انواع مختلفی از اصطلاحات که Razor آنها را پشتیبانی میکند و نحوه استفادهی از آنها را برای ایجاد محتوای نمایشی، ارائه دهم. در یک برنامه MVC، بین نقشهایی که توسط View و Action متدها انجام میشود، جدایی روشنی وجود دارد. در اینجا قوانین سادهای وجود دارند که در جدول زیر مشخص شدهاند:
کامپوننت |
انجام میشود |
انجام نمیشود |
Action Method |
یک شیء ViewModel را به View ارسال میکند. |
یک فرمت داده را به View ارسال میکند. |
View |
از شیء ViewModel برای ارائه محتوا به کاربر استفاده میکند. |
هر جنبهای از شیء View Model مشخصات را تغییر میدهد. |
برای به دست آوردن بهترین نتیجه از MVC، نیاز به تفکیک و جداسازی بین قسمتهای مختلف برنامه را دارید. همانطور که میبینید، میتوانید کاملا با Razor کار کنید و این نوع فایلها شامل دستورالعملهای سی شارپ نیز هستند. اما شما نباید از Razor برای انجام منطق کسب و کار استفاده کنید و یا هر گونه اشیاء Domain Model خود را دستکاری کنید. کد زیر نشان میدهد که یک عبارت جدید به View اضافه میشود:
*@ @model Product @{ ViewBag.Title = "Product"; } <p>Product Name: @Model.Name</p> <p>Product Price: @($"{Model.Price:C2}")</p>
پردازش دادهها در مقابل فرمت
تفاوت بین پردازش داده و قالب بندی داده مهم است.
- نمایش فرمت دادهها: به همین دلیل در آموزش قبل من یک نمونه از شیء کلاس Product را برای View ارسال کردهام و نه فرمت خاص یک شیء را به صورت یک رشته نمایشی.
- پردازش داده: انتخاب اشیاء دادهای برای نمایش، مسئولیت کنترلر است و در این حالت مدلی را برای دریافت و تغییر داده مورد نیاز، فراخوانی میکند.
گاهی سخت است که متوجه شویم کدی جهت پردازش داده است و یا فرمت آن.
اضافه نمودن مقدار داده ای
سادهترین کاری را که میتوانید با یک عبارت Razor انجام دهید این است که یک مقدار داده را در نمایش دهید. رایجترین کار برای انجام آن، استفاده از عبارت Model@ است. ویوو Index یک مثال از این مورد است؛ شبیه به این مورد:
<p>Product Name: @Model.Name</p>
using Microsoft.AspNetCore.Mvc; using Razor.Models; namespace Razor.Controllers { public class HomeController : Controller { // GET: /<controller>/ public ViewResult Index() { Product myProduct = new Product { ProductID = 1, Name = "Kayak", Description = "A boat for one person", Category = "Watersports", Price = 275M }; return View(myProduct); } } }
خصوصیت ViewBag یک شیء پویا را باز میگرداند که میتواند برای تعیین خواص دلخواهی مورد استفاده قرار گیرد. از آنجا که ویژگی ViewBag پویا است، لازم نیست که نام خصوصیات را پیش از آن اعلام کنم. اما این بدان معنا است که ویژوال استودیو قادر به ارائه پیشنهادهای تکمیل کننده برای ViewBag نیست.
در مثال زیر از یک مدل نوع دار و مزایای به همراه آن استفاده شدهاست:
<p>Product Name: @Model.Name</p> <p>Product Price: @($"{Model.Price:C2}")</p> <p>Stock Level: @ViewBag.StockLevel</p>
تنظیم مقادیر مشخص
شما همچنین میتوانید از عبارات Razor برای تعیین مقدار عناصر، استفاده کنید:
@model Product @{ ViewBag.Title = "Product"; } p>Product Name: @Model.Name</p> <p>Product Price: @($"{Model.Price:C2}")</p> <p>Stock Level: @ViewBag.StockLevel</p> <div data-productid="@Model.ProductID" data-stocklevel="@ViewBag.StockLevel"> <p>Product Name: @Model.Name</p> <p>Product Price: @($"{Model.Price:C2}")</p> <p>Stock Level: @ViewBag.StockLevel</p> </div>
نکته: ویژگیهای دادهها که نام آنها *-data است، روشی برای ایجاد ویژگیهای سفارشی برای سالها بوده است و بعنوان بخشی از استاندارد HTML5 است. عموما کدهای جاوا اسکریپت از آنها برای یافتن اطلاعات استفاده میکنند.
اگر برنامه را اجرا کنید و به منبع HTML که به مرورگر فرستاده شده نگاهی بیندازید، خواهید دید که Razor مقادیر صفات را تعیین کرده است؛ مانند این:
<div data-productid="1" data-stocklevel="2"> <p>Product Name: Kayak</p> <p>Product Price: £275.00</p> <p>Stock Level: 2</p> </div>
استفاده از عبارتهای شرطی
Razor قادر به پردازش عبارات شرطی است. در ادامه کدهای Index View را که در آن دستورات شرطی اضافه شدهاند میبینید:
@model Product @{ ViewBag.Title = "Product Name"; } <div data-productid="@Model.ProductID" data-stocklevel="@ViewBag.StockLevel"> <p>Product Name: @Model.Name</p> <p>Product Price: @($"{Model.Price:C2}")</p> <p>Stock Level: @switch (ViewBag.StockLevel) { case 0:@:Out of Stock break; case 1: case 2: case 3: <b>Low Stock (@ViewBag.StockLevel)</b> break; default: @: @ViewBag.StockLevel in Stock break; } </p> </div>
برای شروع یک عبارت شرطی، یک علامت @ را در مقابل کلمه کلیدی if یا swicth سی شارپ قرار دهید. سپس بخش کد را داخل } قرار میدهیم. درون قطعه کد Razor، میتوانید عناصر HTML و مقادیر داده را در خروجی نمایش دهید؛ مانند:
<b>Low Stock (@ViewBag.StockLevel)</b>
با این حال، اگر میخواهید متن واقعی را در نظر بگیرید و دستورات Razor را لغو کنید،میتوانید از :@ استفاده کنید تا عین آن عبارت درج شود.
همانطور که در قسمت اول گفته شد، اجزای رابط کاربری (تگهای HTML) در کتابخانهی React به عنوان کامپوننتها (مؤلفههای جزء) شناخته میشوند. React تگها را به عنوان اجزایی مستقل و با وضعیتی مشخص در حافظه میشناسد. دلایل ارزشمند بودن این روش در ادامه بررسی میشود.
خوانایی بهتر (Readability)
React میتواند تگهای یگانه یا مخلوطی از تگهای به هم مرتبط را در پس زمینه ساخته و با یک نام واحد (کامپوننت) به HTML DOM ارسال کند. یعنی اگر جایی یک کامپوننت صدا زده شود، تگ یا تگهای مرتبط به آن کامپوننت را به عنوان خروجی خواهیم داشت. همانطور که میشود تگهای مختلف را به صورت تو در تو استفاده کرد، کامپوننتها را هم میشود به همین روش فراخوانی کرد. در مثال زیر روش صدا زدن چند کامپوننت و تگهایی را که ارائه میدهد، داریم.
// Components in a JavaScript file. <clickableImage href="http://google.com" src="google.png" /> <LinksContainer> <LinksList> <clickableImage href="http://yahoo.com" src="yahoo.png" /> </LinksList> </LinksContainer> <!--Output in HTML DOM--> <a href="http://google.com"> <img src="google.png" /> </a> <div> <div> <ul> <li> <a href="http://google.com"> <img src="google.png" /> </a> </li> </ul> </div> </div>
در قسمت کامپوننتها میبینیم که چطور کامپوننتها یکبار به صورت تکی و یک بار به صورت تو در تو اجرا میشوند. خروجی در قسمت Output واضح است که با نام کامپوننتها هماهنگی دارد. با این مثال چند مورد مشخص میشود.
- به هر کامپوننت قبلا گفته شده چه تگهایی را باید ایجاد کند. در نتیجه با هر بار فراخوانی در هر مکان، تگ یا تگهایی که به آن معرفی شده را میسازد.
- هر کامپوننت میتواند مقادیری را به عنوان ورودی دریافت کند و آنها را به تگها در خروجی اعمال کند. در مثال بالا href و src در فراخوانیهای مختلف، مقادیر متفاوتی را به خروجی میفرستند.
- با انتخاب نام مناسب برای کامپوننتها، بدون آنکه بدانیم چطور ساخته شدهاند میتوانیم حدس بزنیم چه تگهایی را خواهند ساخت و این دلیلی است که خوانایی برنامه افزایش میابد.
- دلیل دیگر که باعث خوانایی برنامه میشود، این است که هر یک از این کامپوننتها میتوانند تگهای زیادی را یک جا بسازند که این کار منجر به کم شدن مقدار کد برنامه میشود. برنامه هر چه کم کدتر، با خوانایی بیشتر!
قابلیت استفاده مجدد
در ادامه وقتی با روش ساخت کامپوننتها آشنا شدیم، متوجه میشویم که کامپوننتها چیزی بیشتر از یک تابع نیستند. وقتی نام یک کامپوننت را فراخوانی کنیم در واقع یک تابع را اجرا میکنیم، به آن پارامتر ورودی را میدهیم و از آن خروجی میگیریم. میدانیم که توابع را میشود یکبار ساخت و چندبار استفاده کرد. بخصوص اگر این توابع به متغیرهای سراسری و سایر توابع وابسته نباشند و به صورت مستقل عمل کنند، میشود آنها را به برنامههای دیگر هم انتقال داد.
نحوه ساخت یک کامپوننت در React
در React به سه روش میشود کامپوننتها را ایجاد کرد. در روش اول توضیحات زیاد خواهند بود، اما در دو روش بعدی فقط نکات کلیدی گفته خواهد شد.
Stateless function components
میخواهیم یک منو از نوشیدنیها را با استفاده از کامپوننتها نمایش دهیم. در یک فایل جاوااسکریپت کدهای زیر را وارد کنید. در ادامه هر بخش توضیح داده خواهد شد.
var hotDrinks = [ { item: "Tea", price: "7000" }, { item: "Espresso", price: "10000" }, { item: "Hot Chocolate", price: "12000" } ]; var MenuItem = function (props) { return ( <li className="list-group-item"> <span className="badge">{props.price}</span> <p>{props.item}</p> </li> ) }; var Menu = function (props) { return ( <div className="row"> <div className="col-md-4"> <ul className="list-group"> {props.data.map(item => <MenuItem {...item} />)} </ul> </div> </div> ) }; ReactDOM.render( <Menu data={hotDrinks} />, document.getElementById("reactTestContainer") )
- فرض میکنیم که لیست نوشیدنیها و قیمت آنها را به فرمتی که میبینید از سرور دریافت کردهایم. (hotDrinks)
- شیء MenuItem یک تابع بدون نام را اجرا میکند. از دیدگاه React این تابع یک کامپوننت است. کامپوننت با هر بار فراخوانی مقادیری را برای یک نوشیدنی و قیمت آن، دریافت میکند.کامپوننت به عنوان خروجی یک تگ <li>، پر شده با مقادیر ورودی را بازگشت میدهد.
- شیء Menu یک تابع بدون نام را اجرا میکند. از دید React این تابع یک کامپوننت است. کامپوننت با هر بار فراخوانی، مجموعهای از نوشیدنیها و قیمت آنها را دریافت میکند. متد map به کمک یک Arrow Function آرایهای از کامپوننت MenuItem ایجاد میکند که به ازای هر عضو ایجاد شده، یکبار MenuItem اجرا میشود. هر عضو (item) دارای یک نام نوشیدنی و قیمت آن است. سه نقطه در {…item} برای پر کردن جای خالی نیست! این عبارت یعنی اینکه مقادیر نام و قیمت را به صورت جداگانه (یعنی دو پارامتر مجزا) به کامپوننت MenuItem ارسال میکند. کامپوننت، به عنوان خروجی یک تگ <ul>، پر شده با آرایهای از کامپوننت MenuItem را بازگشت میدهد.
- متد render از شیء ReactDOM وظیفه ساخت تگهای JSX واقع در کامپوننتها را در HTML DOM به عهده دارد. پارامتر اول render، کامپوننت Menu است با ورودی دادههای گرفته شده از سرور. همانطور که شرح داده شد، کامپوننت Menu با فراخوانی و به کمک دادههای ورودی، کامپوننت MenuItem را پیادهسازی خواهد کرد. پارامتر دوم render، محلی است که تگها باید در آن ساخته شوند. مثلا یک تگ <div>
- در هر کدام از کامپوننتها و در قسمت ReactDOM.render میشود از کامپوننتهای دیگر به صورت تو در تو استفاده کرد.
React.createClass
React یک API درونی برای ایجاد کامپوننتها، به نام createClass دارد. این تابع باید یک شیء پیکربندی درون خود داشته باشد که در آن و بین دو آکولاد {} خواص و متدها تعریف میشوند. تابع createClass برای کار حداقل باید یک متد به نام render داشته باشد که در آن تگهای JSX را قرار میدهیم. کامپوننت MenuItem را که به صورت Stateless ساختیم، دوباره با createClass ایجاد میکنیم.
var MenuItem = React.createClass({ render: function () { return ( <li className="list-group-item"> <span className="badge">{this.props.price}</span> <p>{this.props.item}</p> </li> ) } });
برای خواندن مقادیر ورودی در این روش باید از this استفاده کنیم. بر اساس قواعد شیء گراییِ، MenuItem و Menu کلاس هستند و هر بار در ReactDOM.render کامپوننت Menu را به HTML DOM ارسال میکنیم. یک نمونه از این کلاس ساخته میشود و کلاس Menu، نمونههایی از کلاس MenuItem را میسازد. this به نمونهی ساخته شده از یک کلاس اشاره دارد.
React.Component
در روش آخر با استفاده از extend، از کلاس React.Component ارث بری میکنیم و کامپوننت را میسازیم. مفاهیم کلاس و ارث بری در جاوااسکریپ را میشود از اینجا یاد گرفت. مجددا MenuItem را با این روش ایجاد میکنیم.
class MenuItem extends React.Component { render() { return ( <li className="list-group-item"> <span className="badge">{this.props.price}</span> <p>{this.props.item}</p> </li> ); } }
همانطور که میبینید بین دو روش React.Component و React.createClass تفاوتی جز در syntax آنها نیست. در اینجا از سایر امکانات کلاس در جاوااسکریپت مثل سازنده کلاس میشود استفاده کرد. کامپوننتها در React میتوانند کاری بیشتر از ساخت تگها در HTML DOM را انجام دهند. در قسمت بعد به قابلیت مهم حفظ و دنبال کردن تغییرات در وضعیت کامپوننتها میپردازیم.
الف) Key-Value stores که پایه بانکهای اطلاعاتی NoSQL را تشکیل داده و اهدافی عمومی را دنبال میکنند.
ب) Wide column stores که در شرکتهای بزرگ اینترنتی بیشتر مورد استفاده قرار گرفتهاند.
ج) Document stores یا بانکهای اطلاعاتی NoSQL سندگرا.
د) Graph databases که بیشتر برای ردیابی ارتباطات بین موجودیتها بکار میروند.
و در تمام این گروهها، مکانیزمهای Key-Value به شدت مورد استفادهاند.
الف) Key-Value stores
Key-Value stores یکی از عمومیترین و پایهایترین گروههای بانکهای اطلاعاتی NoSQL را تشکیل میدهند. البته این مورد بدین معنا نیست که این رده، جزو محبوبترینها نیز بهشمار میروند.
این نوع بانکهای اطلاعاتی شامل جداولی از اطلاعات هستند. هر جدول نیز شامل تعدادی ردیف است؛ چیزی همانند بانکهای اطلاعاتی رابطهای. اما در هر ردیف، یک Dictionary یا آرایهای از اطلاعات key-value شکل را شاهد خواهید بود. در اینجا ساختار و اسکیمای ردیفها میتوانند نسبت به یکدیگر کاملا متفاوت باشند (دید لیبرال نسبت به اسکیما، که در قسمت قبل به آن پرداخته شد). در این بین، تنها تضمین خواهد شد که هر ردیف، Id منحصربفردی دارد.
از این نوع بانکهای اطلاعاتی، در سکوهای کاری ابری زیاد استفاده میشود. دو مثال مهم در اینباره شامل Amazon SimpleDB و Azure Table Storage هستند.
سایر نمونههای مهم دیگری از بانکهای اطلاعاتی NoSQL که بر مبنای مفهوم Key-Value stores کار میکنند، عبارتند از MemcacheDB و Voldemort. به علاوه در Amazon web services بانک اطلاعاتی دیگری به نام DynamoDB به عنوان یک سرویس عمومی در دسترس است. همچنین Dynomite نیز به عنوان نمونه سورس باز Dynamo مطرح است.
Redis و Riak نیز جزو بانکهای اطلاعاتی Key-Value store بسیار معروف بهشمار میروند.
همانطور که در تصویر فوق ملاحظه میکنید، Key-Value stores دارای بانکهای اطلاعاتی شامل جداول مختلف هستند. در اینجا همچنین ساختار ردیفهایی از اطلاعات این جداول نیز مشخص شدهاند. هر ردیف، یک کلید دارد به همراه تعدادی جفت کلید-مقدار. در این جداول، اسکیما ثابت نگه داشته شده است و از ردیفی به ردیف دیگر متفاوت نیست؛ اما این مساله اختیاری است. برای مثال میتوان در ردیف اطلاعات یک مشتری خاص، کلید-مقدارهایی خاص او را نیز درج کرد که لزوما در سایر ردیفها، نیازی به وجود آنها نیست.
به علاوه باید به خاطر داشت که هرچند به ظاهر last_orderها به شماره Id سفارشات مرتبط هستند، اما مفاهیمی مانند کلیدهای خارجی بانکهای اطلاعاتی رابطهای، در اینجا وجود خارجی ندارند. بیشتر در اینجا هدف سهولت جستجوی اطلاعات است.
ب) Wide column stores
Wide column stores دارای جداولی است که درون آنها ستونهایی قابل تعریف است. درون این ستونها که یادآور بانکهای اطلاعاتی رابطهای هستند، اطلاعات به شکل key-value با ساختاری متفاوت، قابل ذخیره سازی هستند. در اینجا هر ستون، میتواند شامل گروهی از ستونها که بر اساس مفاهیم جفتهای key-value کار میکنند، باشد.
این نوع بانکهای اطلاعاتی عموما در سایتهای اینترنتی بسیار بزرگ و برنامههای «Big data» استفاده میشوند. برای مثال:
- BigTable گوگل که یک محصول اختصاصی و غیرعمومی است؛ اما جزئیات آن را به عنوان مقالات علمی منتشر کرده است.
- دنیای سورس باز به رهبری Yahoo، نمونه سورس باز BigTable را به نام Hbase ارائه داده است.
- در فیس بوک، از بانک اطلاعاتی دیگری به نام Cassandra استفاده میکنند. در اینجا به گروهی از ستونها super columns و جداول super column families گفته میشود.
در اینجا نیز جداول و ردیفها وجود دارند و هر ستون باید عضوی از خانواده یک super column باشد. ساختار ردیفها در این تصویر یکسان درنظر گرفته شدهاند، اما اگر نیاز بود، برای مثال میتوان در ردیفی خاص، ساختار را تغییر داد و مثلا middle name را نیز بر اساس نیاز، به ردیفی اضافه کرد.
ج) Document stores
Document stores بجای جداول، دارای بانکهای اطلاعاتی مختلفی هستند و در اینجا بجای ردیفها، سند یا document دارند. ساختار سندها نیز عموما بر مبنای اشیاء JSON تعریف میگردد (که البته این مورد الزامی نبوده و از هر محصول، به محصول دیگری ممکن است متفاوت باشد؛ اما عمومیت دارد). بنابراین هر سند دارای تعدادی خاصیت است (چون اشیاء JSON به این نحو تعریف میگردند) که دارای مقدار هستند. در نگاه اول، شاید این نوع اسناد، بسیار شبیه به key-value stores به نظر برسند. اما در حین تعریف اشیاء JSON، یک مقدار میتواند خود یک شیء کامل دیگر باشد و نه صرفا یک مقدار ساده. به همین جهت عدهای به این نوع بانکهای اطلاعاتی، بانکهای اطلاعاتی Key-value store سفارشی و خاص نیز میگویند.
این نوع ساختار منعطف، برای ذخیره سازی اطلاعات اشیاء تو در تو و درختی بسیار مناسب است. همچنین این اسناد میتوانند حاوی پیوستهایی نیز باشد؛ مانند پیوست یک فایل به یک سند.
در Document stores، نگارشهای قدیمی اسناد نیز نگهداری میگردند. به همین جهت این نوع بانکهای اطلاعاتی برای ایجاد برنامههای مدیریت محتوا نیز بسیار مطلوب میباشند.
با توجه به مزایایی که برای این رده از بانکهای اطلاعاتی NoSQL ذکر گردید، Document stores در بین برنامه نویسها بسیار محبوب و پرکاربرد هستند.
از این دست بانکهای اطلاعاتی NoSQL، میتوان به CouchDB ، MongoDB و RavenDB اشاره کرد.
سایر مزایای Document stores که به پرکاربرد شدن آنها کمک کردهاند به شرح زیر هستند:
- هر سند را میتوان با یک URI آدرس دهی کرد.
- برای نمونه CouchDB از یک full REST interface برای دسترسی و کار با اسناد پشتیبانی میکند (چیزی شبیه به ASP.NET WEB API در دات نت). در اینجا با استفاده از یک وب سرور توکار و بکارگیری HTTP Verbs مانند Put، Delete، Get و غیره، امکان کار با اسناد وجود دارد.
- اغلب بانکهای اطلاعاتی Document stores از JavaScript به عنوان native language خود بهره میبرند (جهت سهولت کار با اشیاء JSON).
در اینجا دو دیتابیس، بجای دو جدول وجود دارند. همچنین در مقایسه با بانکهای اطلاعاتی key-value، برای نمونه، مقدار خاصیت آدرس، خود یک شیء است که از دو خاصیت تشکیل شده است. به علاوه هر خاصیت Most_Recent یک Order، به سند دیگری در بانک اطلاعاتی Orders لینک شده است.
د) Graph databases
Graph databases نوع خاصی از بانکهای اطلاعاتی NoSQL هستند که جهت ردیابی ارتباطات بین اطلاعات طراحی شدهاند و برای برنامههای شبکههای اجتماعی بسیار مفید هستند.
در واژه نامه این بانکهای اطلاعاتی Nodes و Edges (اتصال دهندههای نودها) تعریف شدهاند. در اینجا نودها میتوانند دارای خاصیتها و مقادیر متناظر با آنها باشند.
یکی از معروفترین Graph databases مورد استفاده، Neo4j نام دارد.
در اینجا یک شخص را که دارای رابطه آدرس با شیء آدرس ذکر شده است را مشاهده میکنید. همچنین این شخص دارای رابطه دوستی با سه شخص دیگر است.
ساختار درختی
اصطلاحات درخت
در شکل بالا دایرههایی برای هر بخش از اطلاعت کشیده شده و ارتباط هر کدام از آنها از طریق یک خط برقرار شده است. اعداد داخل هر دایره تکراری نیست و همه منحصر به فرد هستند. پس وقتی از اعداد اسم ببریم متوجه میشویم که در مورد چه چیزی صحبت میکنیم.
در شکل بالا به هر یک از دایرهها یک گره Node میگویند و به هر خط ارتباط دهنده بین گرهها لبه Edge گفته میشود. گرههای 19 و 21 و 14 زیر گرههای گره 7 محسوب میشوند. گرههایی که به صورت مستقیم به زیر گرههای خودشان اشاره میکنند را گرههای والد Parent میگویند و زیرگرههای 7 را گرههای فرزند ChildNodes. پس با این حساب میتوانیم بگوییم گرههای 1 و 12 و 31 را هم فرزند گره 19 هستند و گره 19 والد آن هاست. همچنین گرههای یک والد را مثل 19 و 21 و 14 که والد مشترک دارند، گرههای خواهر و برادر یا حتی همنژاد Sibling میگوییم. همچنین ارتباط بین گره 7 و گرههای سطح دوم و الی آخر یعنی 1 و 12 و 31 و 23 و 6 را که والد بودن آن به صورت غیر مستقیم است را جد یا ancestor مینامیم و نوهها و نتیجههای آنها را نسل descendants.
ریشه Root: به گرهای میگوییم که هیچ والدی ندارد و خودش در واقع اولین والد محسوب میشود؛ مثل گره 7.
برگ Leaf: به گرههایی که هیچ فرزندی ندارند، برگ میگوییم. مثال گرههای 1 و12 و 31 و 23 و 6
گرههای داخلی Internal Nodes: گره هایی که نه برگ هستند و نه ریشه. یعنی حداقل یک فرزند دارند و خودشان یک گره فرزند محسوب میشوند؛ مثل گرههای 19 و 14.
مسیر Path: راه رسیدن از یک گره به گره دیگر را مسیر میگویند. مثلا گرههای 1 و 19 و 7 و 21 به ترتیب یک مسیر را تشکیل میدهند ولی گرههای 1 و 19 و 23 از آن جا که هیچ جور اتصالی بین آنها نیست، مسیری را تشکیل نمیدهند.
طول مسیر Length of Path: به تعداد لبههای یک مسیر، طول مسیر میگویند که میتوان از تعداد گرهها -1 نیز آن را به دست آورد. برای نمونه : مسیر 1 و19 و 7 و 21 طول مسیرشان 3 هست.
عمق Depth: طول مسیر یک گره از ریشه تا آن گره را عمق درخت میگویند. عمق یک ریشه همیشه صفر است و برای مثال در درخت بالا، گره 19 در عمق یک است و برای گره 23 عمق آن 2 خواهد بود.
تعریف خود درخت Tree: درخت یک ساختار داده برگشتی recursive است که شامل گرهها و لبهها، برای اتصال گرهها به یکدیگر است.
جملات زیر در مورد درخت صدق میکند:
- هر گره میتواند فرزند نداشته باشد یا به هر تعداد که میخواهد فرزند داشته باشد.
- هر گره یک والد دارد و تنها گرهای که والد ندارد، گره ریشه است (البته اگر درخت خالی باشد هیچ گره ای وجود ندارد).
- همه گرهها از ریشه قابل دسترسی هستند و برای دسترسی به گره مورد نظر باید از ریشه تا آن گره، مسیری را طی کرد.
برای پیاده سازی یک درخت، از دو کلاس یکی جهت ساخت گره که حاوی اطلاعات است <TreeNode<T و دیگری جهت ایجاد درخت اصلی به همراه کلیه متدها و خاصیت هایش <Tree<T کمک میگیریم.
public class TreeNode<T> { // شامل مقدار گره است private T value; // مشخص میکند که آیا گره والد دارد یا خیر private bool hasParent; // در صورت داشتن فرزند ، لیست فرزندان را شامل میشود private List<TreeNode<T>> children; /// <summary>سازنده کلاس </summary> /// <param name="value">مقدار گره</param> public TreeNode(T value) { if (value == null) { throw new ArgumentNullException( "Cannot insert null value!"); } this.value = value; this.children = new List<TreeNode<T>>(); } /// <summary>خاصیتی جهت مقداردهی گره</summary> public T Value { get { return this.value; } set { this.value = value; } } /// <summary>تعداد گرههای فرزند را بر میگرداند</summary> public int ChildrenCount { get { return this.children.Count; } } /// <summary>به گره یک فرزند اضافه میکند</summary> /// <param name="child">آرگومان این متد یک گره است که قرار است به فرزندی گره فعلی در آید</param> public void AddChild(TreeNode<T> child) { if (child == null) { throw new ArgumentNullException( "Cannot insert null value!"); } if (child.hasParent) { throw new ArgumentException( "The node already has a parent!"); } child.hasParent = true; this.children.Add(child); } /// <summary> /// گره ای که اندیس آن داده شده است بازگردانده میشود /// </summary> /// <param name="index">اندیس گره</param> /// <returns>گره بازگشتی</returns> public TreeNode<T> GetChild(int index) { return this.children[index]; } } /// <summary>این کلاس ساختار درخت را به کمک کلاس گرهها که در بالا تعریف کردیم میسازد</summary> /// <typeparam name="T">نوع مقادیری که قرار است داخل درخت ذخیره شوند</typeparam> public class Tree<T> { // گره ریشه private TreeNode<T> root; /// <summary>سازنده کلاس</summary> /// <param name="value">مقدار گره اول که همان ریشه میشود</param> public Tree(T value) { if (value == null) { throw new ArgumentNullException( "Cannot insert null value!"); } this.root = new TreeNode<T>(value); } /// <summary>سازنده دیگر برای کلاس درخت</summary> /// <param name="value">مقدار گره ریشه مثل سازنده اول</param> /// <param name="children">آرایه ای از گرهها که فرزند گره ریشه میشوند</param> public Tree(T value, params Tree<T>[] children) : this(value) { foreach (Tree<T> child in children) { this.root.AddChild(child.root); } } /// <summary> /// ریشه را بر میگرداند ، اگر ریشه ای نباشد نال بر میگرداند /// </summary> public TreeNode<T> Root { get { return this.root; } } /// <summary>پیمودن عرضی و نمایش درخت با الگوریتم دی اف اس </summary> /// <param name="root">ریشه (گره ابتدایی) درختی که قرار است پیمایش از آن شروع شود</param> /// <param name="spaces">یک کاراکتر جهت جداسازی مقادیر هر گره</param> private void PrintDFS(TreeNode<T> root, string spaces) { if (this.root == null) { return; } Console.WriteLine(spaces + root.Value); TreeNode<T> child = null; for (int i = 0; i < root.ChildrenCount; i++) { child = root.GetChild(i); PrintDFS(child, spaces + " "); } } /// <summary>متد پیمایش درخت به صورت عمومی که تابع خصوصی که در بالا توضیح دادیم را صدا میزند</summary> public void TraverseDFS() { this.PrintDFS(this.root, string.Empty); } } /// <summary> /// کد استفاده از ساختار درخت /// </summary> public static class TreeExample { static void Main() { // Create the tree from the sample Tree<int> tree = new Tree<int>(7, new Tree<int>(19, new Tree<int>(1), new Tree<int>(12), new Tree<int>(31)), new Tree<int>(21), new Tree<int>(14, new Tree<int>(23), new Tree<int>(6)) ); // پیمایش درخت با الگوریتم دی اف اس یا عمقی tree.TraverseDFS(); // خروجی // 7 // 19 // 1 // 12 // 31 // 21 // 14 // 23 // 6 } }
پیمایش درخت به روش عمقی (DFS (Depth First Search
هدف از پیمایش درخت ملاقات یا بازبینی (تهیه لیستی از همه گرههای یک درخت) تنها یکبار هر گره در درخت است. برای این کار الگوریتمهای زیادی وجود دارند که ما در این مقاله تنها دو روش DFS و BFS را بررسی میکنیم.
روش DFS: هر گرهای که به تابع بالا بدهید، آن گره برای پیمایش، گره ریشه حساب خواهد شد و پیمایش از آن آغاز میگردد. در الگوریتم DFS روش پیمایش بدین گونه است که ما از گره ریشه آغاز کرده و گره ریشه را ملاقات میکنیم. سپس گرههای فرزندش را به دست میآوریم و یکی از گرهها را انتخاب کرده و دوباره همین مورد را رویش انجام میدهیم تا نهایتا به یک برگ برسیم. وقتی که به برگی میرسیم یک مرحله به بالا برگشته و این کار را آنقدر تکرار میکنیم تا همهی گرههای آن ریشه یا درخت پیمایش شده باشند.
همین درخت را در نظر بگیرید:
پیمایش درخت را از گره 7 آغاز میکنیم و آن را به عنوان ریشه در نظر میگیریم. حتی میتوانیم پیمایش را از گره مثلا 19 آغاز کنیم و آن را برای پیمایش ریشه در نظر بگیریم ولی ما از همان 7 پیمایش را آغاز میکنیم:
ابتدا گره 7 ملاقات شده و آن را مینویسیم. سپس فرزندانش را بررسی میکنیم که سه فرزند دارد. یکی از فرزندان مثل گره 19 را انتخاب کرده و آن را ملاقات میکنیم (با هر بار ملاقات آن را چاپ میکنیم) سپس فرزندان آن را بررسی میکنیم و یکی از گرهها را انتخاب میکنیم و ملاقاتش میکنیم؛ برای مثال گره 1. از آن جا که گره یک، برگ است و فرزندی ندارد یک مرحله به سمت بالا برمیگردیم و برگهای 12 و 31 را هم ملاقات میکنیم. حالا همهی فرزندان گره 19 را بررسی کردیم، بر میگردیم یک مرحله به سمت بالا و گره 21 را ملاقات میکنیم و از آنجا که گره 21 برگ است و فرزندی ندارد به بالا باز میگردیم و بعد گره 14 و فرزندانش 23 و 6 هم بررسی میشوند. پس ترتیب چاپ ما اینگونه میشود:
7-19-1-12-31-21-14-23-6
پیمایش درخت به روش (BFS (Breadth First Search
در این روش (پیمایش سطحی) گره والد ملاقات شده و سپس همه گرههای فرزندش ملاقات میشوند. بعد از آن یک گره انتخاب شده و همین پیمایش مجددا روی آن انجام میشود تا آن سطح کاملا پیمایش شده باشد. سپس به همین مرحله برگشته و فرزند بعدی را پیمایش میکنیم و الی آخر. نمونهی این پیمایش روی درخت بالا به صورت زیر نمایش داده میشود:
7-19-21-14-1-12-31-23-6
اگر خوب دقت کنید میبینید که پیمایش سطحی است و هر سطح به ترتیب ملاقات میشود. به این الگوریتم، پیمایش موجی هم میگویند. دلیل آن هم این است که مثل سنگی میماند که شما برای ایجاد موج روی دریاچه پرتاب میکنید.
برای این پیمایش از صف کمک گرفته میشود که مراحل زیر روی صف صورت میگیرد:
- ریشه وارد صف Q میشود.
- دو مرحله زیر مرتبا تکرار میشوند:
- اولین گره صف به نام V را از Q در یافت میکنیم و آن را چاپ میکنیم.
- فرزندان گره V را به صف اضافه میکنیم.
var myMixins = { moveUp: function(){ console.log( "move up" ); }, moveDown: function(){ console.log( "move down" ); }, stop: function(){ console.log( "stop! in the name of love!" ); } };
// A skeleton carAnimator constructor function CarAnimator(){ this.moveLeft = function(){ console.log( "move left" ); }; } // A skeleton personAnimator constructor function PersonAnimator(){ this.moveRandomly = function(){ /*..*/ }; } // Extend both constructors with our Mixin _.extend( CarAnimator.prototype, myMixins ); _.extend( PersonAnimator.prototype, myMixins ); // Create a new instance of carAnimator var myAnimator = new CarAnimator(); myAnimator.moveLeft(); myAnimator.moveDown(); myAnimator.stop(); // Outputs: // move left // move down // stop! in the name of love!
// Disposable Mixin class Disposable { isDisposed: boolean; dispose() { this.isDisposed = true; } } // Activatable Mixin class Activatable { isActive: boolean; activate() { this.isActive = true; } deactivate() { this.isActive = false; } }
class SmartObject implements Disposable, Activatable { constructor() { setInterval(() => console.log(this.isActive + " : " + this.isDisposed), 500); } interact() { this.activate(); } // Disposable isDisposed: boolean = false; dispose: () => void; // Activatable isActive: boolean = false; activate: () => void; deactivate: () => void; }
applyMixins(SmartObject, [Disposable, Activatable]);
let smartObj = new SmartObject(); setTimeout(() => smartObj.interact(), 1000);
function applyMixins(derivedCtor: any, baseCtors: any[]) { baseCtors.forEach(baseCtor => { Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => { derivedCtor.prototype[name] = baseCtor.prototype[name]; }); }); }
اصل معکوس سازی وابستگیها
1) Dependency inversion principle یا DIP (اصل معکوس سازی وابستگیها)
DIP یکی از اصول طراحی نرم افزار است و D آن همان D معروف SOLID است (اصول پذیرفته شده شیءگرایی).
2) Inversion of Control یا IOC (معکوس سازی کنترل)
الگویی است که نحوه پیاده سازی DIP را بیان میکند.
3) Dependency injection یا DI (تزریق وابستگیها)
یکی از روشهای پیاده سازی IOC است.
4) IOC container
به فریم ورکهایی که کار DI را انجام میدهند گفته میشود.
Dependency inversion principle چیست؟
اصل معکوس سازی وابستگیها به این معنا است که بجای اینکه ماژولهای سطح پایین سیستم، رابطهای قابل استفادهای از خود را در اختیار سطوح بالاتر سیستم قرار دهند، ماژولهای قرار گرفته در سطوحی بالاتر، اینترفیسهایی را تعریف میکنند که توسط ماژولهای سطح پایین پیاده سازی خواهند شد.
همانطور که ملاحظه میکنید به این ترتیب وابستگیهای سیستم معکوس خواهند شد. نمونهای از عدم استفاده از این طراحی را در دنیای واقعی به صورت رومزه با آنها سر و کار داریم؛ مانند وسایل الکترونیکی قابل حملی که نیاز به شارژ مجدد دارند. برای مثال تلفنهای همراه، دوربینهای عکاسی دیجیتال و امثال آن.
هر کدام از اینها، رابطهای اتصالی متفاوتی دارند. یکی USB2، یکی USB3 دیگری Mini USB و بعضیها هم از پورتهای دیگری استفاده میکنند. چون هر کدام از لایههای زیرین سیستم (در اینجا وسایل قابل شارژ) رابطهای اتصالی مختلفی را ارائه دادهاند، برای اتصال آنها به منبع قدرت که در سطحی بالاتر قرار دارد، نیاز به تبدیلگرها و درگاههای مختلفی خواهد بود.
اگر در این نوع طراحیها، اصل معکوس سازی وابستگیها رعایت میشد، درگاه و رابط اتصال به منبع قدرت باید تعیین کننده نحوه طراحی اینترفیسهای لایههای زیرین میبود تا با این آشفتگی نیاز به انواع و اقسام تبدیلگرها، روبرو نمیشدیم.
اگر وابستگیها معکوس نشوند مطابق تصویر فوق، کلاس سطح بالایی را خواهیم داشت که به اینترفیس کلاسهای سطح پایین وابسته است. البته در اینجا اینترفیس یک کلمه عمومی است و بیشتر نحوه در معرض دید و استفاده قرار دادن اعضای یک کلاس مد نظر بوده است تا اینکه مثلا الزاما اینترفیسهای زبان خاصی مدنظر باشند.
مشکلی که در این حالت به زودی بروز خواهد کرد، افزایش کلاسهای سطح پایین و بیشتر شدن وابستگی کلاسهای سطح بالا به آنها است. به این ترتیب قابلیت استفاده مجدد خود را از دست خواهند داد.
در تصویر فوق حالتی را مشاهده میکنید که وابستگیها معکوس شدهاند. تغییر مهمی که در اینجا نسبت به حالت قبل رخ داده است، بالا بردن اینترفیس، به بالای خط میانی است که در تصویر مشخص گردیده است. این خط، معرف تعریف لایههای مختلف سیستم است. به عبارتی کلاسهای سطح بالا در لایه دیگری نسبت به کلاسهای سطح پایین قرار دارند. در اینجا اجازه دادهایم تا کلاس لایه بالایی اینترفیس مورد نیاز خود را تعریف کند. این نوع اینترفیسها در زبان سی شارپ میتوانند یک کلاس Abstract و یا حتی یک Interface متداول باشند.
با معکوس شدن وابستگیها، لایه سطح بالا است که به لایه زیرین عنوان میکند: تو باید این امکانات را در اختیار من قرار دهی تا بتوانم کارم را انجام دهم.
اکنون اگر در یک سیستم واقعی تعداد کلاسهای سطح پایین افزایش پیدا کنند، نیازی نیست تا کلاس سطح بالا تغییری کند. کلاسهای سطح پایین تنها باید عملکردهای تعیین شده در اینترفیس را پیاده سازی کنند. و این برخلاف حالتی است که وابستگیها معکوس نشدهاند:
تاریخچه اصل معکوس سازی وابستگیها
اصل معکوس سازی وابستگیها در نشریه C++ Report سال 1996 توسط شخصی به نام Bob Martin (معروف به Uncle Bob!) برای اولین بار مطرح گردید. ایشان همچنین یکی از آغاز کنندگان گروهی بود که مباحث Agile را ارائه کردند. به علاوه ایشان برای اولین بار مباحث SOLID را در دنیای شیءگرایی معرفی کردند (همان مباحث معروف هر کلاس باید تک مسئولیتی باشد، باز باشد برای توسعه، بسته برای تغییر و امثال آن که ما در این سری مباحث قسمت D آنرا در حالت بررسی هستیم).
مطابق تعاریف Uncle Bob:
الف) ماژولهای سطح بالا نباید به ماژولهای سطح پایین وابسته باشند. هر دوی اینها باید به Abstraction وابسته باشند.
ب) Abstraction نباید وابسته به جزئیات باشد. جزئیات (پیاده سازیها) باید وابسته به Abstraction باشند.
مثال برنامه کپی
اگر به مقاله Uncle Bob مراجعه کنید، یکی از مواردی را که عنوان کردهاند، یک برنامه کپی است که میتواند اطلاعات را از صفحه کلید دریافت و در یک چاپگر، چاپ کند.
حال اگر به این مجموعه، ذخیره سازی اطلاعات بر روی دیسک سخت را اضافه کنیم چطور؟ به این ترتیب سیستم با افزایش وابستگیها، پیچیدگی و if و elseهای بیشتری را خواهد یافت؛ از این جهت که سطح بالایی سیستم به صورت مستقیم وابسته خواهد بود به ماژولهای سطح پایین آن.
روشی را که ایشان برای حل این مشکل ارائه دادهاند، معکوس کردن وابستگیها است:
در اینجا سطح بالایی سیستم وابسته است به یک سری تعاریف Abstract خواندن و یا نوشتن؛ بجای وابستگی مستقیم به پیاده سازیهای سطح پایین آنها.
در این حالت اگر تعداد Readers و یا Writers افزایش یابند، باز هم سطح بالایی سیستم نیازی نیست تغییر کند زیرا وابسته است به یک اینترفیس و نه پیاده سازی آن که محول شده است به لایههای زیرین سیستم.
این مساله بر روی لایه بندی سیستم نیز تاثیرگذار است. در روش متداول برنامه نویسی، لایه بالایی به صورت مستقیم متدهای لایههای زیرین را صدا زده و مورد استفاده قرار میدهد. به این ترتیب هر تغییری در لایههای مختلف، بر روی سایر لایهها به شدت تاثیرگذار خواهد بود. اما در حالت معکوس سازی وابستگیها، هر کدام از لایههای بالاتر، از طریق اینترفیس از لایه زیرین خود استفاده خواهد کرد. در این حالت هرگونه تغییری در لایههای زیرین برنامه تا زمانیکه اینترفیس تعریف شده را پیاده سازی کنند، اهمیتی نخواهد داشت.
مثال برنامه دکمه و لامپ
مثال دیگری که در مقاله Uncle Bob ارائه شده، مثال برنامه دکمه و لامپ است. در حالت متداول، یک دکمه داریم که وابسته است به لامپ. برای مثال وهلهای از لامپ به دکمه ارسال شده و سپس دکمه آنرا کنترل خواهد کرد (خاموش یا روشن). مشکلی که در اینجا وجود دارد وابستگی دکمه به نوعی خاص از لامپ است و تعویض یا استفاده مجدد از آن به سادگی میسر نیست.
راه حلی که برای این مساله ارائه شده، ارائه یک اینترفیس بین دکمه و لامپ است که خاموش و روشن کردن در آن تعریف شدهاند. اکنون هر لامپی (یا هر وسیله الکتریکی دیگری) که بتواند این متدها را ارائه دهد، در سیستم قابل استفاده خواهد بود.