آشنایی با نحوهٔ ساده‌سازی سورس‌کد با حذف دستورات شرطی

معمولاً یکی از چالش‌هایی که دولوپرهای مبتدی با آن مواجه می‌شوند این است که چگونه می‌توان مسئله‌ای را به روش‌های دیگری مثلاً بدون استفاده از دستورهای شرطی، اپراتورهای به اصطلاح 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 متناظر آن (دستور متناظر برای اِعمال روی پارامتر ورودی) از طریق فانکشن مد نظر فراخوانی شده و اجرا گردد.

جمع‌بندی
لازم به یادآوری است که کدنویسی با به‌کارگیری دستورهای شرطی و یا خلاصه‌نویسی آن‌ها در برنامه‌نویسی مانعی ندارد اما این در حالی است که اجتناب از آن‌ها در برخی موقعیت‌ها منجر به سادگی و افزایش خوانایی کد می‌شود اما در عین حال توجه داشته باشیم که این موضوع یک قاعدۀ کلی نبوده و ممکن است در همۀ موارد صادق نباشد به طوری که اجتناب از دستورات شرطی در برخی مسائل حتی ممکن است موجب کاهش خوانایی سورس‌کد نیز گردد!

نظرات
اگر login نکردی برامون ایمیلت رو بنویس: