بررسی روش تزریق وابستگیها در سازندهی کلاسهای صفحات وب فرمها (بجای صرفا تزریق در خواص عمومی صفحات):
Modern ASP.NET Web Forms Development – Dependency Injection
Media Type یا MIME Type نشان دهنده فرمت یک مجموعه داده است. در HTTP، مدیا تایپ بیان کننده فرمت message body یک درخواست / پاسخ است و به دریافت کننده اعلام میکند که چطور باید پیام را بخواند. محل استاندارد تعیین Mime Type در هدر Content-Type است. درخواست کننده میتواند با استفاده از هدر Accept لیستی از MimeTypeهای قابل قبول را به عنوان پاسخ، به سرور اعلام کند.
Asp.net Web API از MimeType برای تعیین نحوه serialize یا deserialize کردن محتوای دریافتی / ارسالی استفاده میکند
مسئله
یک پروژه Web API بسازید و view model زیر را در آن تعریف کنید:
public class NewProduct { [Required] public string Name { get; set; } public double Price { get; set; } public byte[] Pic { get; set; } }
همانطور که میبینید یک فیلد از نوع byte[] برای تصویر محصول در نظر گرفته شده است.
حالا یک کنترلر API ساخته و اکشنی برای دریافت اطلاعات محصول جدید از کاربر مینویسیم :public class ProductsController : ApiController { [HttpPost] public HttpResponseMessage PostProduct(NewProduct model) { if (ModelState.IsValid) { // ثبت محصول return new HttpResponseMessage(HttpStatusCode.Created); } return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState); } }
و یک صفحه html به نام index.html که حاوی یک فرم برای ارسال اطلاعات باشد :
<!DOCTYPE html> <html> <head> <title></title> </head> <body> <h1>ساخت MediaTypeFormatter برای Multipart/form-data</h1> <h2>محصول جدید</h2> <form id="newProduct" method="post" action="/api/products" enctype="multipart/form-data"> <div> <label for="name">نام محصول : </label> <input type="text" id="name" name="name" /> </div> <div> <label for="price">قیمت : </label> <input type="number" id="price" name="price" /> </div> <div> <label for="pic">تصویر : </label> <input type="file" id="pic" name="pic" /> </div> <div> <button type="submit">ثبت</button> </div> </form> </body> </html>
زمانی که فرم حاوی فایلی برای آپلود باشد مشخصه encType باید برابر با Multipart/form-data مقداردهی شود تا اطلاعات فایل به درستی کد شوند. در زمان ارسال فرم Content-type درخواست برابر با Multipart/form-data و فرمت اطلاعات درخواست ارسالی به شکل زیر خواهد بود :
همانطور که میبینید هر فیلد در فرم، در یک بخش جداگانه قرار گرفته است که با خط چین هایی از هم جدا شده اند. هر بخش، headerهای جداگانه خود را دارد.
- Content-Disposition که نام فیلد و نام فایل را شامل میشود .
- content-type که mime type مخصوص آن بخش از دادهها را مشخص میکند.
پس از اینکه فرم را تکمیل کرده و ارسال کنید ، با پیام خطای زیر مواجه میشوید :
خطای روی داده اعلام میکند که Web API فاقد MediaTypeFormatter برای خواندن اطلاعات ارسال شده با فرمتMultiPart/Form-data است. Web API برای خواندن و بایند کردن پارامترهای complex Type از درون بدنه پیام یک درخواست از MediaTypeFormatter استفاده میکند و همانطور که گفته شد Web API فاقد Formatter توکار برای deserialize کردن دادههای با فرمت Multipart/form-data است.
راه حلها :روشی که در سایت asp.net برای آپلود فایل در web api استفاده شده، عدم استفاده از پارامترها و خواندن محتوای Request در درون کنترلر است. که به طبع در صورتی که بخواهیم کنترلرهای تمیز و کوچکی داشته باشیم روش مناسبی نیست. از طرفی امتیاز parameter binding و modelstate را هم از دست خواهیم داد.
روش دیگری که میخواهیم در اینجا پیاده سازی کنیم ساختن یک MediaTypeFormatter برای خواندن فرمت Multipart/form-data است. با این روش کد موردنیاز کپسوله شده و امکان استفاده از binding و modelstate را خواهیم داشت.
برای ساختن یک MediaTypeFormatter یکی از 2 کلاس MediaTypeFormatter یا BufferedMediaTypeFormatter را باید پیاده سازی کنیم . تفاوت این دو در این است که BufferedMediaTypeFormatter برخلاف MediaTypeFormatter از متدهای synchronous استفاده میکند.
یک کلاس به نام MultiPartMediaTypeFormatter میسازیم و کلاس MediaTypeFormatter را به عنوان کلاس پایه آن قرار میدهیم .
public class MultiPartMediaTypeFormatter : MediaTypeFormatter { ... }
ابتدا در تابع سازنده کلاس فرمت هایی که میخواهیم توسط این کلاس خوانده شوند را تعریف میکنیم :
public MultiPartMediaTypeFormatter() { SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data")); }
سپس با پیاده سازی توابع CanReadType و CanWriteType مربوط به کلاس MediaTypeFormatter مشخص میکنیم که چه مدلهایی را میتوان توسط این کلاس serialize / deserialize کرد. در اینجا چون میخواهیم این کلاس محدود به یک مدل خاص نباشد، از یک اینترفیس برای شناسایی کلاسهای مجاز استفاده میکنیم .
public interface INeedMultiPartMediaTypeFormatter { }
و آنرا به کلاس newProduct اضافه میکنیم :
public class NewProduct : INeedMultiPartMediaTypeFormatter { ... }
public override bool CanReadType(Type type) { return typeof(INeedMultiPartMediaTypeFormatter).IsAssignableFrom(type); } public override bool CanWriteType(Type type) { return false; }
و اما تابع ReadFromStreamAsync که کار خواندن محتوای ارسال شده و بایند کردن آنها به پارامترها را برعهده دارد
public async override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContent content, IFormatterLogger formatterLogger)
ابتدا محتوای ارسال شده را خوانده و اطلاعات فرم را استخراج میکنیم و از طرف دیگر با استفاده از کلاس Activator یک نمونه از مدل جاری را ساخته و لیست propertyهای آنرا استخراج میکنیم.
MultipartMemoryStreamProvider provider = await content.ReadAsMultipartAsync(); IEnumerable<HttpContent> formData = provider.Contents.AsEnumerable(); var modelInstance = Activator.CreateInstance(type); IEnumerable<PropertyInfo> properties = type.GetProperties();
سپس در یک حلقه به ترتیب برای هر property متعلق به مدل، در میان اطلاعات فرم جستجو میکنیم. برای پیدا کردن اطلاعات متناظر با هر property در هدر Content-Disposition که در بالا توضیح داده شد، به دنبال فیلد همنام با property میگردیم .
foreach (PropertyInfo prop in properties) { var propName = prop.Name.ToLower(); var propType = prop.PropertyType; var data = formData.FirstOrDefault(d => d.Headers.ContentDisposition.Name.ToLower().Contains(propName));
گفتیم که هر فیلد یک هدر، Content-Type هم میتواند داشته باشد. این هدر به صورت پیش فرض معادل text/plain است و برای فیلدهای عادی قرار داده نمیشود . در این مثال چون فقط یک
فیلد غیر رشته ای داریم فرض را بر این گرفته ایم که در صورت وجود Content-Type، فیلد مربوط به تصویر است. در صورتیکهContentType وجود داشته باشد، محتوای فیلد را به شکل Stream
خوانده به byte[] تبدیل و با استفاده از متد SetValue در property مربوطه قرار میدهیم.
if (data != null) { if (data.Headers.ContentType != null) { using (var fileStream = await data.ReadAsStreamAsync()) { using (MemoryStream ms = new MemoryStream()) { fileStream.CopyTo(ms); prop.SetValue(modelInstance, ms.ToArray()); } } }
در صورتی که Content-Type غایب باشد بدین معنی است که محتوای فیلد از نوع رشته است ( عدد ، تاریخ ، guid ، رشته ) و باید به نوع مناسب تبدیل شود. ابتدا آن را به صورت یک رشته میخوانیم و با استفاده از Convert.ChangeType آنرا به نوع مناسب تبدیل میکنیم و در property متناظر قرار میدهیم .
if (data != null) { if (data.Headers.ContentType != null) { //... } else { string rawVal = await data.ReadAsStringAsync(); object val = Convert.ChangeType(rawVal, propType); prop.SetValue(modelInstance, val); } }
return modelInstance;
config.Formatters.Add(new MultiPartMediaTypeFormatter());
در این قسمت در ابتدا نحوهی باز کردن یک پایگاه داهی چند بعدی را در محیط BIMS بررسی کرده و سپس چگونگی ساخت یک MDB را از پایه بررسی میکنیم. برای ادامه دادن این قسمت نیاز میباشد که پایگاه دادهی AdventureWorkDW2008 را در SSAS نصب کرده باشید .
در ابتدا مطابق شکل زیر منوی File سپس زیر منوی Open و Analysis Service Database را انتخاب نمایید.
در ادامه میبایست نام Server را مشخص نمایید و دقت داشته باشید که در اینجا منظور از نام سرور، نام سرور SSAS میباشد (در صورتیکه بر روی خود سرور در حال کار میباشید از . به جای نام سرور استفاده کنید). سپس در قسمت Database، نام پایگاه دادهی چند بعدی را انتخاب نمایید. در صورتی که به جز Adventure Work DW 2008 ، پایگاه دادههای چند بعدی دیگری را در SSAS داشته باشید، یک لیست از آنها را مشاهده خواهید کرد و در صورتیکه لیست شما خالی میباشد، احتمال دارد نام سرور اشتباه باشد یا روی سرویس SSAS مربوط به آن سرور هیچ پایگاه دادهی چند بعدی نصب نباشد.
حال مسیری را برای ذخیره سازی پروژهی جدید در نظر بگیرید:
پس از کمی شکیبایی، واکشی اطلاعات از روی پایگاه دادهی چند بعدی انتخاب شده انجام میشود و یک پروژه در ارتباط با آن پایگاه داده ساخته میشود.
همان طور که مشخص میباشد، یک شیء درون شاخهی Data Source وجود دارد که مشخص کنندهی ارتباط این پروژه با پایگاه دادهی Data Warehouse است. برای مشاهدهی این ارتباط، بر روی Adventure Work DW کلیک راست کنید و سپس گزینهی Open را انتخاب نمایید. در ادامه گزینهی Edit را بزنید.
سپس در پنجرهی جدید، تنظیمات رشتهی ارتباطی با DW را مشاهده نمایید
با زدن کلید Test Connection باید پیام Test Connection Succeeded را مشاهده نمایید. اکنون پنجرهها را با زدن کلید OK ببندید.
در قسمت Data Source View سه شی تعریف شده است؛ براساس دسته بندی مورد نظر و جاری در Business موجود در Adventure Work .
با کلیک راست کردن بر روی Adventure Works DW و انتخاب گزینهی Open، اقدام به باز کردن DSV انتخاب شده کنید. در صفحهی باز شده میتوانید انواع دیاگرام تهیه شده را مشاهده نمایید و همچنین لیستی از جداول موجود در این DSV مشخص میباشد.
با کلیک راست کردن در فضای خالی دیاگرام ، امکان Add/Remove کردن جداول را به دیاگرام دارید.
در شکل بالا بعد از انتخاب یک جدول در سمت راست و انتقال آن به سمت چپ میتوانید با زدن دکمهی Add Related Table براساس کلیدهای خارجی، جداول مرتبط با جدول انتخاب شده را به صورت خودکار انتخاب نمایید و به قسمت چپ انتقال دهید.
شما در ساخت Cube مشخص مینمایید که Cube را از کدام DSV خواهید ساخت. بنابراین انتخاب جداول در DSV ها میبایست براساس نوع Business شما باشد تا در ساخت Cube به مشکلی برخورد نکنید.
در ساختار درختی موجود در پنجرهی Solution در شاخهی Cube، میتوانید Adventure Works را باز کنید (کلیک راست و انتخاب Open ) .
در شکل بالا در سمت چپ، میتوانید Measure ها و Dimension های موجود در این Cube را مشاهده کنید. همچنین در قسمت بالا چندین Tab وجود دارند که در هر کدام تنظیمات بیشتری را بر روی Cube اعمال میکنیم. با توجه به اینکه طراحی Cube ها کاری تخصصی میباشد و نیاز به اطلاعات زیادی دارد اجازه دهید مقاله ای در خصوص طراحی Cube در SSAS جداگانه انتشار داده شود و فعلا در همین حد بسنده کنیم. با این حال در صورت نیاز میتوانید برای اطلاعات بیشتر در این خصوص کتاب Microsoft SQL Server Analysis Services 2008 With MDX از انتشارات Wrox را مطالعه نمایید.
در Solution Explorer در شاخهی ،Dimensions میتوانید تمامی بعدهایی که در تمامی Cube های شما استفاده شدهاند را مشاهده نمایید.
با انتخاب یک بعد (ترجیحا بعد Date ) و با کلیک راست کردن و انتخاب گزینهی Open آن را باز نمایید.
در پنجرهی باز شده میتوانید 4 Tab در بالا را مشاهد نمایید و در Tab نخست، Attribute ها و همچنین ساختار Hierarchies و در آخر Data source View را مشاهده نمایید.
در Attribute relationships می توانید ارتباط صفتهای یک بعد را مشخص نمایید.
در Browsing Tab میتوانید محتوای Dimension را بررسی نمایید (البته اگر در پروژهی جدید قرار دارید حتما میبایست پروژه را Deploy کرده باشید. در حالتیکه یک پایگاه داهی چند بعدی را باز میکنید، نیازی به Deploy کردن نمیباشد؛ زیرا حتما قبلا این کار انجام شده است (زیرا شما پایگاه دادهی چند بعدی را بعد از Deploy کردن پروژهی SSAS خواهید داشت ))
در صورتیکه مانند روش بالا یک پایگاه دادهی چند بعدی را باز کنیم، دیگر نیازی به Deploy کردن نمیباشد و فقط برای اعمال تغییرات روی پایگاه دادهی چند بعدی باید پروژه را Process کنیم و برای این منظور روی نام پروژه کلیک راست کرده و گزینهی Process را انتخاب کنید. با این کار تغییرات اعمال شده در BIMS روی پایگاه دادهی SSAS اعمال میگردند و دادهها با توجه به ساختار Cube ها دوباره پردازش میشوند.
برای ساخت یک پروژهی جدید به شکل زیر عمل میکنیم :
در ابتدا BIMS را باز کرده و سپس به منوی File رفته و در قسمت New گزینهی Project را انتخاب میکنیم. سپس در صفحهی باز شده، مطابق شکل زیر عمل کرده و یک پروژه از نوع Analysis Service Multidimensional … میسازیم.
سپس برروی شاخهی Data Source کلیک راست کرده و گزینهی New Data Source را میزنیم و پنجرههای ویزارد را به جلو میرویم.
در ابتدا باید یک Connection به DW تولید کنیم. برای این منظور در پنجرهی فوق دکمهی New را زده و اطلاعات را مطابق شکل زیر پر میکنیم.
و سپس OK را میزنیم.
در صورتی که SSAS در یک سرور دیگر نصب شده است در پنجرهی بعدی نیاز میباشد نام کاربری را که به سرویس SSAS در آن سرور دسترسی دارد را وارد کنیم.
در صورتی که SSAS روی سیستم Local نصب شده است و کاربری که با آن Login هستیم دسترسی کافی به SSAS را دارد، گزینهی Use the credentials of the current user را انتخاب میکنیم.
در صفحهی آخر یک نام برای DS انتخاب میکنیم.
سپس نیاز میباشد یک DSV بسازیم. برای این منظور روی شاخهی Data Source View کلیک راست کرده و گزینهی New را انتخاب کرده و سپس در پنجرهی Wizard باید Data Source ساخته شده در مرحلهی قبل را انتخاب کرده و سپس Next را بزنیم. در اینجا بر اساس بیزینسهای مختلف، راه کارهای گوناگونی را داریم. به عبارت دیگر میتوان جداول Fact و Dimension های مرتبط با آنرا بر اساس زیر سیستمهای مختلف انتخاب کرده و برای هر کدام از آنها یک DSV بسازیم. به نظر من میتوانیم تمامی جداول را در این مرحله انتخاب کرده و سپس این تفکیک بندی را در سطح Cube ها انجام داد. به طور کلی دقت داشته باشید به هیچ عنوان DSV و Cube های سیستم را خیلی تفکیک نکنید. زیرا در نوشتن کوئریها و Join بین Cube ها با مشکل و سختی روبرو خواهید شد. (از لحاظ تجربی تفکیک بندی به شرطی صورت گیرد که نیازی به Join کردن Cube ها در MDX Query ها نباشد.)
سپس یک نام برای DSV خود انتخاب کرده و Finish را بزنید.
خوب؛ آخرین مرحله ساخت Cube میباشد (البته در طراحی Cube مطالب بسیاری وجود دارند که در یک مقالهی دیگر تلاش خواهم کرد تمامی آن موارد را توضیح دهم.)
برای ساخت Cube ، روی شاخهی Cube کلیک راست کرده و گزینهی New را بزنید.
سپس Use Existing Table را انتخاب کرده و Next را بزنید.
در پنجرهی بعدی باید DSV را انتخاب کرد و بعد جداول مورد نیاز در طراحی Cube را انتخاب کنید. فراموش نکنید در صورت انتخاب یک Fact تمامی Dimension های مرتبط با آن را انتخاب نماید. دکمه Next را بزنید.
در پنجرهی بعدی باید جداول Fact را انتخاب کرده و دکمهی Next را بزنید.
سپس در پنجرهی بعدی دایمنشن را انتخاب نمایید. (ترجیحا اجازه بدهید خود BIMS برای شما Dimension ها را بسازد، هرچند که خود شما میتوانید بعدا به صورت دستی Dimension ها را ایجاد کنید).
بعد از زدن دکمهی Next نامی برای Cube خود انتخاب نمایید و سپس دکمهی Finish را بزنید.
بعد از ساخت Cube ، چندین دایمنشن به صورت خودکار ساخته میشوند . البته گاهی نیاز میباشد که اقدام به ساخت ساختارهای سلسله مراتبی در Dimension ها کنیم (این مورد را در یک مقاله جداگانه آموزش خواهم داد.)
پروژه با کلیدهای ترکیبی Ctrl+Shift+B ساخته میشود و بعد از اطمینان از درست بودن ساخت پروژه، آن را باید Deploy کرد.
برای Deploy کردن یک پروژه کافی است بعد از تنظیم کردن رشتهی ارتباطی در DS (قبلا توضیح داده شده است) روی پروژه کلیک راست کرده و گزینهی Deploy را بزنیم.
زمانی نیاز به این بازسازی کد بهوجود میآید که استفاده کنندهی از کلاسها، درگیر جزییات بیش از اندازهی کلاسها میشود. به طور مثال به نمودار بالا توجه نمایید.
در این نمودار تکه کدی مدل شده است که در آن ClientClass استفاده کننده از امکانات دو کلاس دیگر است. برای بدست آوردن مدیر یک شخص در این طراحی نیاز است ابتدا ClientClass اطلاعات مربوط به department یک شخص را با استفاده از متد GetDepartment بدست آورد. سپس با استفاده از متد GetManager در کلاس Department اقدام به دریافت اطلاعات مدیر نماید.
در طراحی بالا، برای دریافت اطلاعات مدیر یک فرد، با تکه کدی مانند زیر روبرو خواهیم شد:
var manager = person.GetDepartment().GetManager();
یکی از اصلیترین اصول طراحی کلاسها، کپسوله سازی اعضای کلاس، از استفاده کنندگان بیرونی آن است. کپسوله سازی به این معنی است که کلاس کمترین نیاز را به اطلاع از دیگر بخشهای سیستم داشته باشد. بنابراین در زمان تغییر آن بخشها دیگر نیازی نیست کلاس استفاده کننده از ریز تغییرات اطلاع پیدا کند.
روش کلی بازسازی کد پنهان سازی delegate، ایجاد یک کلاس (یا استفاده از کلاسهای موجود) به عنوان سرویس دهنده یا server است. در این کلاس به ازای کارکردهایی که نیاز به استفاده از چندین شیء یا متد را داشته باشند، یک متد ایجاد میکنیم. این متد روال لازم برای فراخوانیها را خود مدیریت و پیاده سازی میکند.
در مثال ذکر شدهی در ابتدای نوشتار میتوان کلاس سرویس دهندهی کارکرد دریافت مدیر را کلاس Person دانست. با این ترتیب بازسازی کد، رابطهی بین کلاسها را به صورت زیر تغییر میدهد.
با کمی توجه در نمودار میتوان متوجه شد متد موجود در کلاس Person به متد GetManager تغییر کرده است. زیرا در این کارکرد برای دریافت مدیر به کلاس Person رجوع میکنیم و نیازی نیست مستقیما به کلاس Department رجوع کنیم. مدیریت کردن نحوه دریافت مدیر یک Person نیز بر عهده این متد است.
همچنین برای دریافت مدیر یک شخص با چنین تکه کدی روبرو خواهیم شد:
var manager = person.GetManager();
همان طور که قبلا نیز ذکر شد، یکی از مزایای عمده این روش طراحی، مخفی کردن اطلاعات اضافی، از دید استفاده کنندگان کلاس است که در این مثال، نحوه دقیق دریافت مدیر است.
{ "name": "string", "email": "string", "password": "string", "isAdmin": true, "id": 0 }
import http from "./httpService"; import { apiUrl } from "../config.json"; const apiEndpoint = apiUrl + "/users"; export function register(user) { return http.post(apiEndpoint, { email: user.username, password: user.password, name: user.name }); }
class RegisterForm extends Form { state = { data: { username: "", password: "", name: "" }, errors: {} };
import * as userService from "../services/userService";
import { register } userService from "../services/userService";
doSubmit = async () => { try { await userService.register(this.state.data); } catch (ex) { if (ex.response && ex.response.status === 400) { const errors = { ...this.state.errors }; // clone an object errors.username = ex.response.data; this.setState({ errors }); } } };
{ "email": "string", "password": "string" }
var jwt = _tokenFactoryService.CreateAccessToken(user); return Ok(new { access_token = jwt });
import { apiUrl } from "../config.json"; import http from "./httpService"; const apiEndpoint = apiUrl + "/auth"; export function login(email, password) { return http.post(apiEndpoint + "/login", { email, password }); }
import * as auth from "../services/authService"; class LoginForm extends Form { state = { data: { username: "", password: "" }, errors: {} }; // ... doSubmit = async () => { try { const { data } = this.state; const { data: { access_token } } = await auth.login(data.username, data.password); console.log("JWT", access_token); localStorage.setItem("token", access_token); this.props.history.push("/"); } catch (ex) { if (ex.response && ex.response.status === 400) { const errors = { ...this.state.errors }; errors.username = ex.response.data; this.setState({ errors }); } } };
var jwt = _tokenFactoryService.CreateAccessToken(user); this.Response.Headers.Add("x-auth-token", jwt);
const response = await userService.register(this.state.data); console.log(response);
var jwt = _tokenFactoryService.CreateAccessToken(data); this.Response.Headers.Add("x-auth-token", jwt); this.Response.Headers.Add("access-control-expose-headers", "x-auth-token");
class RegisterForm extends Form { // ... doSubmit = async () => { try { const response = await userService.register(this.state.data); console.log(response); localStorage.setItem("token", response.headers["x-auth-token"]); this.props.history.push("/"); } catch (ex) { if (ex.response && ex.response.status === 400) { const errors = { ...this.state.errors }; // clone an object errors.username = ex.response.data; this.setState({ errors }); } } };
اما بعد از سپری شدن مدتی از توسعه محصول ممکن است اقلام اطلاعاتی خاصی بر روی هر یک از آیتمهای بالا نیاز شود. به طور مثال برای آدرس نیاز باشد اطلاعات استان و شهر جداگانه قابل ذخیره سازی و گزارش گیری باشند و یا در کنار نام مسئول رسیدگی به تیکت، شماره تلفن او نیز وجود داشته باشد.
در چنین شرایطی، یک اقدام ممکن، افزودن اقلام اطلاعاتی مورد نیاز در همان مکان آیتم قبلی است؛ به طور مثال اگر نام مسئول بر روی موجودیت تیکت باشد، شماره تلفن مسئول نیز در همان موجودیت تیکت اضافه شود.
راه حل مناسبتر برای حل این نوع مشکلات ایجاد کلاس خاص آیتم اطلاعاتی و استفاده از شیء آن بهجای مقدار مربوطه است. به طور مثال به طراحی زیر دقت نمایید. در طراحی زیر کلاس دیگری به نام Agent ایجاد و در کلاس تیکت از آن استفاده کردهایم.
این بازسازی کد دو مزیت کلی دارد: