برنامه نویسی تابعی (Functional) در جاوا اسکریپت — راهنمای کاربردی
برنامهنویسی تابعی مفهومی فوقالعاده است. در حال حاضر، با معرفی React، بسیاری از کدهای فرانت-اند جاوا اسکریپت با ذهنیتی ناشی از مفاهیم «برنامهنویسی تابعی» نوشته میشوند. اما سؤال این است که چگونه میتوانیم ذهنیتهای برنامهنویسی تابعی را در کدهای روزمره خود پیادهسازی کنیم؟
بیان مسئله
تصور کنید یک کاربر به صفحه login/ ما میآید و به طور اختیاری یک پارامتر کوئری redirect_to دارد. برای مثال لینک وی به صورت login?redirect_to=%2Fmy-page/ است. دقت کنید که %2Fmy-page در واقع همان my-page/ است که در URL کدگذاری شده است. ما باید این رشته کوئری را استخراج کنیم و آن را در یک متغیر محلی طوری ذخیره کنیم که وقتی عمل login صورت گرفت، کاربر بتواند به صفحه my-page ریدایرکت کند.
گام 0: رویکرد دستوری
اگر ما میخواستیم راهحل را به سادهترین وجه و با استفاده از صادر کردن یک فهرست از دستورها حل کنیم، آن را چگونه مینوشتیم؟ در این صورت ما به موارد زیر نیاز داشتیم؟
- تجزیه رشته کوئری
- دریافت مقدار redirect_to
- کدگشایی از این مقدار
- ذخیرهسازی مقدار کدگشایی شده در یک متغیر محلی
همچنین باید بلوکهای try catch را پیرامون تابع «unsafe» نیز قرار دهیم. بدین ترتیب بلوک کد ما چیزی شبیه زیر خواهد بود:
1function persistRedirectToParam() {
2 let parsedQueryParam;
3
4 try {
5 parsedQueryParam = qs.parse(window.location.search); // https://www.npmjs.com/package/qs
6 } catch (e) {
7 console.log(e);
8 return null;
9 }
10
11 const redirectToParam = parsedQueryParam.redirect_to;
12
13 if (redirectToParam) {
14 const decodedPath = decodeURIComponent(redirectToParam);
15
16 try {
17 localStorage.setItem("REDIRECT_TO", decodedPath);
18 } catch (e) {
19 console.log(e);
20 return null;
21 }
22
23 return decodedPath;
24 }
25
26 return null;
27}
گام 1: نوشتن همه مراحل به صورت تابع
برای لحظهای بلوکهای try catch را فراموش کنید و تلاش کنید همه چیز را به صورت یک تابع بیان کنید:
1// let's declare all of the functions we need to have
2
3const parseQueryParams = (query) => qs.parse(query);
4
5const getRedirectToParam = (parsedQuery) => parsedQuery.redirect_to;
6
7const decodeString = (string) => decodeURIComponent(string);
8
9const storeRedirectToQuery = (redirectTo) => localStorage.setItem("REDIRECT_TO", redirectTo);
10
11function persistRedirectToParam() {
12 // and let's call them
13
14 const parsed = parseQueryParams(window.location.search);
15
16 const redirectTo = getRedirectToParam(parsed);
17
18 const decoded = decodeString(redirectTo);
19
20 storeRedirectToQuery(decoded);
21
22 return decoded;
23}
زمانی که شروع به بررسی همه خروجیها به صورت مقادیر بازگشتی یک تابع بکنیم، میبینیم که میتوانیم بدنه تابع اصلی خودمان را بازنویسی بکنیم. زمانی که این اتفاق رخ میدهد، درک تابع ما بسیار آسانتر و تست کردن آن نیز سادهتر میشود.
در بخشهای پیشین، ما باید تابع اصلی خود را به صورت یک مجموعه کلی تست میکردیم؛ اما اینک 4 تابع کوچکتر داریم و برخی از آنها صرفاً واسطههایی برای تابعهای دیگر هستند و از این رو آن چه باید تست شود نیز کاهش یافته است.
در ادامه این تابعهای واسطه را پیدا کرده و آنها را حذف میکنیم و بدین ترتیب کد کمتری خواهیم داشت:
1const getRedirectToParam = (parsedQuery) => parsedQuery.redirect_to;
2
3const storeRedirectToQuery = (redirectTo) => localStorage.setItem("REDIRECT_TO", redirectTo);
4
5function persistRedirectToParam() {
6 const parsed = qs.parse(window.location.search);
7
8 const redirectTo = getRedirectToParam(parsed);
9
10 const decoded = decodeURIComponent(redirectTo);
11
12 storeRedirectToQuery(decoded);
13
14 return decoded;
15}
گام 2: تلاش برای ترکیب تابعها
اینک میبینیم که تابع persistRedirectToParams یک ترکیب از 4 تابع دیگر است. در ادامه بررسی میکنیم که آیا میتوانیم این تابع را به صورت یک ترکیب بنویسیم و بدین ترتیب نتایج درونی را که در const به نام s ذخیره میشوند را حذف کنیم.
1const getRedirectToParam = (parsedQuery) => parsedQuery.redirect_to;
2
3// we have to re-write this a bit to return a result.
4const storeRedirectToQuery = (redirectTo) => {
5 localStorage.setItem("REDIRECT_TO", redirectTo)
6 return redirectTo;
7};
8
9function persistRedirectToParam() {
10 const decoded = storeRedirectToQuery(
11 decodeURIComponent(
12 getRedirectToParam(
13 qs.parse
14 )
15 )
16 )(window.location.search)
17
18 return decoded;
19}
این وضعیت خوبی است، اما ممکن است کسی که آن را میخواند، یک تابع تو در تو را تصور کند. اگر این شلوغی را نیز بتوانیم رفع کنید بسیار مناسب خواهد بود.
گام 3: یک ترکیب خواناتر
اگر با redux یا recompose آشنایی داشته باشید احتمالاً تاکنون با compose مواجه شدهاید. compose یک تابع کاربردی است که چندین تابع میپذیرد و یک تابع بازگشت میدهد. compose تابعهای تشکیلدهنده خود را یک به یک فراخوانی میکند. با توجه به این که منابع مناسب دیگری برای یادگیری compose وجود دارند در این نوشته وارد جزئیات آن نمیشویم. کد ما با استفاده از compose به صورت زیر درمیآید:
1const compose = require("lodash/fp/compose");
2const qs = require("qs");
3
4const getRedirectToParam = (parsedQuery) => parsedQuery.redirect_to;
5
6const storeRedirectToQuery = (redirectTo) => {
7 localStorage.setItem("REDIRECT_TO", redirectTo)
8 return redirectTo;
9};
10
11function persistRedirectToParam() {
12 const op = compose(
13 storeRedirectToQuery,
14 decodeURIComponent,
15 getRedirectToParam,
16 qs.parse
17 );
18
19 return op(window.location.search);
20}
یک نکته در مورد compose این است که تابعها را به صورت راست به چپ اجرا میدهد. بدین ترتیب نخستین تابعی که در زنجیره compose فراخوانی شود همان تابع آخر است.
اگر یک ریاضیدان باشید و با مفهوم قضیه نیز آشنایی داشته باشید، این وضعیت مشکلی ایجاد نخواهد کرد، چون به طور طبیعی آن را راست به چپ میخوانید. اما برای بقیه افراد که به کدهای دستوری آشنایی ندارند، باید آن را طوری بنویسیم که از چپ به راست بخوانند.
گام 4: pipe کردن و مسطحسازی
برای حل مشکلی در که در گام قبل اشاره کردیم، مفهومی به نام pipe وجود دارد. pipe چیزی است که همان کار compose را، اما این بار در جهت عکس انجام میدهد. بنابراین نخستین تابع در زنجیره، نخستین تابعی است که نتیجه را پردازش خواهد کرد.
همچنین این طور به نظر میرسد که تابع persistRedirectToParams ما به پوششی برای تابع دیگر به نام op تبدیل شده است. به بیان دیگر تنها کاری که این تابع انجام میدهد، اجرای op است. ما میتوانیم با مسطح سازی تابع خودمان (flattening) از این وضعیت رهایی بیابیم.
1const pipe = require("lodash/fp/pipe");
2const qs = require("qs");
3
4const getRedirectToParam = (parsedQuery) => parsedQuery.redirect_to;
5
6const storeRedirectToQuery = (redirectTo) => {
7 localStorage.setItem("REDIRECT_TO", redirectTo)
8 return redirectTo;
9};
10
11const persistRedirectToParam = fp.pipe(
12 qs.parse,
13 getRedirectToParam,
14 decodeURIComponent,
15 storeRedirectToQuery
16)
17
18// to invoke, persistRedirectToParam(window.location.search);
بدین ترتیب به نتیجه دلخواه رسیدهایم. به خاطر داشته باشید که ما به سهولت بلوک try-catch خود را کنار گذاردیم تا به وضعیت کنونی برسیم. اما اینک مجبور هستیم آن را به نحوی مجدداً وارد کنیم، چون qs.parse نیز مانند storeRedirectToQuery ناامن است. یک گزینه این است که آنها را به تابعهای پوششی تبدیل کرده و آنها را در بلوکهای try-catch قرار دهیم. روش دیگر که با ذهنیت برنامهنویسی تابعی مطابقت بیشتری دارد، آن است که آن را به صورت یک تابع try-catch بیان کنیم.
گام 5: مدیریت استثنا به صورت یک تابع
برخی ابزارهای خاص به این منظور وجود دارند؛ اما سعی میکنیم خودمان یک تابع بنویسیم.
1function tryCatch(opts) {
2 return (args) => {
3 try {
4 return opts.tryer(args);
5 } catch (e) {
6 return opts.catcher(args, e);
7 }
8 };
9}
تابع ما یک شیء opts میگیرد که شامل تابعهای tryer و catcher است. این تابع یک تابع دیگر بازگشت میدهد و هنگامی که با آرگومانها فراخوانی شود، tryer را با آرگومانهای مذکور فراخوانی کرده. همچنین در صورت ناموفق بودن catcher را فراخوانی میکند. اینک وقتی که عملیات ناامنی داشته باشیم، میتوانیم آنها را در بخش tryer قرار دهیم و در صورت ناموفق بودن، از این وضعیت نجات یابیم و نتیجه امنی را از بخش section به دست آوریم و حتی خطا را نیز log کنیم.
گام 6: جمعبندی
بدین ترتیب و با جمعبندی همه مواردی پیشگفته، کد نهایی ما چیزی مانند زیر خواهد بود:
1const pipe = require("lodash/fp/pipe");
2const qs = require("qs");
3
4const getRedirectToParam = (parsedQuery) => parsedQuery.redirect_to;
5
6const storeRedirectToQuery = (redirectTo) => {
7 localStorage.setItem("REDIRECT_TO", redirectTo)
8 return redirectTo;
9};
10
11const persistRedirectToParam = fp.pipe(
12 tryCatch({
13 tryer: qs.parse,
14 catcher: () => {
15 return {
16 redirect_to: null, // we should always give back a consistent result to the subsequent function
17 }
18 }
19 }),
20 getRedirectToParam,
21 decodeURIComponent,
22 tryCatch({
23 tryer: storeRedirectToQuery,
24 catcher: () => null, // if localstorage fails, we get null back
25 }),
26)
27
28// to invoke, persistRedirectToParam(window.location.search);
این کد همان چیزی است که کمابیش میخواستیم؛ اما باید مطمئن شویم که قابلیت استفاده مجدد و تست پذیری کد بهبود یافته است و میتوانیم تابعهای «امن» بسازیم.
1const pipe = require("lodash/fp/pipe");
2const qs = require("qs");
3
4const getRedirectToParam = (parsedQuery) => parsedQuery.redirect_to;
5
6const storeRedirectToQuery = (redirectTo) => {
7 localStorage.setItem("REDIRECT_TO", redirectTo);
8 return redirectTo;
9};
10
11const safeParse = tryCatch({
12 tryer: qs.parse,
13 catcher: () => {
14 return {
15 redirect_to: null, // we should always give back a consistent result to the subsequent function
16 }
17 }
18});
19
20const safeStore = tryCatch({
21 tryer: storeRedirectToQuery,
22 catcher: () => null, // if localstorage fails, we get null back
23});
24
25const persistRedirectToParam = fp.pipe(
26 safeParse,
27 getRedirectToParam,
28 decodeURIComponent,
29 safeStore,
30)
31
32// to invoke, persistRedirectToParam(window.location.search);
اینک ما یک پیادهسازی بسیار بزرگتر از تابع داریم که از 4 تابع منفرد تشکیل یافته است که ارتباط زیادی با هم دارند. با این حال این تابعها پیوستگی اندکی با هم دارند، میتوانند به طور مستقل تست شوند، به طور مستقل مورد استفاده مجدد قرار گیرند، در موارد بروز استثنا پاسخگو باشند و کاملاً اعلانی (declarative) هستند.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی
- مجموعه آموزشهای طراحی و توسعه پروژه های وب
- 12 نکته کلیدی برای ارزیابی کتابخانه های جدید جاوا اسکریپت
- چگونه برنامه نویس وب شویم؟ – بخش اول: فرانتاند (FrontEnd)
- بهینهسازی کدهای جاوا اسکریپت در سال 2۰1۸ — راهنمای جامع
==