جهنم callback ها واقعی است. توسعهدهندگان اغلب callback ها را بدترین چیز ممکن میدانند،و سعی می کنند تا حد ممکن از آنها اجتناب میکنند. انعطاف پذیری جاوا اسکریپت به هیچ وجه در این مورد کمکی نمی کند. اما لازم نیست از callback ها اجتناب کنید. خبر خوب این است که مراحل ساده ای برای نجات از جهنم callback ها وجود دارد.
حذف callback در کدهای شما مانند قطع کردن یک پای خوب است. تابع callback یکی از ویژگی های اصلی جاوا اسکریپت و یکی از بخش های خوب آن است. هنگامی که callback را جایگزین میکنید، اغلب فقط مشکلات را تعویض میکنید و عملا تغییر وجود ندارد.
برخی از برنامه نویس ها می گویند callback ها بسیار زشت هستند و این دلیلی است برای مطالعه زبان جاوا اسکریپت. خب، آیا واقعا callback اینقدر زشت هستند؟
استفاده از callback در جاوا اسکریپت مجموعهای از ویژگی های خوب خاص خود را دارد. دلیلی برای اجتناب از جاوا اسکریپت وجود ندارد زیرا callback میتوانند به آن جهنم زشت تبدیل شوند. ما فقط میتوانیم مطمئن شویم که این اتفاق نمیافتد و یک سری اصول را رعایت می کنیم تا رخ ندهد.
بیایید به آنچه برنامه نویسی callback ارائه می دهد عمیق تر شویم. ترجیح ما این است که به اصول SOLID پایبند باشیم و ببینیم که این ما را به کجا می برد.
Callback Hell چیست؟
ممکن است از خود بپرسید کال بک چیست و چرا باید به آن اهمیت دهید. در جاوا اسکریپت، callback ها تابعی هستند که به عنوان یک نماینده (delegate) عمل می کند. نماینده در یک لحظه دلخواه در آینده اجرا می کند. در جاوا اسکریپت، تفویض اختیار زمانی اتفاق میافتد که تابع دریافت کننده تماس برگشتی را فراخوانی کند. تابع دریافت کننده ممکن است این کار را در هر نقطه دلخواه در اجرای خود انجام دهد. یکی از مهم ترین کاربرد های آن نیز برای تعریف توابعی به عنوان event handler می باشد.
به طور خلاصه، callback تابعی است که به عنوان آرگومان به تابع دیگری ارسال می شود. هیچ اجرای فوری وجود ندارد، زیرا تابع دریافت تصمیم می گیرد که چه زمانی آن را فراخوانی کند. نمونه کد زیر نشان می دهد:
function receiver(fn) {
return fn();
}
function callback() {
return 'foobar';
}
var callbackResponse = receiver(callback);
// callbackResponse == 'foobar'
اگر تا به حال درخواست Ajax نوشته اید، با توابع callback مواجه شده اید. کد ناهمزمان (آنسکرون) از این روش استفاده میکند، زیرا هیچ تضمینی وجود ندارد که چه زمانی callback اجرا میشود.
مشکل callback ناشی از داشتن کد همگامسازی است که به callback دیگری بستگی دارد، زیرا به ترتیب اجرا می شوند. میتوانیم از setTimeout
برای شبیهسازی تماسهای async با توابع callback استفاده کنیم.
برنامه نویس های جاوا اسکریپت به کد پایینی هرم عذاب می گویند. چون زمانی این جهنم کال بک ها رخ می دهد که ما از callback های تو در تو استفاده کرده باشیم!
setTimeout(function (name) {
var catList = name + ',';
setTimeout(function (name) {
catList += name + ',';
setTimeout(function (name) {
catList += name + ',';
setTimeout(function (name) {
catList += name + ',';
setTimeout(function (name) {
catList += name;
console.log(catList);
}, 1, 'Lion');
}, 1, 'Snow Leopard');
}, 1, 'Lynx');
}, 1, 'Jaguar');
}, 1, 'Panther');
با نگاهی به کد بالا، setTimeout
یک تابع callback دریافت می کند که پس از یک میلی ثانیه اجرا می شود. آخرین پارامتر فقط callback را با داده تغذیه می کند. این مانند یک تماس Ajax است، با این تفاوت که پارامتر نام بازگشتی از سرور می آید.
در کد خود، ما لیستی از گربه های وحشی را از طریق کد asynchronous جمع آوری می کنیم. هر callback یک نام گربه را به ما می دهد و ما آن را به لیست اضافه می کنیم. آنچه ما برای رسیدن به آن تلاش می کنیم معقول به نظر می رسد. اما با توجه به انعطافپذیری توابع جاوا اسکریپت، این یک کابوس است.
راه حل هایی برای نجات از جهنم callback
چهار راه حل برای بازگشت به جهنم وجود دارد:
کامنت ها را بنویسید
توابع را به توابع کوچکتر تقسیم کنید
استفاده از پرومیس ها
استفاده از Async/wait
بیاید هر کدام از راه حل ها را با هم بررسی کنیم.
توابع ناشناس
به استفاده از توابع ناشناس در مثال قبلی توجه کنید. توابع ناشناس تابع بی نامی هستند که به یک متغیر اختصاص داده می شوند یا به عنوان آرگومان به توابع دیگر ارسال می شوند.
استفاده از توابع ناشناس در کدها توسط برخی از استانداردهای برنامه نویسی توصیه نمی شود. بهتر است آنها را نامگذاری کنید، بنابراین به جای {}function name
از تابع {}getCat(name)
استفاده کنید. قرار دادن نام در توابع به وضوح برنامه های شما می افزاید. این توابع ناشناس به راحتی تایپ می شوند، اما شما رابه صورت مستقیم به جهنم فرو می برند. وقتی متوجه شدید که در این جاده پر پیچ و خم فرورفتگی ها در حال رفتن هستید، بهتر است توقف کنید و دوباره فکر کنید.
یک رویکرد ساده لوحانه برای از بین بردن این آشفتگی callback ها این است که از اعلان های تابع استفاده کنیم:
setTimeout(getPanther, 1, 'Panther');
var catList = '';
function getPanther(name) {
catList = name + ',';
setTimeout(getJaguar, 1, 'Jaguar');
}
function getJaguar(name) {
catList += name + ',';
setTimeout(getLynx, 1, 'Lynx');
}
function getLynx(name) {
catList += name + ',';
setTimeout(getSnowLeopard, 1, 'Snow Leopard');
}
function getSnowLeopard(name) {
catList += name + ',';
setTimeout(getLion, 1, 'Lion');
}
function getLion(name) {
catList += name;
console.log(catList);
}
هر تابع اعلان مخصوص به خود را دریافت می کند. یک مزیت این است که ما دیگر به هرم وحشتناک دست پیدا نمی کنیم. هر تابع ایزوله می شود و آن بر روی وظیفه خاص خود متمرکز می شود. اکنون هر تابع یک دلیل برای تغییر دارد، بنابراین گامی در مسیر درست است. توجه داشته باشید که ()getPanther
، به عنوان مثال، به پارامتر اختصاص داده می شود. جاوا اسکریپت اهمیتی نمیدهد که چگونه callback ایجاد میکنیم. اما معایب آن چیست؟
برای تفکیک کامل تفاوتها می توانید این مقاله آنوفل را در مورد عبارات توابع در مقابل توابع اعلانی را ببینید.
با این حال، یک نقطه ضعف این است که هر اعلان تابع دیگر در داخل callback قرار نمی گیرد. به جای استفاده از callback به عنوان بسته، هر تابع اکنون به محدوده بیرونی چسبانده می شود. از این رو چرا catList
در محدوده بیرونی اعلان می شود، زیرا این امکان دسترسی به callback را به لیست می دهد. در مواقعی، کله زدن در محدوده گلوبال راه حل ایده آلی نیست. همچنین کدهای تکراری وجود دارد، زیرا یک گربه را به لیست اضافه می کند و callback بعدی را فراخوانی می کند.
اینها بوهای کدی هستند که از جهنم callback به ارث رسیده اند. گاهی اوقات، تلاش برای ورود به آزادی callback نیاز به پشتکار و توجه به جزئیات دارد. ممکن است احساس شود که بیماری بهتر از درمان است. آیا راهی برای کدنویسی بهتر وجود دارد؟
وارونگی وابستگی
اصل وارونگی وابستگی می گوید ما باید به صورت abstractions کدنویسی کنیم، نه اینکه به صورت جزئیات پیاده سازی کنیم. در اصل، یک مشکل بزرگ را در نظر می گیریم و آن را به وابستگی های کوچک تقسیم می کنیم. این وابستگی ها به جایی که جزئیات پیاده سازی بی ربط هستند مستقل می شوند.
این اصل SOLID می گوید:
هنگام پیروی از این اصل، روابط وابستگی مرسوم ایجاد شده از ماژولهای سطح بالا و تنظیم خط مشی به ماژولهای وابستگی سطح پایین معکوس میشوند، بنابراین ماژولهای سطح بالا مستقل از جزئیات پیادهسازی ماژول سطح پایین میشوند.
پس این متن به چه معناست؟ خبر خوب این است که با اختصاص دادن یک فراخوان به یک پارامتر، در حال حاضر این کار را انجام می دهیم! حداقل تا حدی، برای جدا شدن از هم، به callback ها به عنوان وابستگی فکر کنید. این وابستگی تبدیل به قرارداد می شود. از این نقطه به بعد، ما برنامه نویسی SOLID را انجام می دهیم. جالب تر شد؟ مگه نه؟
یکی از راههای به دست آوردن آزادی callback، ایجاد یک قرارداد است:
fn(catList);
این مشخص می کند که ما قصد داریم با callback چه کاری انجام دهیم. باید یک پارامتر واحد را دنبال کند که این یعنی لیست ما از گربه های وحشی است.
این وابستگی اکنون می تواند از طریق یک پارامتر تغذیه شود:
function buildFerociousCats(list, returnValue, fn) {
setTimeout(function asyncCall(data) {
var catList = list === '' ? data : list + ',' + data;
fn(catList);
}, 1, returnValue);
}
توجه داشته باشید که عبارت تابع asyncCall
به buildFerociousCats
بسته می شود. این تکنیک زمانی قدرتمند است که در برنامه نویسی async با callback همراه شود.
قرارداد به صورت ناهمزمان اجرا می شود و data
های مورد نیاز را به دست می آورد، همه اینها با برنامه نویسی صدا. قرارداد با جدا شدن از اجرا، آزادی مورد نیاز خود را به دست می آورد. کد زیبا از انعطاف پذیری جاوا اسکریپت به نفع خود استفاده می کند.
بقیه آنچه باید اتفاق بیفتد بدیهی می شود. ما میتونیم این کار را انجام دهیم:
buildFerociousCats('', 'Panther', getJaguar);
function getJaguar(list) {
buildFerociousCats(list, 'Jaguar', getLynx);
}
function getLynx(list) {
buildFerociousCats(list, 'Lynx', getSnowLeopard);
}
function getSnowLeopard(list) {
buildFerociousCats(list, 'Snow Leopard', getLion);
}
function getLion(list) {
buildFerociousCats(list, 'Lion', printList);
}
function printList(list) {
console.log(list);
}
در اینجا هیچ کد تکراری وجود ندارد. callback ها اکنون وضعیت خود را بدون متغیرهای سراسری پیگیری می کند. یک فراخوانی مانند getLion
میتواند با هر چیزی که به دنبال قرارداد باشد - یعنی هر abstraction که فهرستی از گربههای وحشی را به عنوان پارامتر در نظر میگیرد، زنجیر شود.
callback های چند شکلی
خوب، بیایید کمی دیوانه بازی در بیاریم. اگر بخواهیم رفتار را از ایجاد یک لیست جدا شده با کاما به یک لیست محدود شده با pipe تغییر دهیم، چه؟ یکی از مشکلاتی که می توانیم متصور شویم این است که buildFerociousCats
به جزئیات پیاده سازی چسبانده شده است. به استفاده از list + ',' + data
برای این کار توجه کنید.
پاسخ ساده، رفتار چندشکلی یا همان Polymorphic با callback است. اصل باقی می ماند: با فراخوان ها مانند یک قرارداد رفتار کنید و اجرا را بی ربط کنید. هنگامی که callback به یک abstraction ارتقا یابد، جزئیات خاص می توانند به دلخواه تغییر کنند.
چند شکلی راه های جدیدی برای استفاده مجدد از کد در جاوا اسکریپت باز می کند. به callback چند شکلی به عنوان راهی برای تعریف یک قرارداد سخت فکر کنید، در حالی که اجازه می دهد آزادی کافی برای جزئیات پیاده سازی دیگر مهم نباشد. توجه داشته باشید که ما هنوز در مورد وارونگی وابستگی صحبت می کنیم. یک کال بک چند شکلی فقط یک نام فانتزی است که به یک راه برای پیشبرد این ایده اشاره می کند.
بیایید قرارداد را تعریف کنیم. ما می توانیم از list
و پارامترهای data
در این قرارداد استفاده کنیم:
cat.delimiter(cat.list, data);
سپس می توانیم چند ترفند برای buildFerociousCats
ایجاد کنیم:
function buildFerociousCats(cat, returnValue, next) {
setTimeout(function asyncCall(data) {
var catList = cat.delimiter(cat.list, data);
next({ list: catList, delimiter: cat.delimiter });
}, 1, returnValue);
}
اکنون آبجکت cat
جاوا اسکریپت داده های list
و تابع delimiter
را کپسوله می کند. زنجیرههای callback بعدی، callback را همگامسازی میکنند - که قبلاً fn
نامیده میشد. توجه داشته باشید که آزادی گروه بندی پارامترها به دلخواه با یک شی جاوا اسکریپت وجود دارد.آبجکت cat
دو کلید خاص را انتظار دارد که آن ها list
و delimiter
می باشد. این آبجکت جاوا اسکریپت اکنون بخشی از قرارداد است. بقیه کدها ثابت می ماند.
برای روشن کردن آن، می توانیم این کار را انجام دهیم:
buildFerociousCats({ list: '', delimiter: commaDelimiter }, 'Panther', getJaguar);
buildFerociousCats({ list: '', delimiter: pipeDelimiter }, 'Panther', getJaguar);
callback ها عوض می شوند. تا زمانی که قراردادها محقق شود، جزئیات اجرا بی ربط است. ما می توانیم به راحتی رفتار را تغییر دهیم.callback هایی، که اکنون یک وابستگی است، به یک قرارداد سطح بالا وارونه می شود. این ایده آنچه را که قبلاً درباره callback میدانیم، میگیرد و آن را به سطح جدیدی ارتقا میدهد. کاهش callback به قراردادها، abstractions را بالا میبرد و ماژولهای نرمافزار را از هم جدا میکند.
آنچه در اینجا بسیار رادیکال است این است که تست های واحد به طور طبیعی از ماژول های مستقل سرچشمه می گیرند. قرارداد delimiter
یک تابع خالص است. این بدان معناست که با توجه به تعدادی ورودی، هر بار خروجی یکسانی دریافت می کنیم. این سطح از تست پذیری اطمینان حاصل می کند که راه حل کار خواهد کرد. به هر حال، استقلال ماژولار حق خودارزیابی را اعطا می کند.
یک تست واحد موثر در اطراف delimiter
pip ممکن است چیزی شبیه به این باشد:
describe('A pipe delimiter', function () {
it('adds a pipe in the list', function () {
var list = pipeDelimiter('Cat', 'Cat');
assert.equal(list, 'Cat|Cat');
});
});
به شما اجازه میدهم تصور کنید که جزئیات پیادهسازی چگونه است.
Promise
یک promise صرفاً یک پوشش در اطراف مکانیسم callback است و امکان ادامه جریان اجرا را به فرد امکان می دهد. این باعث می شود کد قابل استفاده مجدد باشد زیرا می توانید یک پرومیس را برگردانید و پرومیس را زنجیره ای کنید.
بیایید در بالای callback چند شکلی بسازیم و این را حول یک promise ببندیم. تابع buildFerociousCats
را تغییر دهید و آن را به یک promise تبدیل کنید:
function buildFerociousCats(cat, returnValue, next) {
return new Promise((resolve) => { //wrapper and return Promise
setTimeout(function asyncCall(data) {
var catList = cat.delimiter(cat.list, data);
resolve(next({ list: catList, delimiter: cat.delimiter }));
}, 1, returnValue);
});
}
به استفاده از resolve
توجه کنید: به جای استفاده مستقیم از callback، این چیزی است که promise را حل می کند. کد مصرف کننده می تواند یک و سپس برای ادامه جریان اجرا اعمال کند.
از آنجایی که ما اکنون یک promise را برمی گردانیم، کد باید promise را در اجرای callback پیگیری کند.
بیایید توابع callback را به روز کنیم تا promise را برگردانیم:
function getJaguar(cat) {
return buildFerociousCats(cat, 'Jaguar', getLynx); // Promise
}
function getLynx(cat) {
return buildFerociousCats(cat, 'Lynx', getSnowLeopard);
}
function getSnowLeopard(cat) {
return buildFerociousCats(cat, 'Snow Leopard', getLion);
}
function getLion(cat) {
return buildFerociousCats(cat, 'Lion', printList);
}
function printList(cat) {
console.log(cat.list); // no Promise
}
آخرین callback، پرومیس های زنجیره ای نمی دهد، زیرا promise یی برای بازگشت ندارد. پیگیری promise ها برای تضمین تداوم در پایان مهم است. از طریق قیاس، وقتی promise می دهیم، بهترین راه برای promise این است که به یاد داشته باشیم که زمانی آن promise را داده ایم.
حالا بیایید callback اصلی را با یک فراخوانی تابع قابل به روز رسانی کنیم:
buildFerociousCats({ list: '', delimiter: commaDelimiter }, 'Panther', getJaguar)
.then(() => console.log('DONE')); // outputs last
اگر کد را اجرا کنیم، خواهیم دید که "DONE" در پایان چاپ می شود. اگر فراموش کنیم یک promise را در جایی در جریان برگردانیم، «DONE» نامرتب ظاهر میشود، زیرا مسیر promise اصلی را از دست میدهد.
Async/Await
در نهایت، میتوانیم async/wait را به عنوان قند حول یک promise در نظر بگیریم. برای جاوا اسکریپت، async/wait در واقع یک promise است، اما برای برنامه نویس بیشتر شبیه کد همزمان میخواند.
از کدی که تاکنون در اختیار داریم، بیایید از شر آن خلاص شویم وthen
سپس callback را در اطراف async/wait ببندیم:
async function run() {
await buildFerociousCats({ list: '', delimiter: pipeDelimiter }, 'Panther', getJaguar)
console.log('DONE');
}
run().then(() => console.log('DONE DONE')); // now really done
خروجی "DONE" بلافاصله پس از await
اجرا می شود، زیرا بسیار شبیه به کدهای همزمان عمل می کند. تا زمانی که فراخوانی به buildFerociousCats
یک promise را برمی گرداند، می توانیم منتظر callback باشیم.async
تابع را بهعنوان تابعی که یک promise را برمیگرداند علامتگذاری میکند، بنابراین همچنان میتوان callback را به run
با then
دیگر زنجیره زد. تا زمانی که آنچه را که به آن می خوانیم یک promise را برمی گرداند، می توانیم promise ها را به طور نامحدود زنجیره ای کنیم.
به خاطر داشته باشید که همه این کدهای ناهمزمان در زمینه یک رشته اجرا می شوند. یک فراخوان جاوا اسکریپت به خوبی در این الگوی تک رشته ای قرار می گیرد، زیرا پاسخ به callback به گونه ای در صف قرار می گیرد که اجرای بیشتر را مسدود نمی کند. این کار باعث میشود موتور جاوا اسکریپت ردیابی callback را آسانتر کند و بدون نیاز به همگامسازی چندین رشته، فوراً callback را دریافت کند.
چگونه از جهنم callback نجات پیدا کنیم؟
می توانید از وارونگی وابستگی و callback های چند شکلی یا promise ها و یا async/wait استفاده کنید تا از جهنم callback ها رهایی پیدا کنید. همچنین ما از اصول SOLID نیز تبعیت خواهیم کرد.
تسلط بر callback در جاوا اسکریپت، درک تمام نکات جزئی است. امیدوارم تغییرات ظریف در توابع جاوا اسکریپت را مشاهده کنید. وقتی که ما اصول اولیه را نداشته باشیم، یک تابع برگشتی به اشتباه درک می شود. هنگامی که توابع جاوا اسکریپت روشن شد، اصول SOLID به زودی دنبال می شود. برای دستیابی به برنامه نویسی SOLID نیاز به درک قوی از اصول اساسی دارد. انعطاف پذیری ذاتی در زبان، بار مسئولیت را بر دوش برنامه نویس می گذارد.
چیزی که من بیشتر از همه دوست دارم این است که جاوا اسکریپت برنامه نویسی خوب را تقویت می کند. درک خوب از همه جزئیات و اصول، ما را در هر زبانی بسیار دور خواهد برد. این رویکرد برای توابع callback در جاوا اسکریپت وانیلی بسیار مهم است. بر حسب ضرورت، تمام گوشه و کنارها مهارت های ما را به سطح بالاتری می برند.
نتیجه
Callback Hell جایی است که نمی خواهید هنگام نوشتن کد جاوا اسکریپت خود را در آن بیابید. این پیچ و خم از Callback های تودرتو است که میتواند حتی با تجربهترین توسعهدهندگان را تا مرز جنون سوق دهد.
اما نترس!
با کمک promise ها و async/await، میتوانید کد خود را از چنگال Callback Hell نجات دهید و کار کردن با آن را خواناتر، قابل نگهداری و لذتبخشتر کنید.
promise ها یا async/await را انتخاب کنید و با کدهای تمیزتر و کارآمدتر از اعماق Callback Hell بیآسیب بیرون خواهید آمد.