ساخت API مدرن با GraphQL، بخش سوم

در بخش‌های قبل (۱ و ۲) گرف‌کیوال رو معرفی کردیم و به کمک جنگو گرافین یک ای‌پی‌آی ساده ساختیم تا با مفاهیم اولیه گرف‌کیوال و گرافین آشنا بشیم. در این بخش توضیح می‌دم چگونه کدها رو لایه بندی کنیم که پروژه‌ی تمیزتر و قابل نگه‌داری داشته باشیم.

قبل از شروع این بخش لازمه مستندات graphql و مستندات graphene رو مطالعه کرده باشید و با مفاهیم‌شون آشنا باشید.

این پروژه در این آدرس در دسترس عموم قرار دارد.

لایه بندی و تفکیک وظایف

اگه بخوایم بی‌خیال و کثیف کد بزنیم، چی کار باید بکنیم؟ یه تابع می‌نویسیم که همه کار توش انجام بشه. ریکوئست توش گرفته می‌شه، جیسون توش پارس می‌شه، هدر خونده می‌شه و تشخیص می‌ده که کاربر لاگین هست یا نه، چند تا شرط رو چک می‌کنه که ببینه کاربر دسترسی داره یا نه، یه چند تا کوئری هم به دیتا بیس می‌زنه، شاید یه چهار خط اون وسط چند تا منطق کسب و کار (Business logic) هم اعمال کنه، آبجکت جیسون رو سریالایز کنه و بنویسه در جواب ریکوئست، ناقابل چهارصد خط، تست هم که نداره، اصلا ما اهل این سوسول بازی‌ها نیستیم، تازه بخوایم تست هم بنویسیم نمی‌شه، پس به جد بی‌خیال تست.

برای اینکه این اتفاق نیفته، لازمه که کدها لایه بندی مناسب داشته باشند، لایه بندی کد به ما کمک می‌کنه تا هر لایه رو بتونیم به طور مستقل مورد توجه قرار بدیم و در هر لایه تنها دغدغه‌های همون لایه رو داشته باشیم و حداکثر لازمه که interface لایه‌های دیگه رو مد نظر قرار بدیدم.

یه لایه بندی خوب، برای ما تعریف می‌کنه که هر لایه چه وظایفی داره، چه فرضیاتی داره و مهم‌تر از اون چه فرضیاتی نداره، چه لایه‌ای رو می‌شناسه و چه لایه‌ای رو نباید بشناسه.

لایه‌های جنگو و گرافین

وقتی داریم با گرفین یک ای‌پی‌آی گرف‌کیوال پیاده می‌کنیم، اکثر کدهای ما در این دو دسته می‌گنجه: کدهای تعریف تایپ و کدهای تعریف resolver. کدهای تعریف تایپ به صورت اعلامی (declarative) هستند و اکثر کدهای دارای منطق ما در بخش resolver ها اجرا می‌شن. خوبه قبل از اینکه بریم و بخوایم لایه بندی‌های کد خودمون رو مشخص کنیم، ببینیم در اطرافمون و در جنگو و گرافین چه لایه‌هایی وجود داره و به چه لایه‌هایی دسترسی داریم.

وقتی یه درخواست از کاربر به اپ جنگوی ما می‌رسه، ابتدا جنگو مواردی مثل پارس کردن ورودی رو انجام می‌ده. البته جنگو صرفا مواردی مثل json و query param و form data رو می‌شناسه و گرف‌کیوال رو نمی‌شناسه. همچنین مواردی مثل اینکه درخواست دهنده چه کاربری هستش رو از روی مواردی مثل کوکی‌ها تشخیص داده و از طریق request.user در اختیار ما قرار داده. خود این کارهای در داخل جنگو در چندین لایه مجزا انجام می‌شه، ولی فعلا همین که بدونیم به صورت کلی این کارها قبل از رسیدن درخواست کاربر به دست ما توسط جنگو انجام می‌شه، کافیه

در لایه‌ی بعدی، گرفین وجود داره که موضوعات مربوط به گرف‌کیوال رو می‌شناسه. بلده کوئری گرف‌کیوال رو پارس بکنه، ورودی‌های کوئری بگیره و صحت‌سنجی و تبدیل کنه و resolver متناسب با کوئری رو صدا بزنه و در ادامه خروجی رو بگیره و صحت سنجی کنه و بدست جنگو بسپاره تا به عنوان جواب درخواست به کاربر برگردونده بشه.

لایه‌ی دیگه که جنگو در اختیار ما قرار داده، لایه ORM هستش که به ما کمک می‌کنه بتونیم داده‌ها رو در پایگاه‌داده ذخیره و بازیابی کنیم.

احراز هویت و مجاز شماری

یکی از مسائلی که تقریبا تمام سرویس‌ها با اون مواجه هستند، دو مسئله‌ی «احراز هویت» و «مجاز شماری» است. خوبه اول این دو مورد رو تعریف کنیم:

احراز هویت: احراز هویت یا Authentication قراره این سوال رو جواب بده، کاربر چه کسی هستش؟

مجاز شماری: مجاز شماری یا Authorization قراره این سوال رو جواب بده که کاربر حق داره چه کارهایی انجام بده؟

این دو موضوع رو کجای کد باید اعمال کنیم؟

همون‌طور که قبل‌تر گفته شده، احراز هویت توسط جنگو انجام شده. ولی منطق مربوط به مجاز شماری، کاملا به کسب و کار ما بر می‌گرده و این ما هستیم که باید کدش رو بزنیم. کدهای مجاز شماری رو کجا بزنیم؟ وسط کدهای resolver گرف‌کیوال خوبه؟ نه.

فرض کنیم ما بجز گرف‌کیوال ممکن است روش‌های ارتباطی دیگه‌ای مثل rest یا cli یا grpc یا تمپلیت‌های جنگو داریم. اگه ما کدهای مجاز شماری رو داخل resolver گرف‌کیوال بنویسیم، این منطق مجاز شماری تنها روی گرف‌کیوال اعمال می‌شه و مثلا روی کدهای رِست اعمال نمی‌شه. یا اگه بخوای اعمال بشه، لازمه کدهای مشابه زده بشه و معلوم هم نیست با چه مکانیسمی قراره این منطق‌ها با هم سینک بمونند و احتمالا بعد از یه مدت می‌بینیم منطق اعمال مجاز شماری بین روش‌های ارتباطی مختلف، فرق کرده و تفاوت رفتار دارند. مثلا کاربر از طریق گرف‌کیوال تنها پست‌های منتشر شده رو می‌تونه ببینه، ولی از طریق رِست همه‌ی پست‌ها رو می‌بینه در حالی که نباید این اتفاق می‌افتاد.

اینکه یه کاربر حق داره چه کارهایی بکنه و چه چیزهایی رو ببینه ربطی به این نداره که این کاربر از چه طریقی به اپ ما وصل شده.

روش نامناسب اعمال مجاز شماری منبع
روش نامناسب اعمال مجاز شماری منبع

خب پس باید چی کار کنیم؟

لایه منطق کسب و کار

جواب اینه که باید یه لایه داشته باشیم که منطق کسب و کار ما بر عهده‌ی اون لایه باشه و بخشی از وظایف لایه کسب و کار، اعمال قواعد مجاز شماری هستش. به این ترتیب در لایه‌ی گرف‌کیوال و در کدهای resolver که ما می‌نویسیم، نباید اثری از کدهای مجازشماری و کدهای منطق کسب و کار وجود داشته باشه.در غیر این صورت مجبور می‌شیم که این کدها رو در روش‌های ارتباطی دیگه مثل رِست هم کپی کنیم.

روش مناسب اعمال مجاز شماری منبع
روش مناسب اعمال مجاز شماری منبع

لایه کسب و کار قرار نیست ذره‌ای در مورد پروتکل‌های http و گرف‌کیوال و ... بدونه، این لایه باید منطق خالص باشه. و تنها حق داره لایه‌ی داده رو بشناسه. خود من وقتی با جنگو کد می‌زنم، اجازه می‌دم لایه کسب و کار، ORM جنگو رو به عنوان لایه‌ی دیتا بشناسه و از اون استفاده کنه، ولی به هیچ وجه مواردی مثل http توی کدهای این لایه نیست.

سوالی که اینجا پیش می‌آد اینه که پس لایه گرف‌کیوال اینجا چی کار می‌کنه؟ با این روش، لایه گرف‌کیوال یه لایه‌ی بسیار نازک می‌شه که یه وظایف بیشتر نداره: صدا کردن تابع مناسب از لایه‌ی کسب و کار و پاس دادن ورودی‌های گرفته شده از کاربر به لایه‌ی کسب و کار. در صورت لزوم ممکن است برای این که ورودی‌ها مناسب لایه‌ی کسب و کار بشه، یه تبدیل روی ورودی‌ها انجام بده. ولی باید مراقب باشیم هیچ بخشی از مجاز شماری و یا منطق کسب و کار در لایه گرف‌کیوال نباشه. در حالت ایده‌آل هیچ عبارت شرطی مثل if در لایه‌ی گرف‌کیوال و resolver ها وجود ندار.

