معمولاً یکی از چالشهایی که دولوپرهای مبتدی با آن مواجه میشوند این است که چگونه میتوان مسئلهای را به روشهای دیگری مثلاً بدون استفاده از دستورهای شرطی، اپراتورهای به اصطلاح Ternary و یا دستورات سوئیچ حل کرد و لازم به یادآوری است که تلاش برای مواجهه با چنین چالشهایی برای ایشان مفید است چرا که منجر به یادگیری نحوۀ تفکر و تحلیل مسائل به گونهای متفاوت شده و به تدریج از ذهنی خلاق و الگوریتمیک برخوردار خواهند شد (جهت آشنایی با مفهوم Ternary Operator میتوانید به مقالۀ آشنایی با اپراتور Null Coalesce در PHP مراجعه نمایید.)
البته لازم به یادآوری است که اجتناب از بهکارگیری دستوراتی همچون if
در کدنویسی صرفاً به منظور افزایش خوانایی کد نبوده و دلیلی منطقی نیز دارا است و آن هم مفهومی میباشد موسوم به Code as Data بدین معنی که عدم استفاده از دستورهای شرطی منجر به نزدیکی ساختار منطقی کد به سینتکس زبان برنامهنویسی مد نظر میشود به طوری که اصطلاحاً Business Logic برنامه را به راحتی میتوان از روی سورسکد و سینتکس آن درک کرد و همچنین دیتای حاصل از اجرای کد روند منطقی برنامه را تعیین میکند که در همین راستا قصد داریم تا در ادامهٔ این آموزش برخی از مسائل برنامهنویسی را به دو روش متفاوت (هم بر اساس دستورهای شرطی و هم بدون استفاده از آنها) و با بهکارگیری زبان برنامهنویسی جاوااسکریپت حل کنیم تا ببینیم کدام یک از این راهحلها منجر به افزایش خوانایی سورسکد میشوند.
مسئلۀ اول: شمارش تعداد اعداد صحیح فرد در یک آرایه
ابتدا آرایهای متشکل از چند عدد صحیح تعریف کرده و در ادامه تعداد اعداد فرد موجود در آن را مشخص میکنیم:
const arrayOfIntegers = [1, 4, 5, 9, 0, -1, 5];
let counter = 0;
arrayOfIntegers.forEach((integer) => {
const remainder = Math.abs(integer % 2);
if (remainder === 1) {
counter++;
}
});
console.log(counter);
همانطور که میبینید، ابتدا آرایهای به نام arrayOfIntegers
تعریف کرده و اعداد فوق را به آن اختصاص دادهایم و با بهکارگیری کیورد const
گفتهایم که مقدار این متغیر صرفاً محدود به بلوک جاری بوده و امکان اختصاص آرایۀ جدید و یا به اصطلاح Declare (تعریف) کردن مجدد آن با مقادیر جدید در بلوک جاری فراهم نشود که این امر منجر به کاهش ایجاد باگ در روند اجرای برنامه خواهد شد.
در ادامه متغیری تحت عنوان counter
تعریف کرده و مقدار اولیۀ صفر را به آن اختصاص دادهایم و با بهکارگیری کیورد let
گفتهایم مقدار این متغیر در داخل بلوک جاری اعتبار داشته اما این در حالی است که امکان تغییر آن در روند اجرای برنامه و در صورت لزوم فراهم است و همانطور که در مثال فوق مشاهده میکنید، مقدار متغیر counter
در طی اجرا تغییر پیدا میکند (جهت آشنایی با این کیوردها در زبان برنامهنویسی جاوااسکریپت توصیه میکنیم به مقالۀ درآمدی بر کلیدواژههای let و const مراجعه نمایید.)
در ادامه، فانکشن از پیش تعریفشدۀ ()forEach
را فراخوانی کرده و در آن گفتهایم هر یک از اعداد آرایۀ فوقالذکر را بر عدد 2 تقسیم کرده و در ادامه باقیماندۀ حاصل از تقسیم را به فانکشن ()abs
میدهیم که یک فانکشن از پیش تعریفشده و متعلق به کلاس Math
میباشد که این وظیفه را دارا است تا کلیهٔ مقادیر را به عدد مثبت تبدیل کند که در این مثال نیز باقیماندۀ تقسیم را در قالب یک عدد مثبت ریترن کرده که در ادامه نتیجه را در متغیری به نام remainder
نگاهداری میکنیم.
اما همانطور که میدانیم، باقیماندۀ حاصل از تقسیم هر عدد بر 2 برابر با 1 و یا 0 میباشد و از همین روی در هر بار تقسیم اعداد آرایه بر عدد 2، مقدار ذخیرهشده در متغیر remainder
را بررسی میکنیم بدین صورت که در ادامه گفتهایم اگر چنانچه مقدار متغیر remainder
برابر با 1 بود یک واحد به متغیر counter
اضافه شود و این حلقه تا زمان پایان یافتن مقادیر آرایه ادامه خواهد یافت و در سطر بعد نیز مقدار نهایی متغیر counter
در خروجی چاپ خواهد شد به طوری که داریم:
5
حال در این مرحله قصد داریم تا مسئلۀ فوق را بدون استفاده از دستورات شرطی پیادهسازی کنیم که برای این منظور داریم:
const arrayOfIntegers = [1, 4, 5, 9, 0, -1, 5];
let counter = 0;
arrayOfIntegers.forEach((integer) => {
const remainder = Math.abs(integer % 2);
counter += remainder;
});
console.log(counter);
همانطور که میبینید، این راهحل تا حدودی مشابه حالت قبلی بوده و تنها تفاوت آن در حذف دستور شرطی است که در آن گفتهایم باقیماندۀ تقسیم هر یک از اعداد آرایه بر عدد 2 به مقدار قبلی متغیر counter
اضافه شود و بنابراین در هر بار تقسیم، مقدار ذخیرهشده در متغیر remainder
با مقدار پیشین متغیر counter
جمع میشود و در شرایطی که عدد مد نظر زوج باشد باقیماندۀ تقسیم برابر با 0 بوده و counter
افزایش نمییابد و چنانچه عدد مذکور فرد باشد باقیماندۀ تقسیم برابر با 1 شده و یک واحد نیز به counter
افزوده میشود و در سطر پایانی هم مقدار نهایی متغیر counter
در خروجی چاپ خواهد شد.
نکتۀ قابلتوجه در پیادهسازی مسئلۀ فوق بدون استفاده از دستورهای شرطی این است که در روش دوم از دیتای حاصل از باقیماندۀ تقسیم بر عدد 2 در پیادهسازی منطق برنامۀ خود استفاده کردهایم که این امر اشاره به مفهومی دارد که در ابتدا بیان شد (Code as Data) و بدین ترتیب میتوان گفت در برخی موارد عدم استفاده از دستورهای شرطی در کدنویسی منجر بدین میشود تا دیتای حاصل از اجرای برنامه قابلیت تغییر در روند کلی سورسکد را در زمان اجرا داشته باشد.
مسئلۀ دوم: پیادهسازی فانکشنی به منظور مشخص کردن وضعیت روز فعلی
در این مسئله قصد داریم تا فانکشنی را پیادهسازی کنیم که با دریافت روزهای هفته به عنوان پارامتر ورودی، یکی از استرینگهای «Weekend» به معنای «آخر هفته» و یا استرینگ «Weekday» به معنای «روز کاری» را در خروجی چاپ کند که در ابتدا مسئله را با استفاده از یک دستور شرطی پیادهسازی میکنیم به طوری که برای این منظور داریم:
const weekendOrWeekday = (inputDate) => {
const day = inputDate.getDay();
if (day === 0 || day === 6) {
return 'weekend';
}
return 'weekday';
};
console.log(weekendOrWeekday(new Date()));
همانطور که در کد فوق میبینید، فانکشنی تحت عنوان ()weekendOrWeekday
تعریف کرده و متغیری به نام inputDate
را به عنوان آرگومان ورودی به آن پاس دادهایم و در ادامه گفتهایم در صورت فراخوانی، فانکشن از پیش تعریفشدۀ ()getDay
روی پارامتر ورودی فراخوانی شده و نتیجه به متغیری به نام day
منتسب شود که در سطر بعد مقدار این متغیر چک میشود بدین صورت که گفتهایم چنانچه مقدار متغیر day
برابر با صفر و یا شش بود استرینگ «Weekend» و در غیر این صورت استرینگ «Weekday» در خروجی ریترن شود و در سطر پایانی هم فانکشن مذکور را فراخوانی کردهایم بدین صورت که در ابتدا آبجکتی از نوع ()Date
را به عنوان پارامتر ورودی به آن پاس دادهایم و در ادامه فانکشن ()getDay
را روی این آبجکت فراخوانی کرده که عدد مربوط به روز جاری از هفته ریترن میشود که این عدد در دستور if
چک شده و در نهایت یکی از دو استرینگ مذکور در خروجی چاپ میشود:
weekday
حال در ادامه سعی میکنیم تا مسئلۀ فوق را بدون استفاده از دستورهای شرطی پیادهسازی کنیم که در همین راستا راهحل پیشین را به صورت زیر ریفکتور خواهیم کرد:
const weekendOrWeekday = (inputDate) => {
const day = inputDate.getDay();
return weekendOrWeekday.labels[day] ||
weekendOrWeekday.labels['default'];
};
weekendOrWeekday.labels = {
0: 'weekend',
6: 'weekend',
default: 'weekday'
};
console.log(weekendOrWeekday(new Date()));
در کد فوق نیز ابتدا فانکشن ()weekendOrWeekday
را تعریف کرده و متغیری به نام inputDate
به عنوان آرگومان ورودی به آن پاس دادهایم و گفتهایم در صورت فراخوانی، فانکشن از پیش تعریفشدۀ ()getDay
روی پارامتر ورودی فراخوانی شود که در نتیجه عدد مربوط به روز جاری در متغیر day
نگاهداری خواهد شد.
در ادامه، مقدار متناظر با عدد ذخیرهشده در این متغیر از آبجکت labels
استخراج شده و در خروجی ریترن میشود بدین صورت که آبجکت labels
یکسری پراپرتی دارا است به طوری که کلیدها در این آبجکت بیانگر عدد مربوط به روز جاری و مقادیر متناظر آن نیز مربوط به یکی از دو استرینگ فوقالذکر میباشد که با ریترن شدن عدد مربوط به روز جاری، فانکشن ()weekendOrWeekday
به این آبجکت مراجعه میکند و چنانچه مقدار متغیر day
برابر با یکی از اعداد صفر و یا شش بود استرینگ «Weekend» و در غیر این صورت استرینگ «Weekday» را به عنوان مقدار متناظر با عدد روز جاری در خروجی ریترن میکند.
در واقع، به جای چک کردن مقدار عدد مربوط به روز جاری در دستور if
اعداد مذکور را در آبجکت labels
نگاهداری میکنیم و بدین ترتیب در هنگام فراخوانی فانکشن ()weekendOrWeekday
آبجکتی از نوع ()Date
به عنوان پارامتر ورودی به آن داده میشود که در ادامه ()getDay
روی این آبجکت فراخوانی شده و عدد مربوط به روز جاری در خروجی ریترن میشود و در ادامه فانکشن ()weekendOrWeekday
بسته به مقدار بازگشتی، یکی از استرینگهای متناظر آن در آبجکت labels
را در خروجی ریترن میکند.
مسئلۀ سوم: پیادهسازی فانکشنی که بسته به نوع پارامتر تَسک خاصی عملی میگردد
در این مسئله قصد داریم تا فانکشنی را پیادهسازی کنیم که متناسب با به اصطلاح Type (نوع) پارامتر ورودی، یکسری دستورات را اجرا کند که در ادامه هر یک از آنها را توضیح دادهایم:
- اگر پارامتر ورودی از نوع عدد بود، آن را دو برابر کند.
- در مورد پارامتر ورودی از نوع استرینگ، تکتک حروف استرینگ به ترتیب با خودش کانکت شود.
- در مورد پارامتر ورودی از نوع آرایه یا آبجکت، فانکشن اصلی به ازای هر یک از مقادیر آرایه فراخوانی شود.
- در مورد پارامتر ورودی از نوع فانکشن نیز میباید فانکشن مذکور دو مرتبه فراخوانی شود.
حال فانکشن فوق را بر اساس دستور switch
پیادهسازی میکنیم که در همین راستا کدی مانند زیر خواهیم داشت:
const doubler = (input) => {
switch (typeof input) {
case 'number':
return input + input;
case 'string':
return input
.split('')
.map((letter) => letter + letter)
.join('');
case 'object':
Object.keys(input)
.map((key) => (input[key] = doubler(input[key])));
return input;
case 'function':
input();
input();
}
};
// integer
console.log(doubler(-10));
// string
console.log(doubler('hey'));
// array
console.log(doubler([5, 'hello']));
// object
console.log(doubler({
a: 5,
b: 'hello'
}));
// function
console.log(
doubler(function() {
console.log('call-me');
}),
);
در کد فوق فانکشنی به نام ()doubler
با یک آرگومان ورودی تحت عنوان input
تعریف کردهایم که در ادامه با استفاده از اپراتور typeof
نوع پارامتر ورودی را مشخص میکنیم (جهت کسب اطلاعات بیشتر در رابطه با اپراتورها در زبان جاوااسکریپت، توصیه میکنیم به مقالۀ درآمدی بر اپراتورها در زبان جاوااسکریپت مراجعه نمایید.) سپس نوع پارامتر ورودی را به عنوان شرط داخل دستور switch
قرار داده و در چند case
مختلف بررسی میکنیم تا ببینیم نوع متغیر ورودی با کدام یک از کِیسها برابری میکند و در صورت یافتن کِیس مطابق با شرط مد نظر، دستور داخل آن به ازای متغیر مذکور اجرا خواهد شد که در ادامه کد مربوط به تکتک کِیسها را بررسی میکنیم.
همانطور که میبینید، در کیس اول گفتهایم اگر نوع پارامتر ورودی عدد بود با خودش جمع شده و در خروجی ریترن شود و در شرایطی که پارامتر ورودی از نوع استرینگ بود، فانکشن از پیش تعریفشدۀ ()split
را فراخوانی کردهایم که این وظیفه را دارا است تا استرینگ دریافتی را به حروف تشکیلدهندهاش تجزیه کرده و در آرایهای نگاهداری میکند و در ادامه آرایۀ مذکور را به فانکشن از پیش تعریفشدۀ ()map
میدهیم که این فانکشن نیز به منظور اِعمال یکسری دستورات مد نظر روی تکتک مقادیر آرایه به کار برده میشود که در آن گفتهایم تکتک حروف آرایه را با خودش کانکت کرده و در آرایهای جدید ذخیره کند و در نهایت فانکشن از پیش تعریفشدۀ ()join
را داریم که بدین وسیله مقادیر موجود در آرایۀ جدید را در قالب یک استرینگ در خروجی برمیگردانیم.
کیس بعد در مورد پارامتر ورودی از نوع آرایه یا آبجکت است که در آن گفتهایم مقادیر Key از پارامتر ورودی را در یک آرایه ذخیره کرده و در ادامه با بهکارگیری فانکشن ()map
یکسری دستورات روی تکتک مقادیر متناظر با این کلیدها اِعمال خواهیم کرد بدین صورت که به ازای هر یک از مقادیر، فانکشن اصلی را مجدداً فراخوانی میکنیم و در نهایت هم کِیس مربوط به پارامتر ورودی از نوع فانکشن را داریم که در آن گفتهایم پارامتر ورودی، که در اینجا خود یک فانکشن است، دو مرتبه فراخوانی شود.
حال در این مرحله نیاز است تا فانکشن ()doubler
را به ازای ورودیهای مختلف فراخوانی کنیم و همانطور که میبینید، در ابتدا فانکشن مذکور را به ازای عدد 10- فراخوانی کردهایم که در این مورد دستور مربوط به کِیس اول اجرا شده و عدد 20- در خروجی چاپ میشود سپس فانکشن ()doubler
را برای پارامتر ورودی از نوع استرینگ «hey» فراخوانی کردهایم که در این مورد کِیس دوم اجرا شده و تکتک حروف استرینگ ورودی با خودش کانکت شده و استرینگی به صورت زیر در خروجی چاپ میشود:
hheeyy
در ادامه، فانکشن ()doubler
به ازای پارامتر ورودی از نوع آرایه فراخوانی شده است که در کیس متناظر با آن یا به عبارتی :'case 'object
گفتهایم فانکشن مذکور به ازای هر یک از مقادیر آرایه مجدداً فراخوانی شود و همانطور که میبینید، اندیس اول از این آرایه یک عدد میباشد که در نتیجه این عدد دو برابر میشود و اندیس دوم نیز یک استرینگ است که هر یک از حروف آن تکتک با خودش کانکت شده و در نهایت خروجی حاصل از اجرا آرایهای به ترتیب زیر خواهد بود:
[10, "hheelllloo"]
در مورد دستور سطر بعدی نیز باید گفت که پارامتر ورودی از نوع آبجکت میباشد که در کیس مربوطه :'case 'object
گفتهایم فانکشن اصلی به ازای هر یک از مقادیر میباید مجدداً فراخوانی شود (همچون شرایطی که در مورد آرایه وجود داشت) که در همین راستا خروجی این دستور به ترتیب زیر خواهد بود:
{a: 10, b: "hheelllloo"}
و در مورد دستور آخر نیز پارامتر ورودی از نوع فانکشن را داریم که در کِیس متناظر آن گفتهایم فانکشن ورودی دو مرتبه فراخوانی شود که نتیجۀ نهایی در خروجی بدین صورت خواهد بود:
call-me
call-me
حال اگر بخواهیم مسئلۀ فوق را بدون استفاده از دستورات شرطی پیادهسازی کنیم، بلوک کد مثال قبل را به شکل زیر تغییر میدهیم:
const doubler = (input) => {
return doubler.operationsByType[typeof input](input);
};
doubler.operationsByType = {
number: (input) => input + input,
string: (input) =>
input
.split('')
.map((letter) => letter + letter)
.join(''),
function: (input) => {
input();
input();
},
object: (input) => {
Object.keys(input)
.map((key) => (input[key] = doubler(input[key])));
return input;
},
};
// integer
console.log(doubler(-10));
// string
console.log(doubler('hey'));
// array
console.log(doubler([5, 'hello']));
// object
console.log(doubler({
a: 5,
b: 'hello'
}));
// function
console.log(
doubler(function() {
console.log('call-me');
}),
);
در کد فوق فانکشنی تحت عنوان ()doubler
تعریف کرده و متغیری به نام input
را به عنوان آرگومان ورودی به آن پاس دادهایم و در ادامه گفتهایم فانکشن ()doubler
به آبجکتی تحت عنوان operationsByType
مراجعه کرده و بسته به نوع پارامتر ورودیاش یکی از دستورهای معادل با مقادیر کلید را انتخاب کرده و نتیجۀ حاصل از اجرای آن را در خروجی ریترن کند.
در واقع، ساختار آبجکت operationsByType
بدین صورت است که یکسری Key و Value برای آن تعریف کردهایم به طوری که کلیدها بیانگر نوع پارامتر ورودی و مقادیر متناظر نیز نشاندهندۀ دستور مد نظر جهت اِعمال روی پارامتر ورودی هستند و فانکشن ()doubler
پس از مشخص کردن نوع پارامتر ورودیاش به این آبجکت مراجعه کرده و دستور متناظر با نوع پارامتر مذکور را جستجو کرده و در ادامه آن را اجرا میکند که نتیجۀ نهایی در خروجی ریترن میشود.
همانطور که میبینید، در این اسکریپت دیتای مورد استفاده در دستورات switch
مربوط به کد قبل (دیتایی که بر اساس آن دستور مناسب انتخاب شده و اجرا میشود) را به آبجکتی تحت عنوان operationsByType
انتقال دادهایم به طوری که در پیادهسازی فانکشن ()doubler
گفتهایم به ازای هر یک از پارامترهای ورودی، نوع این پارامتر در آبجکت operationsByType
چک میشود که بدین ترتیب به ازای هر یک از مقادیر Key (نوع پارامتر ورودی) مقدار Value متناظر آن (دستور متناظر برای اِعمال روی پارامتر ورودی) از طریق فانکشن مد نظر فراخوانی شده و اجرا گردد.
جمعبندی
لازم به یادآوری است که کدنویسی با بهکارگیری دستورهای شرطی و یا خلاصهنویسی آنها در برنامهنویسی مانعی ندارد اما این در حالی است که اجتناب از آنها در برخی موقعیتها منجر به سادگی و افزایش خوانایی کد میشود اما در عین حال توجه داشته باشیم که این موضوع یک قاعدۀ کلی نبوده و ممکن است در همۀ موارد صادق نباشد به طوری که اجتناب از دستورات شرطی در برخی مسائل حتی ممکن است موجب کاهش خوانایی سورسکد نیز گردد!