اثبات قانون مشاهدهگر در برنامه نویسی
مفاهیم برنامه نویسی ـ آشنایی با سازندهها
اگر مورد خاصی مد نظر دارید بفرمایید تا توضیح بدم یا در صورت لزوم در متن اصلاح کنم. در نظر داشته باشید این سری مطالب با مطالب دیگر موجود در سایت متفاوت است و هدف خاصی را دنبال میکند که در بخش اول توضیح داده شد. بنابراین نمیتوان در این سری مطالب نگران کاربر مبتدیتر نبود چراکه جامعه هدف این بحثها هستند. به دلیل همین جامعه هدف مورد نظر، نوشتن این گونه مطالب دشوارتر از بیان مفاهیم پیچیدهتر برای کاربران حرفهایتر است. و همین امر به علاوه وقت محدود بنده سبب تأخیر در ارسال مطالب است. ضمناً صرف بیان انبوهی از اطلاعات تأثیر لازم را در خواننده نمیگذارد. در بسیاری مواقع بیان برخی مفاهیم مهندسی نرم افزار یا ویژگیهای جدید زبان (مانند var) به صورت مقایسه ای با روش پیشین سبب تثبیت بهتر مطالب در ذهن خواننده میشود. اما در شیوه کد نویسی تا حد ممکن سعی شده اصول رعایت شود و بیهوده خواننده با روش ناصحیح آشنا نشود. اگر نکته خاصی یافتید بفرمایید اصلاح کنیم.
MethodA().MethodB().MethodC();
public class RepresentativeEmployeeQuery { public dynamic GetById(int id) { throw new NotImplementedException(); } } public class RepresentativeQuery { public dynamic GetById(int id) { throw new NotImplementedException(); } } public class CustomerQuery { public dynamic GetById(int id) { throw new NotImplementedException(); } }
public static class Programm { static void Main(string[] args) { var customer = new CustomerQuery().GetById(1); var representativeId = customer.RepresentativeId; var representative = new RepresentativeQuery().GetById(representativeId); var managerId = representative.ManagerId; var manager = new RepresentativeEmployeeQuery().GetById(managerId); var managerName = manager.FullName; } }
- کلاس CustomerQuery پرس و جوهای مربوط به مشتری را مدیریت میکند.
- کلاس RepresentativeQuery پرس و جوهای مربوط به نمایندگی را مدیریت میکند .
- کلاس RepresentativeEmployeeQuery پرس و جوهای مربوط به کارمندان نمایندگی را مدیریت میکند.
مشکلات این کد بد بو
- افزایش کدهای تکراری
- افزایش احتمال بروز اشکال در زنجیره فراخوانیها با تغییر هر مرحله از آن
- نیاز به دانش درباره مراحل داخلی زنجیره، توسط تمامی استفاده کنندگان از آن (به طور مثال هر استفاده کنندهای که نام مدیر نمایندگی یک مشتری را نیاز دارد)
- افزایش احتمال بروز اشکال در پیاده سازی هر یک از استفاده کنندگان
روشها اصلاح این کد بد بو
- مخفی کردن زنجیره فراخوانی و مستقل سازی استفاده کننده از زنجیره فرخوانی (Hide delegate)
- ادغام بخشی از متدها در زنجیره فراخوانی و از بین بردن زنجیره فراخوانی
چه کدهایی Message Chain نیستند؟
MVC Scaffolding #1
کل سری ASP.NET MVC
به همراه کل سری EF Code First
MVC Scaffolding چیست؟
MVC Scaffolding ابزاری است برای تولید خودکار کدهای «اولیه» برنامه، جهت بالا بردن سرعت تولید برنامههای ASP.NET MVC مبتنی بر EF Code First.
بررسی مقدماتی MVC Scaffolding
امکان اجرای ابزار MVC Scaffolding از دو طریق دستورات خط فرمان Powershell و یا صفحه دیالوگ افزودن یک کنترلر در پروژههای ASP.NET MVC وجود دارد. در ابتدا حالت ساده و ابتدایی استفاده از صفحه دیالوگ افزودن یک کنترلر را بررسی خواهیم کرد تا با کلیات این فرآیند آشنا شویم. سپس در ادامه به خط فرمان Powershell که اصل توانمندیها و قابلیتهای سفارشی MVC Scaffolding در آن قرار دارد، خواهیم پرداخت.
برای این منظور یک پروژه جدید MVC را آغاز کنید؛ ابزارهای مقدماتی MVC Scaffolding از اولین به روز رسانی ASP.NET MVC3 به بعد با VS.NET یکپارچه هستند.
ابتدا کلاس زیر را به پوشه مدلهای برنامه اضافه کنید:
using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace MvcApplication1.Models { public class Task { public int Id { set; get; } [Required] public string Name { set; get; } [DisplayName("Due Date")] public DateTime? DueDate { set; get; } [DisplayName("Is Complete")] public bool IsComplete { set; get; } [StringLength(450)] public string Description { set; get; } } }
همانطور که ملاحظه میکنید در قسمت قالبها، تولید کنترلرهایی با اکشن متدهای ثبت و نمایش اطلاعات مبتنی بر EF Code First انتخاب شده است. کلاس مدل نیز به کلاس Task فوق تنظیم گردیده و در زمان انتخاب DbContext مرتبط، گزینه new data context را انتخاب کرده و نام پیش فرض آنرا پذیرفتهایم. زمانیکه بر روی دکمه Add کلیک کنیم، اتفاقات ذیل رخ خواهند داد:
الف) کنترلر جدید TasksController.cs به همراه تمام کدهای Insert/Update/Delete/Display مرتبط تولید خواهد شد.
ب) کلاس DbContext خودکاری به نام MvcApplication1Context.cs در پوشه مدلهای برنامه ایجاد میگردد تا کلاس Task را در معرض دید EF Code first قرار دهد. (همانطور که عنوان شد یکی از پیشنیازهای بحث Scaffolding آشنایی با EF Code first است)
ج) در پوشه Views\Tasks، پنج View جدید را جهت مدیریت فرآیندهای نمایش صفحات Insert، حذف، ویرایش، نمایش و غیره تهیه میکند.
د) فایل وب کانفیگ برنامه جهت درج رشته اتصالی به بانک اطلاعاتی تغییر کرده است. حالت پیش فرض آن استفاده از SQL CE است و برای استفاده از آن نیاز است قسمت 15 سری EF سایت جاری را پیشتر مطالعه کرده باشید (به چه اسمبلیهای دیگری مانند System.Data.SqlServerCe.dll برای اجرا نیاز است و چطور باید اتصال به بانک اطلاعاتی را تنظیم کرد)
معایب:
کیفیت کد تولیدی پیش فرض قابل قبول نیست:
- DbContext در سطح یک کنترلر وهله سازی شده و الگوی Context Per Request در اینجا بکارگرفته نشده است. واقعیت یک برنامه ASP.NET MVC کامل، داشتن چندین Partial View تغدیه شونده از کنترلرهای مختلف در یک صفحه واحد است. اگر قرار باشد به ازای هر کدام یکبار DbContext وهله سازی شود یعنی به ازای هر صفحه چندین بار اتصال به بانک اطلاعاتی باید برقرار شود که سربار زیادی را به همراه دارد. (قسمت 12 سری EF سایت جاری)
- اکشن متدها حاوی منطق پیاده سازی اعمال CRUD یا همان Create/Update/Delete هستند. به عبارتی از یک لایه سرویس برای خلوت کردن اکشن متدها استفاده نشده است.
- از ViewModel تعریف شدهای به نام Task هم به عنوان Domain model و هم ViewModel استفاده شده است. یک کلاس متناظر با جداول بانک اطلاعاتی میتواند شامل فیلدهای بیشتری باشد و نباید آنرا مستقیما در معرض دید یک View قرار داد (خصوصا از لحاظ مسایل امنیتی).
مزیتها:
قسمت عمدهای از کارهای «اولیه» تهیه یک کنترلر و همچنین Viewهای مرتبط به صورت خودکار انجام شدهاند. کارهای اولیهای که با هر روش و الگوی شناخته شدهای قصد پیاده سازی آنها را داشته باشید، وقت زیادی را به خود اختصاص داده و نهایتا آنچنان تفاوت عمدهای هم با کدهای تولیدی در اینجا نخواهند داشت. حداکثر فرمهای آنرا بخواهید با jQuery Ajax پیاده سازی کنید یا کنترلهای پیش فرض را با افزونههای jQuery غنی سازی نمائید. اما شروع کار و کدهای اولیه چیزی بیشتر از این نیست.
نصب بسته اصلی MVC Scaffolding توسط NuGet
بسته اصلی MVC Scaffolding را با استفاده از دستور خط فرمان Powershell ذیل، از طریق منوی Tools، گزینه Library package manager و انتخاب Package manager console میتوان به پروژه خود اضافه کرد:
Install-Package MvcScaffolding
Attempting to resolve dependency 'T4Scaffolding'. Attempting to resolve dependency 'T4Scaffolding.Core'. Attempting to resolve dependency 'EntityFramework'. Successfully installed 'T4Scaffolding.Core 1.0.0'. Successfully installed 'T4Scaffolding 1.0.8'. Successfully installed 'MvcScaffolding 1.0.9'. Successfully added 'T4Scaffolding.Core 1.0.0' to MvcApplication1. Successfully added 'T4Scaffolding 1.0.8' to MvcApplication1. Successfully added 'MvcScaffolding 1.0.9' to MvcApplication1.
پس از اینکه بسته MvcScaffolding به پروژه جاری اضافه شد، همان مراحل قبل را که توسط صفحه دیالوگ افزودن یک کنترلر انجام دادیم، اینبار به کمک دستور ذیل نیز میتوان پیاده سازی کرد:
Scaffold Controller Task
نکته و مزیت مهم دیگری که در اینجا در دسترس میباشد، سوئیچهای خط فرمانی است که به همراه صفحه دیالوگ افزودن یک کنترلر وجود ندارند. برای مثال دستور Scaffold Controller را تایپ کرده و سپس یک خط تیره را اضافه کنید. اکنون دکمه tab را مجددا بفشارید. منویی ظاهر خواهد شد که بیانگر سوئیچهای قابل استفاده است.
برای مثال اگر بخواهیم دستور Scaffold Controller Task را با جزئیات اولیه کاملتری ذکر کنیم، مانند تعیین نام دقیق کلاس مدل و کنترلر تولیدی به همراه نام دیگری برای DbContext مرتبط، خواهیم داشت:
Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext
بهبود مقدماتی کیفیت کد تولیدی MVC Scaffolding
در همان کنسول پاروشل NuGet، کلید up arrow را فشار دهید تا مجددا دستور قبلی اجرا شده ظاهر شود. اینبار دستور قبلی را با سوئیچ جدید Repository (استفاده از الگوی مخزن) اجرا کنید:
Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext -Repository
Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext -Repository -Force
public class TasksController : Controller { private readonly ITaskRepository taskRepository; // If you are using Dependency Injection, you can delete the following constructor public TasksController() : this(new TaskRepository()) { } public TasksController(ITaskRepository taskRepository) { this.taskRepository = taskRepository; }
public interface ITaskRepository : IDisposable { IQueryable<Task> All { get; } IQueryable<Task> AllIncluding(params Expression<Func<Task, object>>[] includeProperties); Task Find(int id); void InsertOrUpdate(Task task); void Delete(int id); void Save(); }
پیاده سازی این اینترفیس در حالت متد Save آن شامل فراخوانی context.SaveChanges است. این مورد باید به الگوی واحد کار (که در اینجا تعریف نشده) منتقل شود. زیرا در یک دنیای واقعی حاصل کار بر روی چندین موجودیت باید در یک تراکنش ذخیره شوند و قرارگیری متد Save داخل کلاس مخزن یا سرویس برنامه، مخزنهای تعریف شده را تک موجودیتی میکند.
اما در کل با توجه به اینکه پیاده سازی منطق کار با موجودیتها به کلاسهای مخزن واگذار شدهاند و کنترلرها به این نحو خلوتتر گردیدهاند، یک مرحله پیشرفت محسوب میشود.
برپایی پیشنیازها
ابتدا کتابخانهی react-router-dom را نصب میکنیم:
npm i react-router-dom --save
import { BrowserRouter } from "react-router-dom"; //... ReactDOM.render( <BrowserRouter> <App /> </BrowserRouter>, document.getElementById("root") );
برای تکمیل نیازمندیهایی که در مقدمه عنوان شد، این کامپوننتهای جدید را ایجاد میکنیم:
کامپوننت بدون حالت تابعی src\components\customers.jsx با این محتوا:
import React from "react"; const Customers = () => { return <h1>Customers</h1>; }; export default Customers;
کامپوننت بدون حالت تابعی src\components\rentals.jsx با این محتوا:
import React from "react"; const Rentals = () => { return <h1>Rentals</h1>; }; export default Rentals;
کامپوننت بدون حالت تابعی src\components\notFound.jsx با این محتوا:
import React from "react"; const NotFound = () => { return <h1>Not Found</h1>; }; export default NotFound;
کامپوننت بدون حالت تابعی src\components\movieForm.jsx با این محتوا:
import React from "react"; const MovieForm = () => { return ( <div> <h1>Movie Form</h1> <button className="btn btn-primary">Save</button> </div> ); }; export default MovieForm;
ثبت مسیریابیهای مورد نیاز برنامه
پس از نصب کتابخانهی مسیریابی و راه اندازی آن، اکنون نوبت به تعریف مسیریابیهای مورد نیاز برنامه در فایل app.js است:
import "./App.css"; import React from "react"; import { Redirect, Route, Switch } from "react-router-dom"; import Customers from "./components/customers"; import Movies from "./components/movies"; import NotFound from "./components/notFound"; import Rentals from "./components/rentals"; function App() { return ( <main className="container"> <Switch> <Route path="/movies" component={Movies} /> <Route path="/customers" component={Customers} /> <Route path="/rentals" component={Rentals} /> <Route path="/not-found" component={NotFound} /> <Redirect to="/not-found" /> </Switch> </main> ); } export default App;
تا اینجا اگر برنامه را اجرا کنیم، بلافاصله به http://localhost:3000/not-found هدایت میشویم. از این جهت که هنوز مسیریابی را برای / یا ریشهی سایت که در ابتدا نمایش داده میشود، تنظیم نکردهایم. به همین جهت Redirect زیر را پیش از آخرین Redirect تعریف شده اضافه میکنیم تا با درخواست ریشهی سایت، به آدرس /movies هدایت شویم:
<Redirect from="/" to="/movies" />
<Redirect from="/" exact to="/movies" />
افزودن منوی راهبری به برنامه
ابتدا فایل جدید src\components\navBar.jsx را ایجاد میکنیم؛ با این محتوا:
import React from "react"; import { Link, NavLink } from "react-router-dom"; const NavBar = () => { return ( <nav className="navbar navbar-expand-lg navbar-light bg-light"> <Link className="navbar-brand" to="/"> Home </Link> <button className="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation" > <span className="navbar-toggler-icon" /> </button> <div className="collapse navbar-collapse" id="navbarNavAltMarkup"> <div className="navbar-nav"> <NavLink className="nav-item nav-link" to="/movies"> Movies </NavLink> <NavLink className="nav-item nav-link" to="/customers"> Customers </NavLink> <NavLink className="nav-item nav-link" to="/rentals"> Rentals </NavLink> </div> </div> </nav> ); }; export default NavBar;
- ساختار کلی NavBar ای را که ملاحظه میکنید، دقیقا از مثالهای رسمی مستندات بوت استرپ 4 گرفته شدهاست و تمام classهای آن با className جایگزین شدهاند.
- سپس تمام anchorهای موجود در یک منوی راهبری بوت استرپ را به Link و یا NavLink تبدیل کردهایم تا برنامه به صورت SPA عمل کند؛ یعنی با کلیک بر روی هر لینک، بارگذاری کامل صفحه در مرورگر صورت نگیرد و تنها محل و قسمتی که توسط کامپوننتهای Route مشخص شده، به روز رسانی شوند. تفاوت NavLink با Link در کتابخانهی react-router-dom، افزودن خودکار کلاس active به المانی است که بر روی آن کلیک شدهاست. به این ترتیب بهتر میتوان تشخیص داد که هم اکنون در کجای منوی راهبری قرار داریم.
- پس از تبدیل anchorها به Link و یا NavLink، مرحلهی بعد، تبدیل hrefهای لینکهای قبلی به ویژگی to است که هر کدام باید به یکی از مسیریابیهای تنظیم شده، مقدار دهی گردد.
پس از تعریف کامپوننت منوی راهبری سایت، به app.js بازگشته و این کامپوننت را پیش از مسیریابیهای تعریف شده اضافه میکنیم:
import NavBar from "./components/navBar"; // ... function App() { return ( <React.Fragment> <NavBar /> <main className="container"> // ... </main> </React.Fragment> ); } export default App;
به علاوه به فایل index.css برنامه مراجعه کرده و padding این navBar را صفر میکنیم تا از بالای صفحه و بدون فاصلهای نمایش داده شود و container اصلی نیز اندکی از پایین آن فاصله پیدا کند:
body { margin: 0; padding: 0 0 0 0; font-family: sans-serif; } .navbar { margin-bottom: 30px; } .clickable { cursor: pointer; }
اگر دقت کنید چون آدرس http://localhost:3000/movies در حال نمایش است، در منوی راهبری، گزینهی متناظر با آن، با رنگی دیگر مشخص (فعال) شدهاست.
لینک کردن عناوین فیلمهای نمایش داده شده به کامپوننت movieForm
برای تبدیل عناوین نمایش داده شدهی در جدول فیلمها به لینک، به کامپوننت src\components\moviesTable.jsx مراجعه کرده و تغییرات زیر را اعمال میکنیم:
- در قدم اول باید بجای ذکر خاصیت Title در آرایهی ستونهای جدول:
class MoviesTable extends Component { columns = [ { path: "title", label: "Title" },
class MoviesTable extends Component { columns = [ { path: "title", label: "Title", content: movie => <Link to={`/movies/${movie._id}`}>{movie.title}</Link> },
همچنین این Link را هم باید در بالای این ماژول import کرد:
import { Link } from "react-router-dom";
تعریف مسیریابی نمایش جزئیات یک فیلم انتخابی
اگر به تصویر فوق دقت کنید، به آدرسهایی مانند http://localhost:3000/movies/5b21ca3eeb7f6fbccd47181a رسیدهایم که به همراه id هر فیلم هستند. اکنون میخواهیم کلیک بر روی این لینکها را جهت فعالسازی صفحهی نمایش جزئیات فیلم، تنظیم کنیم. به همین جهت به فایل app.js مراجعه کرده و مسیریابی زیر را به ابتدای Switch تعریف شده اضافه میکنیم:
<Route path="/movies/:id" component={MovieForm} />
import MovieForm from "./components/movieForm";
تکمیل کامپوننت نمایش جزئیات یک فیلم
اکنون میخواهیم صفحهی نمایش جزئیات فیلم، به همراه نمایش id فیلم باشد و همچنین با کلیک بر روی دکمهی Save آن، کاربر را به صفحهی movies هدایت کند. به همین جهت فایل src\components\movieForm.jsx را به صورت زیر ویرایش میکنیم:
import React from "react"; const MovieForm = ({ match, history }) => { return ( <div> <h1>Movie Form {match.params.id} </h1> <button className="btn btn-primary" onClick={() => history.push("/movies")} > Save </button> </div> ); }; export default MovieForm;
- چون این کامپوننت، یک کامپوننت تابعی بدون حالت است، props را باید از طریق آرگومان خود دریافت کند و البته در همینجا امکان Object Destructuring خواصی که از آن نیاز داریم، مهیا است؛ مانند { match, history } که ملاحظه میکنید.
- سپس شیء match، امکان دسترسی به params ارسالی به صفحه را مانند id فیلم، میسر میکند.
- با استفاده از شیء history و متد push آن میتوان علاوه بر به روز رسانی تاریخچهی مرورگر، به مسیر مشخص شده بازگشت که در همینجا و به صورت inline، تعریف شدهاست.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-17.zip
متدهای async تقلبی
class MyService { public int CalculateXYZ() { // Tons of work to do in here! for (int i = 0; i != 10000000; ++i) ; return 42; } }
class MyService { public async Task<int> CalculateXYZAsync() { return await Task.Run(() => { // Tons of work to do in here! for (int i = 0; i != 10000000; ++i) ; return 42; }); } }
کاری که در اینجا انجام شده، استفادهی ناصحیح از Task.Run در حین طراحی یک متد و یک API است. عملیات انجام شده در آن واقعا غیرهمزمان نیست و در زمان انجام آن، باز هم ترد جدید اختصاص داده شده را تا پایان عملیات قفل میکند. اینجا است که باید بین CPU-bound operations و IO-bound operations تفاوت قائل شد. اگر Entity Framework 6 و یا کلاس WebClient و امثال آن، متدهایی Async را نیز ارائه دادهاند، اینها به معنای واقعی کلمه، غیرهمزمان هستند و در آنها کوچکترین CPU-bound operation ایی انجام نمیشود.
در حلقهای که در مثال فوق در حال پردازش است و یا تمام اعمال انجام شده توسط CPU، از مرزهای سیستم عبور نمیکنیم. نه قرار است فایلی را ذخیره کنیم، نه با اینترنت سر و کار داشته باشیم و یا مثلا اطلاعاتی را از وب سرویسی دریافت کنیم و نه هیچگونه IO-bound operation خاصی قرار است صورت گیرد.
زمانیکه برنامه نویسی قرار است با API شما کار کند و به امضای async Task میرسد، فرضش بر این است که در این متد واقعا یک کار غیرهمزمان در حال انجام است. بنابراین جهت بالابردن کارآیی برنامه، این نسخه را نسبت به نمونهی غیرهمزمان انتخاب میکند.
حال تصور کنید که استفاده کننده از این API یک برنامهی دسکتاپ نیست، بلکه یک برنامهی ASP.NET است. در اینجا Task.Run فراخوانی شده صرفا سبب خواهد شد عملیات مدنظر، بر روی یک ترد دیگر، نسبت به ترد اصلی اختصاص داده شده توسط ASP.NET برای فراخوانی و پردازش CalculateXYZAsync، صورت گیرد. این عملیات بهینه نیست. تمام پردازشهای درخواستهای ASP.NET در تردهای خاص خود انجام میشوند. وجود ترد دوم ایجاد شده توسط Task.Run در اینجا چه حاصلی را بجز سوئیچ بیجهت بین تردها و همچنین بالا بردن میزان کار Garbage collector دارد؟ در این حالت نه تنها سبب بالا بردن مقیاس پذیری سیستم نشدهایم، بلکه میزان کار Garbage collector و همچنین سوئیچ بین تردهای مختلف را در Thread pool برنامه به شدت افزایش دادهایم. همچنین یک چنین سیستمی برای تدارک تردهای بیشتر و مدیریت آنها، مصرف حافظهی بیشتری نیز خواهد داشت.
یک اصل مهم در طراحی کدهای Async
استفاده از Task.Run در پیاده سازی بدنه متدهای غیرهمزمان، یک code smell محسوب میشود.
چکار باید کرد؟
اگر در کدهای خود اعمال Async واقعی دارید که IO-bound هستند، از معادلهای Async طراحی شده برای کار با آنها، مانند متد SaveChangesAsync در EF، متد DownloadStringTaskAsync کلاس WebClient و یا متدهای جدید Async کلاس Stream برای خواندن و نوشتن اطلاعات استفاده کنید. در یک چنین حالتی ارائه متدهای async Task بسیار مفید بوده و در جهت بالابردن مقیاس پذیری سیستم بسیار مؤثر واقع خواهند شد.
اما اگر کدهای شما صرفا قرار است بر روی CPU اجرا شوند و تنها محاسباتی هستند، اجازه دهید مصرف کننده تصمیم بگیرد که آیا لازم است از Task.Run برای فراخوانی متد ارائه شده در کدهای خود استفاده کند یا خیر. اگر برنامهی دسکتاپ است، این فراخوانی مفید بوده و سبب آزاد شدن ترد UI میشود. اگر برنامهی وب است، به هیچ عنوان نیازی به Task.Run نبوده و فراخوانی متداول آن با توجه به اینکه درخواستهای برنامههای ASP.NET در تردهای مجزایی اجرا میشوند، کفایت میکند.
به صورت خلاصه
از Task.Run در پیاده سازی بدنه متدهای API خود استفاده نکنید.
از Task.Run در صورت نیاز (مثلا در برنامههای دسکتاپ) در حین فراخوانی و استفاده از متدهای API ارائه شده استفاده نمائید:
private async void MyButton_Click(object sender, EventArgs e) { await Task.Run(() => myService.CalculateXYZ()); }
بنابراین نوشتن یک چنین کدهایی در پیاده سازی یک API غیرهمزمان
await Task.Run(() => { for (int i = 0; i != 10000000; ++i) ; });
برای مطالعه بیشتر
Should I expose asynchronous wrappers for synchronous methods
ابزارهای زیادی جهت تست کردن API های برنامههای وب موجود است. یکی از معروفترین آنها Fiddler است که ابزاری مستقل جهت دیباگ تحت پروتکل HTTP میباشد. یکی دیگر از این نرم افزارها Swashbuckle است که از Nuget قابل دریافت است و صفحهای را به برنامه اضافه میکند که به اختصار API های برنامه را نمایش داده و امکان اجرای آنها را فراهم میکند.
اما سادهترین راه جهت کردن تست کردن API های یک برنامه، استفاده از PowerShell است که عمل ایجاد درخواستهای HTTP را به راحتی از طریق Command line فراهم میکند و به شما اجازه میدهد بدون وارد شدن به جزئیات، بر روی نتیجه API مورد نظر تمرکز کنید. PowerShell ابتدا فقط برای محیط ویندوز طراحی شده بود ولی در حال حاضر قابلیت اجرای تحت Linux و Mac را نیز دارد.
در این بخش به شما نشان خواهم داد که چگونه متدهای یک Controller را با استفاده از PowerShell تست نمایید.
بدین منظور ابتدا کلاسی را به نام Reservation با مشخصات زیر، در یک پروژه ASP.NET Core Web Application ایجاد نمایید.public class Reservation { public int ReservationId { get; set; } public string ClientName { get; set; } public string Location { get; set; } }
public interface IRepository { IEnumerable<Reservation> Reservations { get; } Reservation this[int id] { get; } Reservation AddReservation(Reservation reservation); Reservation UpdateReservation(Reservation reservation); void DeleteReservation(int id); }
public class MemoryRepository : IRepository { private Dictionary<int, Reservation> items; public MemoryRepository() { items = new Dictionary<int, Reservation>(); new List<Reservation> { new Reservation { ClientName = "Alice", Location = "Board Room" }, new Reservation { ClientName = "Bob", Location = "Lecture Hall" }, new Reservation { ClientName = "Joe", Location = "Meeting Room 1" } }.ForEach(r => AddReservation(r)); } public Reservation this[int id] => items.ContainsKey(id) ? items[id] : null; public IEnumerable<Reservation> Reservations => items.Values; public Reservation AddReservation(Reservation reservation) { if (reservation.ReservationId == 0) { int key = items.Count; while (items.ContainsKey(key)) { key++; }; reservation.ReservationId = key; } items[reservation.ReservationId] = reservation; return reservation; } public void DeleteReservation(int id) => items.Remove(id); public Reservation UpdateReservation(Reservation reservation) => AddReservation(reservation); }
سپس کنترلری را به نام ReservationController به پروژه اضافه کنید که شامل متدهای زیر باشد:
[Route("api/[controller]")] public class ReservationController : Controller { private IRepository repository; public ReservationController(IRepository repo) => repository = repo; [HttpGet] public IEnumerable<Reservation> Get() => repository.Reservations; [HttpGet("{id}")] public Reservation Get(int id) => repository[id]; [HttpPost] public Reservation Post([FromBody] Reservation res) => repository.AddReservation(new Reservation { ClientName = res.ClientName, Location = res.Location }); [HttpPut] public Reservation Put([FromBody] Reservation res) => repository.UpdateReservation(res); [HttpPatch("{id}")] public StatusCodeResult Patch(int id, [FromBody] JsonPatchDocument<Reservation> patch) { Reservation res = Get(id); if (res != null) { patch.ApplyTo(res); return Ok(); } return NotFound(); } [HttpDelete("{id}")] public void Delete(int id) => repository.DeleteReservation(id); }
تست کردن درخواست از نوع GET
حالا دستور زیر را در پنجره PowerShell وارد کنید:Invoke-RestMethod http://localhost:7000/api/reservation -Method GET
location | clientName | reservationId |
Board Room | Alice | 0 |
Lecture Hall | Bob | 1 |
Meeting Room 1 | Joe | 2 |
حال جهت نمایش فقط یک رکورد از اطلاعات فوق، دستور زیر را وارد نمایید:
Invoke-RestMethod http://localhost:7000/api/reservation/1 -Method GET
location | clientName | reservationId |
Lecture Hall | Bob | 1 |
تست درخواست از نوع Post
تمامی متدهای ارائه شده توسط یک API را میتوان به کمک PowerShell تست کرد. اگرچه در این حالت فرمت بعضی از درخواستهای ارسالی ممکن است کمی به هم ریخته به نظر برسد. در اینجا درخواستی جهت اضافه کردن یک رکورد را به لیست Reservation ارسال میکنیم و نتیجه را در پاسخ ارسال شده از سمت سرور خواهیم دید:
Invoke-RestMethod http://localhost:7000/api/reservation -Method POST -Body (@{clientName="Anne"; location="Meeting Room 4"} | ConvertTo-Json) -ContentType "application/json"
location | clientName | reservationId |
Meeting Room 4 | Anne | 1 |
Invoke-RestMethod http://localhost:7000/api/reservation -Method GET
location | clientName | reservationId |
Board Room | Alice | 0 |
Lecture Hall | Bob | 1 |
Meeting Room 1 | Joe | 2 |
Meeting Room 4 | Anne | 3 |
تست درخواست از نوع PUT
درخواست از نوع PUT جهت جایگزینی دادهای موجود در لیست، با دادهی جدید است. بدین منظور کد داخلی شیء مورد نظر (در اینجا ReservationId) باید در URL گنجانده شود و مقادیر فیلدهای کلاس، در متن درخواست. مثالی جهت درخواست اصلاح داده موجود از طریق فرمان PowerShell :
Invoke-RestMethod http://localhost:7000/api/reservation -Method PUT -Body (@{reservationId="1"; clientName="Bob"; location="Media Room"} | ConvertTo-Json) -ContentType "application/json"
location | clientName | reservationId |
Media Room | Bob | 1 |
Invoke-RestMethod http://localhost:7000/api/reservation -Method GET
location | clientName | reservationId |
Board Room | Alice | 0 |
Media Room | Bob | 1 |
Meeting Room 1 | Joe | 2 |
Meeting Room 4 | Anne | 3 |
استفاده از دستور PATCH
این دستور جهت تغییر اطلاعات یک رکورد به کار میرود، ولی استفاده از آن در غالب برنامههای پیاده سازی شده نادیده گرفته میشود که البته چنانچه کاربران برنامههای مذکور به تمامی فیلدهای یک رکورد دسترسی داشته باشند، معقولانه به نظر میرسد. اما در یک برنامه بزرگ و پیچیده ممکن است به دلایلی از جمله مسایل امنیتی، کاربران اجازه دسترسی به تمامی فیلدهای یک رکورد را نداشته باشند و در این حالت نمیتوان از دستور PUT جهت ارسال درخواست استفاده کرد. دستورهای مبتنی بر PATCH گزینشی بوده و میتوان فقط فیلدهای خاصی را که مورد نظر میباشند، با آن تغییر داد.
ASP.NET Core MVC از استاندارد Json PATCH پشتیبانی میکند و این کار اجازه میدهد تا بتوان تغییرات را بطور یکنواختی انجام داد. جهت مشاهده جزئیات این دستور میتوانید به این لینک مراجعه کنید. اما برای مثال فرض کنید میخواهید چنین درخواستی را که به فرمت Json است از طریق HTTP PATCH به سمت سرور ارسال کنید:
[ { "op": "replace", "path": "clientName", "value": "Bob"}, { "op": "replace", "path": "location", "value": "Lecture Hall"} ]
دربسیاری از مواقع فقط از دستور replace درخواست PATCH استفاده میشود و کد داخلی رکورد مورد نظر، جهت تغییر در Url درخواست ارسالی، فرستاده خواهد شد. ASP.NET Core MVC به طور اتوماتیک این دستور را پردازش کرده و آنرا به صورت آبجکتی از نوع <JsonPatchDocument<T به اکشن کنترلر قید شده، پاس خواهد داد که در اینجا نوع T، از نوع کلاس مورد نظر ما میباشد. فرمت دستور ارسالی از طریق Powershell به شکل زیر خواهد بود:
Invoke-RestMethod http://localhost:7000/api/reservation/2 -Method PATCH -Body (@ { op="replace"; path="clientName"; value="Bob"},@{ op="replace"; path="location"; value="Lecture Hall"} | ConvertTo-Json) -ContentType "application/json"
جهت مشاهده تغییر ایجاد شده، دستور زیر را مجددا اجرا نمایید:
Invoke-RestMethod http://localhost:7000/api/reservation -Method GET
location | clientName | reservationId |
Board Room | Alice | 0 |
Media Room | Bob | 1 |
Lecture Hall | Bob | 2 |
Meeting Room 4 | Anne | 3 |
استفاده از دستور Delete
استفاده از دستور Delete هم به شکل زیر خواهد بود:
Invoke-RestMethod http://localhost:7000/api/reservation/2 -Method DELETE
Invoke-RestMethod http://localhost:7000/api/reservation -Method GET
where T:clas where T:struc ...
همانطور که مطلع هستید هر عنصری جنریکی را که تعریف میکنید حداقل دارای یک پارامتر نوع هست و در زمان بکارگیری آن جنریک باید نوع آن را مشخص نمایید. برای نمونه مثال زیر را در نظر بگیرید :
public class MyCollection<T> { private List<T> collections = new List<T>(); public void Add(T value) { collections.Add(value); } }
MyCollection<int> myintObj = new MyCollection<int>(); myintObj.Add(12); myintObj.Add(33); myintObj.Add(33.3);// ERROR z
ایجاد قیدها یا محدودیتها بر روی پارامترهای جنریکها شامل پنج حالت میباشد:
حالت اول : Where T:struct
در این حالت T باید یک ساختار باشد .
حالت دوم : where T:class
T باید یک نوع ارجاعی باشد. اگر در مثال فوق این قید را به آن اضافه کنیم، در هنگام ساخت نمونهای از کلاس فوق، اگر یک نوع value type را به T نسبت دهیم، در هنگام وارد کردن یک نوع value type با خطا مواجه خواهیم شد. مثال:
public class MyCollection<T> where T:class { private List<T> collections = new List<T>(); public void Add(T value) { collections.Add(value); } }
MyCollection<int> myintObj = new MyCollection<int>(); // ERROR , int is value type
حالت سوم : ()Where T:new
نوعی که به T نسبت داده میشود باید یک سازندهی پیش فرض داشته باشد.
داخل پرانتز : سازندهی پیش فرض: زمانی که شما یک کلاس مینویسید اگر آن کلاس دارای هیچ سازندهای نباشد، کامپایلر یک سازندهی بدون پارامتر را به کلاس فوق اضافه میکند که کار آن مقدار دهی به فیلدهای کلاس است. در اینجا از مقادیر پیش فرض استفاده میشود. مثلا برای int مقدار صفر و برای string مقدار "" و به همین ترتیب.
اگر از مقدار دهی پیش فرض توسط کامپایلر خرسند نیستید، میتوانید سازنده پیش فرض را تغییر داده و مطابق میل خود فیلدها را مقدار دهی اولیه کنید .
حالت چهارم : where T:NameOfBaseClass
نوعی که به T نسبت داده میشود باید از کلاس NameOfBaseClass ارث بری کرده باشد.
حالت پنجم : where T:NameOfInterface
همانند حالت چهارم میباشد؛ با این تفاوت: نوعی که به T نسبت داده میشود باید واسط NameOfInterface را پیاده سازی کرده باشد.
پنج حالت فوق نمونههایی از ایجاد محدودیت بر روی پرامتر نوع اعضای جنریک بودند و اما در ادامه قصد داریم نکاتی را در این باب، بیان کنیم:
نکته اول : میتوانید محدودیتهای فوق را با هم ترکیب کنید برای اینکار آنها را با کاما از هم جدا کنید :
public class MyCollection<T> where T:class,IDisposable,new() { //content }
- باید از نوع ارجاعی باشد.
- باید واسط IDisposable را پیاده سازی کرده باشد.
- باید یک سازندهی پیش فرض داشته باشد.
نکته دوم : زمانیکه از چندین محدودیت استفاده میکنید مثل مثال فوق، باید محدودیت ()new در آخرین جایگاه محدودیتها قرار گیرد؛ در غیر اینصورت با خطای زمان ترجمه روبه رو خواهید شد .
نکته سوم : میتوان محدودیتهای فوق را علاوه بر کلاس، بر روی متدهای جنریک نیز اعمال کنید:
public void Swap<T>(ref T val1,ref T val2) where T:struct { //content }
public class MyCollection<T,K> where T:class where K:IDisposable,new() { //content }
متدها به روشهای مختلفی تعریف میشوند و هر کدام رفتارهای مختلفی را در زمان ارث بری از خود نشان میدهند. روش استفاده استاندارد از آنها مانند شکل بالاست ولی در صورتیکه بخواهید این روش را تغییر دهید میتوانید به آنها کلمات کلیدی اضافه کنید.
Abstract methodها
abstract متدها به هیچ جایی اشاره نمیکنند. مانند شکل زیر:
درصورتی که کلاسهای شما دارای اعضای abstract باشند، باید خودشان نیز abstract باشند. شما نمیتوانید از این کلاسها وهلهایی ایجاد نمایید؛ ولی میتوانید از آنها در ارث بری سایر کلاسها استفاده کنید و از سایر کلاسها یک وهله ایجاد نمایید.
public abstract class Person { public abstract void ShowInfo(); } public class Teacher : Person { public override void ShowInfo() { Console.WriteLine("I am a teacher!"); } } public class Student : Person { public override void ShowInfo() { Console.WriteLine("I am a student!"); } }
Person person = new Teacher(); person.ShowInfo(); // Shows 'I am a teacher!' person = new Student(); person.ShowInfo(); // Shows 'I am a student!'
Virtual methodها
از کلاسهای دارای Virtual method میتوان یک وهله ایجاد کرد و حتی امکان تحریف Virtual methodهای کلاس پایه در Derived-class وجود دارد. مانند کد زیر:
public class Person { public virtual void ShowInfo() { Console.WriteLine("I am a person!"); } } public class Teacher : Person { public override void ShowInfo() { Console.WriteLine("I am a teacher!"); } }
باید توجه داشته باشید تصویر بالا با تصویر اول تفاوتی ندارد بدلیل اینکه virtual methodها به روش استاندارد پیاده سازی اشاره میکنند. با استفاده از کلمه کلید virtual شما به عنوان مثال به کلاس Person میگویید که متد ()ShowInfo میتواند پیاده سازیهای دیگری هم شاید داشته باشد و سایر پیاده سازیهای دیگر این متد باید با کلمه کلیدی override در کلاس دیگر (Teacher) مشخص شوند.
در ضمن در صورتیکه پیاده سازی دیگری از آن متد ارائه نشود از پیاده سازی کلاس پایه استفاده میشود.
public class Student : Person { } Person person = new Teacher(); person.ShowInfo(); // Shows 'I am a teacher!' person = new Student(); person.ShowInfo(); // Shows 'I am a person!'
نکته پایانی:
کلمه کلیدی new در متدهای کلاسهای پایه و Derived مانند shadowing عمل میکند. برای بیان بهتر به کد زیر توجه کنید:
public class Person { public void ShowInfo() { Console.WriteLine("I am Person"); } } public class Teacher : Person { public new void ShowInfo() { Console.WriteLine("I am Teacher"); } }