در ادامه یه کم دست به کد می‌شیم.

مثال عملی

فرض بگیریم یه تعداد پست داریم، که برخی از این‌ها در وضعیت پیش‌نویس قرار دارند و برخی هم منتشر شده‌اند. قرار است همه‌ی کاربران بتوانند پست‌های منتشر شده را ببینند ولی پست‌های پیش‌نویس تنها توسط نویسنده قابل مشاهده می‌باشد.

منطق کسب و کار داخل resolver
منطق کسب و کار داخل resolver


در نگاه اول، این کد داره کار می‌کنه و همه چی خوبه، ولی نکته اینجا است که ممکنه کد مشابهی هم در بخش‌های مربوط به رِست و یا view های جنگو زده شده باشه. ولی اگه فردا روزی لازم بشه این منطق عوض بشه،‌ اون وقت لازمه که بریم چند جا رو به صورت مشابه تغییر بدیم و حتی ممکنه بعضی موارد رو فراموش کنیم و رفتار رِست و گرف‌کیوال متفاوت بشه.

برای اصلاح، همون طور که بالاتر بحث شد، یک لایه‌ی جدید باید ایجاد کنیم که منطق‌های مخصوص کسب و کار در اون بخش نوشته شده باشه. من اسم این لایه رو interactor گذاشتم. رویه‌ی من در جنگو این طور بوده که interactor ها تنها حق دارن آبجکت‌های مربوط به مدل رو بشناسن و از مواردی مثل http و graphql نباید خبر داشته باشن. یک interactor می‌تونه یک تابع و یا یک کلاس باشه. در این مورد ساده، من یک تابع تعریف می‌کنم. (اسم interactor از این ارائه‌ی uncle bob گرفته شده)

تابع interactor
تابع interactor
تعریف resolver با استفاده از interactor و بدون پیاده‌سازی مستقیم منطق کسب و کار
تعریف resolver با استفاده از interactor و بدون پیاده‌سازی مستقیم منطق کسب و کار


در لایه‌ی گرف‌کیوال و کد resolver دیگه اثری از کدهای مربوط به منطق کسب و کار ما وجود نداره و در کد resolver تنها تابع interactor استفاده شده. این تابع interactor می‌تونه به صورت مشترک بین رِست و دیگر روش‌های ارتباطی به صورت مشترک استفاده بشه و در صورتی که لازم شد منطق کسب و کار ما تغییر کنه، فقط اون تابع interactor تغییر می‌کنه.

چرا به جای user متغییر info ویا info.context رو به عنوان ورودی به تابع interactor پاس ندیم؟ چون info یه آبجکت مربوط به graphql هستش و info.context هم یک آبجکت مربوط به http هستش و قرار نیست لایه منطق کسب و کار ما از این موضوعات خبر داشته باشند. مثلا اگه info ورودی بود، دیگه بخش مربوط به رِست نمی‌تونست از این تابع استفاده کنه. اگه info.context رو پاس می‌دادیم، بخش مربوط به rpc دیگه نمی‌تونست از این تابع استفاده کنه. ولی user مربوط به لایه‌ی ORM هستش و بنابر این interactor حق داره درباره‌اش بدونه.

تغییر شاید کوچک به نظر برسه، ولی به شدت در تمیزی کد و نگه‌داشت‌پذیری کد (maintainability) کمک کننده هستش، مخصوصا اگه چند کانال ارتباطی مثل رِست و گرف‌کیوال به صورت همزمان وجود داشته باشه و یا اینکه منطق‌های کسب و کار پیچیده باشه، این موضوع بیشتر آشکار می‌شه. در این آدرس می‌تونید نمونه‌ی interactor ها رو در یک پروژه‌ی واقعی ببینید که به نسبت پیچیده‌تر هستند و تعدادشون هم اون‌قدر زیاد هستش که به جای یک فایل، یک پوشه interactors داریم.

نکته‌ی جذاب دیگه این هستش که می‌تونیم کدهای مربوط به منطق کسب و کار رو مستقل از بقیه لایه‌ها تست کنیم:

تست منطق کسب و کار
تست منطق کسب و کار

جمع بندی

در این بخش بررسی کردیم که با اضافه کردن یک لایه‌ی کسب و کار می‌تونیم کد را تمیزتر کنیم و نگه‌داشت‌پذیری کد را افزایش بدیم.

در بخش بعدی در این مورد صحبت خواهیم کرد که چطور می‌تونیم تا جای ممکن اپ‌های جنگو رو از هم مستقل نگه داریم تا قابلیت استفاده‌ی مجدد از اون‌ها بالا بره.