پیش از آنکه با مفهوم و سازوکار این نوع معماری آشنا شویم، نیاز است تا با مفهوم کلیترِ API آشنا گردیم. Application Programming Interface یا به اختصار API به منزلهٔ یک اینترفیس (رابط) مابین دو سیستم نرمافزاری است که این امکان را در اختیار سیستمهای مختلف قرار میدهد تا بتوانند بدون دخالت انسان با یکدیگر ارتباط برقرار سازند که برای کسب اطلاعات بیشتر، میتوانید به آموزش API چیست؟ مراجعه نمایید.
چنانچه یک ایپیآی در قالب یک سرویس تحت وب عرضه گشته و تعامل مابین سرویسهای مختلف را از طریق شبکهٔ اینترنت امکانپذیر سازد آن را Web API یا Web Service مینامیم و این در حالی است که پروتکلهای به کار رفته در وب سرویسها انواع و اقسام مختلفی دارد که از آن جمله میتوان به GraphQL ،SOAP ،PRC و یکی از معروفترین آنها REST اشاره کرد.
حال که با مفهوم ایپیآی آشنا شدیم، در ادامه روی معماری RESTful API تمرکز خواهیم کرد که به منزلهٔ یکی از معروفترین معماریهای مورد استفاده در توسعهٔ وب سرویسها است که تمرکز این دوره روی همین مورد است که برای کسب اطلاعات بیشتر میتوانید به آموزش آشنایی با مفهوم RESTful API مراجعه نمایید.
سازوکار یک RESTful API چگونه است؟
RESTful مخفف کلمات Representational State Transfer میباشد که یکی از انواع معماریهای طراحی API است که امروزه در اکثر شرکتهای نرمافزاری به کار گرفته میشود تا سایر دولوپرها را قادر سازند با سرویسهایی که عرضه میکنند به تعامل بپردازند.
این نوع معماری توسعهٔ وب سرویس در سال 2000 توسط Roy Fielding که یک دانشمند علوم کامپیوتری آمریکایی است در رسالهٔ دکترایش مطرح شد (وی در سال ۱۹۹۹ توسط MIT Technology Review به عنوان یکی از یکصد مخترع زیر ۳۵ سال دنیا لقب گرفت. همچنین وی همبنیانگذار پروژهٔ وب سرور Apache است.)
به طور خلاصه، میتوان گفت که معماری RESTful مجموعهای از یکسری گایدلاین (راهنما) است که با پیروی از آنها میتوان وب سرویسهایی ساخت که سریع، قابلاعتماد و قابلتوسعه باشند. به عبارت دیگر، RESTful چیزی نیست که در زبانهای برنامهنویسی یا فریمورکها گنجانده شده باشد بلکه صرفاً یکسری قوانین توسعهٔ نرمافزار است که چنانچه در توسعهٔ ایپیآیِ خود از آنها تبعیت کنیم، به سادگی برچسب RESTful روی سرویسمان خواهد خورد.
در معماری RESTful حروف RE برگرفته از کلمهٔ Representational میباشند که این اصطلاح به مسئلهٔ فرمت دیتای بازگشتی از سمت سرور اشاره دارد به طوری که این فرمت میتواند اچتیامال، جیسون، اکسامال و یا دهها فرمت دیگر باشد. به طور مثال، در ریکوئست زیر از سرور خواستهایم تا دادههای مد نظرمان را در قالب فرمت جسیون در اختیارمان قرار دهد:
Accept: application/json
برای درک بهتر این موضوع، فرض کنیم مقداری آب داریم که میتوانیم آن را هم در پارچ و هم در یک لیوان عرضه کنیم مضاف بر اینکه بسته به نیاز خود، حتی میتوانیم آن را منجمد کرده و در قالب یک تکه یخ عرضه کنیم. به عبارتی، آب که در این مثال یک ریسورس است میتواند از یکسری Represention یا «ظاهر» مختلف برخوردار باشد. به همین منوال، یک ریسورس در سمت سرور، مثلاً دیتای مرتبط با یک کاربر خاص، میتواند در قالبهای مختلفی همچون اچتیامال، جیسون، اکسامال و یا دیگر فرمتها عرضه گردد و همین میشود که از صفت Representational در نامگذاری این معماری استفاده شده است.
در معماری REST کلمات State Transfer بدین موضوع اشاره دارند که به جای ذخیرهسازی وضعیت کلاینت در سمت سرور، کلیهٔ دادهها در سمتِ خود کلاینت ذخیره میشوند و در هر درخواستی نیز برای سرور ارسال میشوند و همین میشود که این معماری اصطلاحاً Stateless نامیده میشود به طوری که ماهیت Stateless این معماری باعث میشود تا بتوانیم سیستمهای توزیعشدهای توسعه دهیم که در آنها میلیونها کاربر به صورت همزمان میتوانند با سرور ارتباط برقرار ساخته و دادههای مد نظر خود را درخواست کنند (در ادامه بیشتر در این خصوص توضیح خواهیم داد.)
آشنایی با قوانین ششگانهٔ معماری RESTful
به طور کلی، شش قانون در توسعهٔ یک وب سرویس با استفاده از معماری RESTful در نظر گرفته میشوند که در ادامه با تکتک آنها آشنا خواهیم شد:
Client-Server
زمانی که نحوهٔ توسعهٔ کلاینت ساید با سرور ساید از یکدیگر کاملاً مجزا باشد، همین مسئله باعث میگردد تا به سادگی بتوانیم از دیتای سمت سرور در کلاینتهای مختلفی از جمله یک وبسایت یا اپ موبایل استفاده نماییم و با توجه به اینکه در این نوع معماری حداقل وابستگی بین رابط کاربری و سرور وجود دارد، با سهولت هرچه تمام میتوانیم اقدام به توسعهٔ کامپوننتها کنیم بدون آنکه بخواهیم دغدغهٔ تداخلشان با یکدیگر را داشته باشیم. چنین ارتباطی بر اساس اصلِ Uniform Interface برقرار میشود به طوری که این اصل به دولوپرهای فرانتاند و بکاند اجازه میدهد تا بدون هیچگونه وابستگی به یکدیگر، اقدام به توسعهٔ فیچرهای مد نظر خود کنند.
Cacheable
به طور کلی، پاسخ به درخواستهای کلاینت میتواند در سمت سرور کَش شود که این سیاست به منظور بهبود پرفورمنس میتواند اتخاذ گردد.
Stateless
در اصطلاح RESTful، حرف S برگرفته از واژهٔ State به معنی «وضعیت» است اما در عین حال یکی از خصیصههای این معماری، برخورداری از قابلیتی است تحت عنوان Stateless به معنی «بدونِ وضعیت» که همین تناقض درک این موضوع را دشوار میسازد که در ادامه سعی میکنیم این موضوع را به زبان ساده تشریح نماییم.
زمانی که مابین کلاینت و سرور یک ارتباط از جنسِ اچتیتیپی برقرار میگردد، در سمتِ سرور هرگز وضعیت یا بهتر بگوییم اطلاعاتی از کلاینت ذخیره نخواهد شد بلکه روش کار بدین صورت است که کلاینت یک ریکوئست حاوی کلیهٔ جزئیاتی که سرور برای اجرای تَسک مذکور نیاز دارد برای سرور ارسال میکند و سرور هم بدون آنکه دغدغهٔ این را داشته باشد که کلاینت چیست و کجا است، ریسپانس مرتبط را در اختیارش میگذارد و در نهایت هم این ارتباط به پایان میرسد.
به طور خلاصه، Stateless حاکی از آن است که هر درخواست از سمت کلاینت مستقل از دیگر درخواستها است و زمانی که کلاینت درخواستی از جنس اچتیتیپی ایجاد میکند، چنین درخواستی حاوی کلیهٔ جزئیات و اطلاعات مورد نیاز سرور برای هندل کردن تَسک مربوطه است و سرور هرگز به دادههای قبلی برای این کار اتکا نمیکند. به طور مثال، اگر دادهای مهم باشد و در سرور برای درست هندل کردن درخواست به آن نیاز داشته باشد، کلاینت میباید هر دفعه آن داده را در کنار دادههای دیگر ارسال کند. به طور مثال، چنانچه به منظور استفاده از سرویسی نیاز به لاگین کردن داشته باشیم، توکنی که نشان میدهد ما کاربر معتبری هستیم (Authentication) در هر بار ارسال درخواست میباید برای سرور ارسال گردد (در همین راستا، میتوانید به این پرسشوپاسخ در سایت استکاورفلو مراجعه نمایید.)
برای درک بهتر این موضوع، میتوان یکی از شبکههای اجتماعی همچون توییتر را مثال زد. در این اپلیکیشن، همچون سایر شبکههای اجتماعی، وقتی که به اصطلاح Scroll Down میکنیم توییتهای جدیدی در معرض دیدمان قرار میگیرد اما این در حالی است که اگر اپ را ببندیم و مجدد آن را باز نماییم، مجدد به ابتدای تایملاین باز میگردیم و سرور هرگز نسبت به اینکه آخرین باری که اپ را مورد استفاده قرار دادیم در کجای تایملاین قرار داشتیم اطلاعی ندارد و این همان مفهوم Stateless است. به عبارت دیگر، سرور هرگز نسبت به وضعیت درخواستهای قبلی کلاینت (در این مثال اپلیکیشن توییتر) اطلاعی ندارد.
اساساً میتوان گفت که یکی از مزیتهای کلیدی Stateless آن است که تغییرات صورتگرفته روی سرور هرگز کلاینت را با مشکل مواجه نمیکنند. به طور مثال، اگر در همان لحظهای که کلاینت در حال برقراری ارتباط با سرور است به هر دلیلی سرور از دسترس خارج شده و سرور جایگزینی برایش در نظر گرفته شود، کلاینت بدون هیچ مشکلی میتواند دیتای مد نظر خود را دریافت کند.
Uniform Interface
یکپارچکی در توسعهٔ یک ایپیآی از جنس RESTful یک باید است مضاف بر اینکه به منظور حفظ یکپارچگی، هر ریسورس باید از اصول ثابتی تبعیت کند که از آن جمله میتوان به نحوهٔ نامگذاری، فرمت دیتا و ... اشاره کرد. وقتی توسعهدهندگان با بخشی از ایپیآی آشنا شدند، باید بتوانند با رویکرد یکسانی از سایر بخشهای ایپیآی مذکور استفاده نمایند. گرچه اصل Uniform Interface در ظاهر پیچیده و گیجکننده به نظر میرسد، اما در عمل مفهوم بسیار سادهای است بدین صورت که در توسعهٔ یک ایپیآی از جنس رِست میباید از متدهای پروتکل اچتیتیپی استفاده کرد (البته محدود به استفاده از این پروتکل نیستیم.)، برای دستیابی به ریسورسهای مختلف میباید از یک یوآرال منحصربهفرد استفاده کرد به علاوه اینکه در ریسپانس میباید با استفاده از کدهای وضعیت، اطلاعات شفافی در اختیار کلاینت قرار دهیم. این اصل خود شامل یکسری اصول دیگر است که عبارتند از:
- Resource-Based: تکتک ریسورسها در معماری رِست از طریق یکسری URI خاص مشخص میشوند و خودِ این ریسورسها نیز از آنچه در سمت فرانتاند در اختیار کاربران قرار میگیرند مجزا هستند. برای مثال، سرور بسته به نیازی که اپلیکیشن دارا است ریسورس درخواستی را در قالب جیسون، اکسامال و یا اچتیامال در اختیار بخش فرانتاند قرار میدهد.
- Manipulation of Resources Through Representations: زمانی که در سمت فرانتاندِ اپلیکیشن یک ریسورس دریافت میشود، از آن پس اپلیکیشن از دیتای کافی به منظور اِعمالِ یکسری تغییرات و یا حذف آن ریسورس برخوردار است (البته این در حالی است که پرمیشن لازم را داشته باشد.)
- Self-descriptive Messages: در معماری رِست هر پیامی دربرگیرندهٔ دیتای کافی و گویا به منظور تشریح چگونگی پردازشاش را دارا است. برای مثال، پیامهای اچتیتیپی به خوبی میتوانند مشخص کنند که مثلاً توسط چه پارسری پردازش شوند.
- HATEOAS: این سرواژه که برگرفته از واژگان Hypermedia As The Engine Of Application State است به این نکته اشاره دارد که کلاینتها State یا «وضعیت» کنونی را از طریق پارامترهای مختلف، هِدِرهای اچتیتیپی و ... در اختیار سرور میگذارند و سرو نیز از طریق کدهای وضعیت و هِدِرهای اچتیتیپی پاسخی به درخواستها عرضه میکند که از بُعد فنی چنین چیزی Hypermedia یا Hyperlinks نامگذاری میشود. با در نظر گرفتن این توضیحات، HATEOAS همچنین بدان معنا است که در صورت نیاز لینکهایی را میتوان در بدنهٔ هِدِرها قرار داد تا لینکی به ریسورسهای مرتبط در اختیار کلاینت قرار گیرد.
- Layered System: این معماری امکانی را در اختیارمان میگذارد تا به طور مثال بتوان اصطلاحاً Business Logic سرویس خود را روی سرور «الف»، دادهها را روی سروی «ب» و همچنین تصدیق اطلاعات کاربر (Authentication) را روی سرور «پ» انجام داد. این نوع معماری که مبتنی بر لایههای انتزاعی مختلفی است امکانی را در اختیار سیستم میگذارد تا هر کامپوننت صرفاً لایهای که در حال تبادل داده با آن است را دیده و درگیر کامپوننتهای دیگر نگردد.
- همچنین این اصل حاکی از آن است که سرورهایی که به عنوان لایههای میانی در نظر گرفته میشوند (همچون Load-balancing) منجر به بهبود توسعهپذیری سیستم میشوند مضاف بر اینکه در همین لایههای میانی میتوان یکسری سیاستهای امنیتی نیز به منظور بهبود امنیت دیتای کاربران اِعمال کرد.
- Code on Demand: این اصل که اختیاری میباشد حاکی از آن است که به منظور اجرای یک درخواست مبتنی بر این معماری، کلاینت میتواند کدی را در قالب اِپلِت جاوا یا اسکریپتی خاص در دیگر زبانها دانلود و اجرا کند. به عبارت دیگر، پیش از این گفتیم که ریسورسها میتوانند از یکسری Represention یا «ظاهر» مختلف برخوردار باشند که با این توضیحات، این اصل حاکی از آن است که یکی از فرمتهایی که میتوان برای یک ریسورس در نظر گرفت، کد یا اسکریپتی است که باید در سمت کلاینت اجرا شده و تَسک خاصی را عملی سازد.
چنانچه سرویسی پنج اصل ابتدایی + اصل ششم (Code on Demand) که اختیاری است را داشته باشد، میتوان برچسب RESTful روی آن زد اما در عین حال توجه داشته باشیم که بسته به نوع نرمافزار و نیازهایی که داریم، گاهی میتوانیم برخی از این اصول را نقض کنیم که در چنین شرایطی باز هم معماری ما RESTful خواهد بود.
آشنایی با مفهوم Resource در معماری RESTful
در این معماری، هر چیزی که در سرور وجود داشته باشد و از سرور بخواهیم تا آن را در اختیارمان بگذارد یک Resource است که به صورت تحتالفظی میتوان آن یک «چیز» معنا کرد و این در حالی است که هر ریسورس یک شناسه دارد که تحت عنوان Resource Identifier شناخته میشود که با استفاده از آن شناسایی میشود. به طور مثال داریم:
https://sokanacademy.com/api/articles/1
در مثال فوق، شناسهٔ ۱ به مقالهای اشاره دارد که با آیدی ۱ در دیتابیس ذخیره شده است. حال اگر بخواهیم ریسورسی حاوی تمامی مقالات داشته باشیم، از یوآرال فرضی زیر استفاده میکنیم:
https://sokanacademy.com/api/articles
در واقع، سرور از طریق این یوآرال لیستی از تمامی مقالات را در اختیارمان خواهد گذاشت. همچنین وضعیت هر ریسورس در یک تایم مشخص تحت عنوان Resource Representation شناخته میشود که حاوی یکسری جزئیات، متادیتا و همچنین لینک به ریسورسهای دیگر است.
پیش از این اشاره کردیم که در معماری RESTful حروف RE برگرفته از کلمهٔ Representational میباشند. در تکمیل این موضوع، میتوان گفت که فرمت دیتای یک ریسورس که تحت عنوان Media Type شناخته میشود دربرگیرندهٔ خصوصیاتی است که نشان میدهند یک Resource Representation، که در پاراگراف بالا گفتیم چیست، چگونه باید پردازش شود. به طور مثال، وقتی که فرمت مذکور به طور مثال text/html
باشد، مرورگر میداند که باید با ریسپانس دریافتی همچون یک فایل اچتیامال رفتار کرده و آن را به شکلی رِندِر کند که برای کاربران قابلدرک باشد.
درآمدی بر عملیات CRUD
سرواژهٔ CRUD برگرفته از واژگان Update ،Read ،Create و Delete است که به منظور انجام عملیاتی از این دست، نیاز است تا با مفهوم HTTP Verbs آشنا شویم که برخی از عمدهترین آنها عبارتند از:
GET
به منظور فراخوانی ریسورسها استفاده میشود.POST
به منظور ثبت یک ریسورس جدید استفاده میشود.PUT
به منظور آپدیت ریسورسهایی که از قبل ثبت شدهاند استفاده میشود (این متد همچنین به منظور ثبت یک ریسورس جدید نیز میتواند مورد استفاده قرار گیرد.)DELETE
به منظور حذف یک ریسورس استفاده میشود.
چنانچه بخواهیم به مثال قبل بازگردیم، به منظور فراخوانی کلیهٔ مقالات، درخواستی به صورت زیر به سمت سرور ارسال میکنیم:
GET /api/articles
توجه داشته باشیم که به عنوان یک قانون توافقی، در صورتی که بخواهیم کلیهٔ ریسورسها را به دست آوریم، نام ریسورس که در اینجا articles
است میباید به صورت جمع باشد. ریسپانس سرور به چنین ریکوئستی نیز به صورت زیر خواهد بود:
[
{
id: 1,
img: 'img-1.jpg',
title: 'title 1',
address: 'address-1.html'
},
{
id: 2,
img: 'img-2.jpg',
title: 'title 2',
address: 'address-2.html'
},
{
id: 3,
img: 'img-3.jpg',
title: 'title 3',
address: 'address-3.html'
},
]
همانطور که میبینیم، آرایهای از یکسری آبجکت مرتبط با مقالات در اختیارمان قرار میگیرد. حال اگر بخواهیم صرفاً یک ریسورس را از سرور فراخوانی کنیم، میباید درخواستی با استفاده از متد GET
به صورت زیر ارسال کنیم:
GET /api/articles/1
همانطور که میبینیم، شناسهٔ مقالهٔ مد نظر خود را هم پاس دادهایم و پاسخی هم که دریافت خواهیم کرد به صورت زیر خواهد بود:
{
id: 1,
img: 'img-1.jpg',
title: 'title 1',
address: 'address-1.html'
},
لازم به یادآوری است که متد GET
پرکاربردترین متد اچتیتیپی است مضاف بر اینکه صرفاً به منظور فراخوانی دیتا استفاده شده و اصطلاحاً Idempotent میباشد. به منظور ایجاد یک مقالهٔ جدید هم از متد POST
به صورت زیر استفاده خواهیم کرد:
POST /api/articles
{
img: 'img-4.jpg',
title: 'title 4',
address: 'address-4.html'
}
اکثر فرمهایی که در سایتها دیده میشوند به منظور ثبت اطلاعات در دیتابیس از متد POST
استفاده میکنند که در صورت موفقیتآمیز بودن عملیات، کد وضعیت 201 بازگردانده خواهد شد (لازم به یادآوری است که متد POST
نه اصطلاحاً Safe است و نه Idempotent به طوری که اگر درخواستی از این جنس را چند بار پشت سر هم تکرار کنیم، به همان تعداد ریسورس جدید روی دیتابیس ایجاد میگردد.)
به منظور آپدیت کردن یک مقاله، درخواستی از جنس PUT
برای سرور ارسال میکنیم به طوری که خواهیم داشت:
PUT /api/articles/1
{
title: 'new title',
address: 'new-address-1.html'
}
وقتی سرور چنین درخواستی را دریافت کنید، ریسورسی با شناسهٔ ۱ را از دیتابیس فراخوانی کرده و دادههای قبلی را با دادههای جدید جایگزین میکند و کد وضعیت 200 را بازمیگرداند. همچنین به خاطر داشته باشیم که متد PUT
اصلاً امن نیست چرا که بواسطهٔ آن میتوانیم دیتای سمت سرور را دستخوش تغییر سازیم اما در عین حال متدی Idempotent است بدان معنا که هر چند بار که آن را تکرار کنیم، صرفاً نتیجهٔ اولین اجرای آن در سرور ذخیره شده و باقی میماند. به همین منوال، به منظور حذف یک مقاله خواهیم داشت:
DELETE /api/articles/1
درخواست فوق منجر به حذف مقالهای با شناسهٔ ۱ خواهد شد و کد وضعیت 200 را بازمیگرداند. لازم به یادآوری است که متد DELETE
نیز از جنس Idempotent است زیرا چنانچه یک ریسورس را حذف کنیم و مجدداً درخواست خود را بارها و بارها تکرار کنیم، آن ریسورس یک بار حذف شده و تکرار عملیات مذکور تغییر جدیدی نمیتواند ایجاد کند و اجرای این متد برای بار دوم، سوم و ... منجر به پیام 404 میشود.
چنانچه بخواهیم مجموعه عملیات CRUD که در بالا مثال زدیم را جمعبندی کنیم، خواهیم داشت:
GET /api/articles
GET /api/articles/1
POST /api/articles
PUT /api/articles/1
DELETE /api/articles/1
هر یوآرال که در بالا مشاهده میکنیم اصطلاحاً یک Endpoint است که دولوپرها با استفاده از آنها میتوانند به تعامل با وب سرویسمان بپردازند.
حال ممکن است این پرسش ایجاد گردد که «چرا هم برای فراخوانی مقالات و هم ثبت یک مقالهٔ جدید از یک اندپوینت استفاده میکنیم؟» که در پاسخ به این پرسش باید گفت درست است هر دو یوآرال (اندپوینت) یکسان هستند، اما سرور پس از دریافت ریکوئست بسته به نوع متد ارسالی (GET
یا POST
) تَسک مربوطه را تشخیص داده و عملی میکند.
همچنین در ارتباط با متدهای پروتکل اچتیتیپی لازم به یادآوری است که متدهای دیگر نیز وجود دارند که از آن جمله میتوان به HEAD
، OPTIONS
و PATCH
اشاره کرد اما این متدها خیلی رایج نیستند.
امنیت متدهای اچتیتیپی
به طور کلی، برخی متدها همچون HEAD
یا GET
هستند که به عنوان متدهای امن در نظر گرفته میشوند چرا که این متدها صرفاً به منظور فراخوانی دیتا از سمت سرور به کار گرفته میشوند و بالتبع نمیتوانند تغییری روی دادهها اِعمال کنند. از سوی دیگر، این متدها اصطلاحاً Idempotent میباشند به این معنی که به هر تعداد دفعه که اجرا شوند، نتیجهٔ یکسانی را ریترن خواهند کرد که در آموزشهای آتی در این خصوص به تفصیل توضیح خواهیم داد.
جمعبندی
REST یک پروتکل نیست زیرا وقتی که صحبت از پروتکل به میان میآید، تحت هر شرایطی و در هر موقعیتی موظف به پیروی از قوانین وضعشده توسط طراحان آن پروتکل هستیم اما این در حالی است که REST صرفاً یکسری استاندارد و اصول است و هر دولوپری میتواند تفسیر شخصی خود را از این اصول داشته باشد اما در عین حال اکثر دولوپرهای علاقمند به توسعهٔ سیستمهای مبتنی بر REST سعی میکنند از یکسری Best Practice که توسط حرفهایهای این حوزه ابداع شدهاند پیروی کنند. همچنین در صورتی که علاقمند به دستیابی به لیستی از ایپیآی شرکتهای مطرح هستید، میتوانید به وبسایت ProgrammableWeb مراجعه نمایید.
به منظور تسهیل فرایند آموزشی، در این سری از آموزشها باکسهایی به صورت زیر مورد استفاده قرار خواهند گرفت تا مخاطبین دوره بهتر بتوانند برخی از نکات مهم این دورهٔ آموزشی را به خاطر بسپارند:
نکته |
نکاتی که منجر به درک بهتر موارد مطروحه میشوند، در قالب باکسهای سبز رنگی به این شکل در اختیار دانشجویان قرار خواهند گرفت. |
هشدار |
برنامهنویسان مبتدی در برخی موارد باید دقت بیشتری به خرج دهند تا در آینده با سردرگمی کمتری مواجه شوند و به همین منظور هم هشدارهای این دورهٔ آموزشی در قالب باکسهای قرمز رنگی به این شکل در معرض دید دانشجویان قرار خواهند گرفت. |
به خاطر داشته باشید |
برخی نکات هستند که مخاطبین این دورهٔ آموزشی باید به خاطر بسپارند چرا که در آموزشهای آتی به آنها نیاز خواهند داشت که کلیهٔ این نکات در قالب باکسهای آبی رنگی به این شکل عرضه خواهند شد. |