ساختارهای کنترلی ساختارهایی هستند که منطق اجرایی برنامه شما را کنترل می کنند. ساده ترین این ساختارها همان دستور if است! یکی از مشکلاتی که در نوشتن کدهای تمیز پیش می آید استفاده بیش از حد از این ساختارهای کنترلی (مخصوصا به صورت تو در تو یا nested) می باشد. به کد زیر توجه کنید:
if (rowCount > rowIdx) { if (drc[rowIdx].Table.Columns.Contains("avalId")) { do { if (Attributes[attrVal.AttributeClassId] == null) { // do stuff } else { if (!(Attributes[attrVal.AttributeClassId] is ArrayList)) { // do stuff } else { if (!isChecking) { // do stuff } else { // do stuff } } } rowIdx++; } while (rowIdx < rowCount && GetIdAsInt32(drc[rowIdx]) == Id); } else rowIdx++; } return rowIdx; }
کدهایی که دارای حلقه ها و شرط های مختلف هستند و اینچنین تو در تو نوشته شده اند از نظر خوانایی در پایین ترین مرحله قرار می گیرند چرا که خواندن و درک آن ها زمان بسیار زیادی خواهد برد. به کدهایی که به شکل بالا هستند arrow code می گویند چرا که ظاهر کلی آن ها شبیه یک نوک تیرهای تیر و کمان است:
if if if if do something endif endif endif endif
ما در این فصل می خواهیم به دو مبحث اصلی بپردازیم: جلوگیری از ایجاد ساختارهای کنترلی تو در تو (nested) و کار با خطاها. مباحث این فصل طوری هستند که کمتر با تئوری سر و کار خواهیم داشت بلکه مستقیما وارد کدنویسی شده و بارها و بارها کدهای خودمان را ویرایش می کنیم بنابراین سعی کنید صبور باشید و مرحله به مرحله را مطالعه نمایید. برای نوشتن کد تمیز در رابطه با ساختارهای کنترلی باید نکات زیر را به یاد داشته باشید:
ما در این فصل با تمام این موارد آشنا خواهیم شد اما قبل از پرداختن به آن ها به مبحث دیگری به نام guard ها می پردازیم.
یکی از بهترین روش های کوتاه کردن arrow code استفاده از مبحثی به نام Guards است. برای درک گاردها بهتر است از یک مثال عملی کمک بگیریم:
if (email.includes('@')) { // انجام عملیات هایی خاص }
این کد یک کد بسیار ساده است که فقط وجود علامت @ در متغیر ایمیل را بررسی می کند. من عملیات های ممکن را به صورت یک کامنت (انجام عملیات هایی خاص) مشخص کرده ام؛ یعنی مهم نیست چه نوع عملیاتی را می خواهیم انجام بدهیم اما فرض می کنیم که عملیات مورد نظر ما بسیار طولانی باشد و به ساختارهای کنترلی بسیار زیادی نیاز داشته باشد. در این حالت می توانیم از یک گارد استفاده کنیم؛ یعنی شرط را برعکس کنیم:
if (!email.includes('@')) { return; } // انجام عملیات هایی خاص
آیا می توانیم از کد بالا متوجه مکانیسم عمل کد شوید؟ ما در اینجا شرط if را برعکس کرده ایم و به آن guard می گوییم. چرا؟ در صورتی که ایمیل مورد نظر دارای علامت @ نباشد، شرط برقرار می شود بنابراین وارد آن می شویم. در این هنگام دستور return باعث می شود از تابع خارج شویم و بقیه کدها (قسمت «انجام عملیات هایی خاص») هیچ گاه اجرا نشود. شرط if یک گارد یا نگهبان است به دلیل اینکه از قسمت های بعدی خودش محافظت می کند. انجام این کار باعث می شود به جای داشتن کدهای تو در تو، کدهای پشت سر هم داشته باشیم. هر چه کدها تو در تو تر باشند خواندن آن ها سخت تر خواهد بود. به طور مثال:
if (user.active) { if (user.hasPurchases()) { // انجام عملیات هایی خاص } }
در این مثال دو شرط if را به صورت تو در تو داریم و خواندن آن ها آزار دهنده است. اگر شرط درونی را تبدیل به یک گارد کنیم، می توانیم خوانایی کد را افزایش بدهیم:
if (!user.hasPurchases()) { return; } if (user.active) { // انجام عملیات هایی خاص }
با انجام این کار از تو در تو بودن شرط های if جلوگیری کرده ایم. فرض ما این است که این کد بخشی از یک تابع است بنابراین با رسیدن به دستور return از تابع خارج می شویم و هیچ گاه به شرط user.active نخواهیم رسید. برای اینکه بهتر متوجه شوید باید یک تمرین ساده را با هم حل کنیم. من برای این این فصل یک کد بسیار شلوغ با چندین سطح تو در تو را انتخاب کرده ام تا شما روی آن کار کنید:
main(); function main() { const transactions = [ { id: "t1", type: "PAYMENT", status: "OPEN", method: "CREDIT_CARD", amount: "23.99", }, { id: "t2", type: "PAYMENT", status: "OPEN", method: "PAYPAL", amount: "100.43", }, { id: "t3", type: "REFUND", status: "OPEN", method: "CREDIT_CARD", amount: "10.99", }, { id: "t4", type: "PAYMENT", status: "CLOSED", method: "PLAN", amount: "15.99", }, ]; processTransactions(transactions); } function processTransactions(transactions) { if (transactions && transactions.length > 0) { for (const transaction of transactions) { if (transaction.type === "PAYMENT") { if (transaction.status === "OPEN") { if (transaction.method === "CREDIT_CARD") { processCreditCardPayment(transaction); } else if (transaction.method === "PAYPAL") { processPayPalPayment(transaction); } else if (transaction.method === "PLAN") { processPlanPayment(transaction); } } else { console.log("Invalid transaction type!"); } } else if (transaction.type === "REFUND") { if (transaction.status === "OPEN") { if (transaction.method === "CREDIT_CARD") { processCreditCardRefund(transaction); } else if (transaction.method === "PAYPAL") { processPayPalRefund(transaction); } else if (transaction.method === "PLAN") { processPlanRefund(transaction); } } else { console.log("Invalid transaction type!", transaction); } } else { console.log("Invalid transaction type!", transaction); } } } else { console.log("No transactions provided!"); } } function processCreditCardPayment(transaction) { console.log( "Processing credit card payment for amount: " + transaction.amount ); } function processCreditCardRefund(transaction) { console.log( "Processing credit card refund for amount: " + transaction.amount ); } function processPayPalPayment(transaction) { console.log("Processing PayPal payment for amount: " + transaction.amount); } function processPayPalRefund(transaction) { console.log("Processing PayPal refund for amount: " + transaction.amount); } function processPlanPayment(transaction) { console.log("Processing plan payment for amount: " + transaction.amount); } function processPlanRefund(transaction) { console.log("Processing plan refund for amount: " + transaction.amount); }
ما در این فصل چندین بار با این کد کار خواهیم کرد. اگر دقت کنید شرط های if در حال صدا زدن متدهایی هستند که در این فایل تعریف شده اند اما فعلا روی شرط if بزرگ آن در تابع processTransactions تمرکز کنید. من از شما می خواهم که با استفاده از گاردها، این شرط های تو در تو را بهتر کنید. در نظر داشته باشید که حتی پس از استفاده از گاردها نیز هنوز تعداد کمی از شروط تو در تو را خواهید داشت.
پاسخ این تمرین ساده است. در قدم اول باید اولین شرط if در تابع processTransactions را به یک گارد تبدیل کنیم. اگر کد را مطالعه کرده باشید احتمال می دهید که پارامتر transaction یک آرایه است با این حساب می توانیم شرط را بدین صورت برعکس کنیم: transactions وجود نداشته باشد یا طول آن برابر صفر باشد. پس از آنکه این کار را انجام دادید، می توانید محتوای بلوک else را که یک console.log است درون خود شرط بگذارید (با برعکس کردن شرط، بلوک else خود شرط خواهد بود). بعد از انجام این کار هنوز یک قسمت دیگر نیز وجود دارد که می تواند تبدیل به گارد شود. کجا؟ این قسمت از کد:
if (transaction.status === "OPEN") { if (transaction.method === "CREDIT_CARD") { processCreditCardPayment(transaction); } else if (transaction.method === "PAYPAL") { processPayPalPayment(transaction); } else if (transaction.method === "PLAN") { processPlanPayment(transaction); } } else { console.log("Invalid transaction type!"); }
ما در اینجا یک شرط بزرگ را داریم که درون خود کدهای زیادی دارد و سپس یک حالت else برایش داریم که پیام خطا را نشان می دهد. هر جایی در هر برنامه ای چنین کدی را دیدید (یک if بزرگ و یک else برای نمایش خطا) سریعا باید متوجه بشوید که امکان استفاده از گاردها در آن وجود دارد. مسئله مهم اینجاست که این شرط درون یک حلقه for نوشته شده است و اگر return کنیم به اشتباه از این حلقه خارج می شویم که کار مناسبی نیست. راه حل این مشکل استفاده از continue به جای return است. همچنین فراموش نکنید که باید پیام console.log را نیز قبل از continue قرار بدهید.
حالا برای راحتی بیشتر شرط مربوط به 'transaction.status !== 'Open را به ابتدای شرط های این تابع منتقل می کنم. با کمی تصحیح و انجام این عملیات نتیجه زیر را دریافت می کنید:
main(); function main() { const transactions = [ { id: "t1", type: "PAYMENT", status: "OPEN", method: "CREDIT_CARD", amount: "23.99", }, { id: "t2", type: "PAYMENT", status: "OPEN", method: "PAYPAL", amount: "100.43", }, { id: "t3", type: "REFUND", status: "OPEN", method: "CREDIT_CARD", amount: "10.99", }, { id: "t4", type: "PAYMENT", status: "CLOSED", method: "PLAN", amount: "15.99", }, ]; processTransactions(transactions); } function processTransactions(transactions) { if (!transactions || transactions.length === 0) { console.log("No transactions provided!"); return; } for (const transaction of transactions) { if (transactions.status !== "OPEN") { console.log("Invalid transaction type!"); continue; } if (transaction.type === "PAYMENT") { if (transaction.method === "CREDIT_CARD") { processCreditCardPayment(transaction); } else if (transaction.method === "PAYPAL") { processPayPalPayment(transaction); } else if (transaction.method === "PLAN") { processPlanPayment(transaction); } } else if (transaction.type === "REFUND") { if (transaction.method === "CREDIT_CARD") { processCreditCardRefund(transaction); } else if (transaction.method === "PAYPAL") { processPayPalRefund(transaction); } else if (transaction.method === "PLAN") { processPlanRefund(transaction); } } else { console.log("Invalid transaction type!", transaction); } } } function processCreditCardPayment(transaction) { console.log( "Processing credit card payment for amount: " + transaction.amount ); } function processCreditCardRefund(transaction) { console.log( "Processing credit card refund for amount: " + transaction.amount ); } function processPayPalPayment(transaction) { console.log("Processing PayPal payment for amount: " + transaction.amount); } function processPayPalRefund(transaction) { console.log("Processing PayPal refund for amount: " + transaction.amount); } function processPlanPayment(transaction) { console.log("Processing plan payment for amount: " + transaction.amount); } function processPlanRefund(transaction) { console.log("Processing plan refund for amount: " + transaction.amount); }
در ادامه بیشتر روی این کد کار می کنیم اما فعلا با استفاده از گاردها توانسته ایم سطح خوانایی کد را ارتقاء بدهیم.
همانطور که در فصل قبل توضیح دادم یکی از مسائلی که باعث عدم خوانایی کدها می شود، تفاوت سطوح کدها در یک بلوک است. ما در جلسه قبل شکستن کد به توابع مختلف را به عنوان راه حل مقابله با چنین مشکلاتی معرفی کردیم و این راه حل در این قسمت نیز مصداق دارد. به طور مثال اولین بخش تابع processTransactions یک شرط if به شکل زیر است:
if (!transactions || transactions.length === 0) { console.log("No transactions provided!"); return; }
ما می توانیم دو شرطی که در این if بررسی شده اند (وجود داشتن transaction و صفر نبودن طول آن (transaction.length) در قالب یک تابع دیگر استخراج کنیم. انجام این کار علاوه بر کوتاه کردن شرط if باعث می شود که بتوانیم از شروط مثبت (عدم استفاده از علامت ! در شرط) استفاده کنیم و همانطور که گفتم خوانایی شروط مثبت بیشتر از خوانایی شروط منفی است. با این حساب یک تابع جدید را تعریف کرده و می گوییم:
function isEmpty(transactions) { return !transactions || transactions.length === 0; }
حالا این تابع transaction ما را گرفته و شرط ما را روی آن بررسی می کند. برای استفاده از آن باید آن را درون شرط if صدا بزنید:
function processTransactions(transactions) { if (isEmpty(transactions)) { console.log("No transactions provided!"); return; } // ادامه کدها
با انجام این کار کد خود را خواناتر کرده ایم اما هنوز تفاوت سطوح کد در شرط if مشخص است؛ ما یک دستور console.log داریم که جزئی تر از دیگر بخش های کد است. باز هم می گویم که تغییر دادن این بخش مربوط به خود شما و نیاز برنامه هایتان است. معمولا در برنامه های واقعی کمتر از console.log استفاده می شود و این دستور log معمولا دستوراتی برای مدیریت خطا است بنابراین من دوست دارم آن را در قالب یک متد جداگانه تعریف کنم تا اگر بعدا قصد ویرایش داشتیم نیاز به ویرایش ۱۰ قسمت مختلف در برنامه نباشد:
function showErrorMessage(message) { console.log(message); }
حالا می توانیم از آن در شرط if استفاده کنیم:
function processTransactions(transactions) { if (isEmpty(transactions)) { showErrorMessage("No transactions provided!"); return; } // ادامه کدها
در ادامه من می خواهم حلقه for را در جای خودش نگه دارم چرا که نام تابع processTransactions (پردازش تراکنش ها) می باشد بنابراین باید بتواند بین تراکنش ها گردش کند اما درون این حلقه یک شرط if بزرگ را داریم که بر اساس انواع تراکنش، عملیات های مختلفی را انجام می دهد. به نظر من بهتر است تمام این کد را استخراج کرده و درون یک تابع کاملا جدید قرار بدهیم. من نام تابع جدید را processTransaction می گذارم (بدون s جمع):
function processTransaction(transaction) { if (transactions.status !== "OPEN") { console.log("Invalid transaction type!"); return; } if (transaction.type === "PAYMENT") { if (transaction.method === "CREDIT_CARD") { processCreditCardPayment(transaction); } else if (transaction.method === "PAYPAL") { processPayPalPayment(transaction); } else if (transaction.method === "PLAN") { processPlanPayment(transaction); } } else if (transaction.type === "REFUND") { if (transaction.method === "CREDIT_CARD") { processCreditCardRefund(transaction); } else if (transaction.method === "PAYPAL") { processPayPalRefund(transaction); } else if (transaction.method === "PLAN") { processPlanRefund(transaction); } } else { console.log("Invalid transaction type!", transaction); } }
از آنجایی که ما transaction یا تراکنش را به صورت یک پارامتر دریافت خواهیم کرد، شرط if درون آن هنوز هم کار خواهد کرد و می تواند به خصوصیات type یا method دسترسی داشته باشد. در نظر داشته باشید که من گارد خودمان (شرط باز نبودن تراکنش یا 'transaction.status !== 'OPEN) را نیز از حلقه اصلی جدا کرده ام و درون تابع processTransaction قرار داده ام. البته زمانی که این کار را انجام می دهیم دیگر نیازی به continue نیست و آن را به همان return برگردانده ایم. چرا؟ به دلیل اینکه حالا scope یا دامنه کد فرق می کند. قبلا این گارد مستقیما درون یک حلقه قرار داشت اما حالا مستقیما درون یک تابع قرار دارد و با return از تابع خارج می شود.
با این حساب می توانیم به متد processTransactions (با s جمع) برگردیم و درون آن processTransaction را صدا بزنیم:
function processTransactions(transactions) { if (isEmpty(transactions)) { showErrorMessage("No transactions provided!"); return; } for (const transaction of transactions) { processTransaction(transaction); } }
حالا همانطور که می بینید متد processTransactions بسیار کوتاه تر و خواناتر شده است. ما همان حلقه for را نگه داشته ایم اما به جای اینکه کدهای جزئی را به صورت خط به خط درون آن بنویسیم، کدها را به تابع مستقل دیگری انتقال داده ایم که در هر گردش یکی از تراکنش ها را گرفته و آن را پردازش می کند.
برای اینکه شما هم همگام با من جلو بیایید، یک بار تمام کدهایتان را با تمام کدهای من مقایسه کنید. کدهایتان باید تا این لحظه دقیقا به شکل زیر باشد:
main(); function main() { const transactions = [ { id: "t1", type: "PAYMENT", status: "OPEN", method: "CREDIT_CARD", amount: "23.99", }, { id: "t2", type: "PAYMENT", status: "OPEN", method: "PAYPAL", amount: "100.43", }, { id: "t3", type: "REFUND", status: "OPEN", method: "CREDIT_CARD", amount: "10.99", }, { id: "t4", type: "PAYMENT", status: "CLOSED", method: "PLAN", amount: "15.99", }, ]; processTransactions(transactions); } function processTransactions(transactions) { if (isEmpty(transactions)) { showErrorMessage("No transactions provided!"); return; } for (const transaction of transactions) { processTransaction(transaction); } } function isEmpty(transactions) { return !transactions || transactions.length === 0; } function showErrorMessage(message) { console.log(message); } function processTransaction(transaction) { if (transactions.status !== "OPEN") { console.log("Invalid transaction type!"); return; } if (transaction.type === "PAYMENT") { if (transaction.method === "CREDIT_CARD") { processCreditCardPayment(transaction); } else if (transaction.method === "PAYPAL") { processPayPalPayment(transaction); } else if (transaction.method === "PLAN") { processPlanPayment(transaction); } } else if (transaction.type === "REFUND") { if (transaction.method === "CREDIT_CARD") { processCreditCardRefund(transaction); } else if (transaction.method === "PAYPAL") { processPayPalRefund(transaction); } else if (transaction.method === "PLAN") { processPlanRefund(transaction); } } else { console.log("Invalid transaction type!", transaction); } } function processCreditCardPayment(transaction) { console.log( "Processing credit card payment for amount: " + transaction.amount ); } function processCreditCardRefund(transaction) { console.log( "Processing credit card refund for amount: " + transaction.amount ); } function processPayPalPayment(transaction) { console.log("Processing PayPal payment for amount: " + transaction.amount); } function processPayPalRefund(transaction) { console.log("Processing PayPal refund for amount: " + transaction.amount); } function processPlanPayment(transaction) { console.log("Processing plan payment for amount: " + transaction.amount); } function processPlanRefund(transaction) { console.log("Processing plan refund for amount: " + transaction.amount); }
همانطور که با نگاه به کد بالا دیده می شود، تابع processTransactions خوانا شده است اما شرط های if تو در تویی که به تابع processTransaction منتقل کردیم هنوز هم بسیار شلوغ و ناخوانا هستند. من از شما می خواهم که سعی کنید با استفاده از نکاتی که در این جلسه توضیح دادم، خودتان این مشکل را حل کنید و سپس به پاسخ من نگاه کنید.
اگر به شرط if بزرگ نگاه کنید، دو قسمت if و else زیر را در آن مشاهده خواهید کرد:
بر این اساس ما می توانیم دو تابع داشته باشیم که هر کدام مسئول پردازش یکی از این دو دسته از تراکنش ها هستند. من با دسته اول شروع می کنم و نام این متد را processPayment می گذارم:
function processPayment(paymentTransaction) { if (paymentTransaction.method === "CREDIT_CARD") { processCreditCardPayment(paymentTransaction); } else if (paymentTransaction.method === "PAYPAL") { processPayPalPayment(paymentTransaction); } else if (paymentTransaction.method === "PLAN") { processPlanPayment(paymentTransaction); } }
همانطور که مشاهده می کنید نیمی از شرط if بزرگ که مربوط به Payment ها بود درون این تابع قرار داده شده است. از آنجایی که من می خواهم پارامتر دریافتی تابع واضح و صریح باشد، نامش را از transaction به paymentTransaction تغییر داده ام. به همین دلیل باید نام تراکنش را در تمام این کد به paymentTransaction تغییر بدهید.
در مرحله بعدی همین کار را برای نیمه دوم شرط if انجام می دهیم. این بار من نام processRefund را برای این تابع انتخاب کرده ام:
function processRefund(refundTransaction) { if (refundTransaction.method === "CREDIT_CARD") { processCreditCardRefund(refundTransaction); } else if (refundTransaction.method === "PAYPAL") { processPayPalRefund(refundTransaction); } else if (refundTransaction.method === "PLAN") { processPlanRefund(refundTransaction); } }
در نهایت به تابع processTransaction (بدون s جمع) برمی گردیم و از این دو تابع در آن استفاده می کنیم:
function processTransaction(transaction) { if (transactions.status !== "OPEN") { console.log("Invalid transaction type!"); return; } if (transaction.type === "PAYMENT") { processPayment(transaction); } else if (transaction.type === "REFUND") { processRefund(transaction); } else { console.log("Invalid transaction type!", transaction); } }
با انجام این کار کدها خواناتر شده اند اما هنوز هم جای اصلاح دارند. به طور مثال ما در این تابع دو دستور console.log داریم که باعث تفاوت سطح در کدها شده اند. یکی از این دو دستور علاوه بر چاپ یک پیام در کنسول، خود تراکنش و داده هایش را نیز چاپ می کند (آرگومان دوم برای console.log) بنابراین اگر بخواهیم از تابع showErrorMessage خودمان استفاده کنیم باید آن را کمی ویرایش کنیم:
function showErrorMessage(message, item) { console.log(message); if (item) { console.log(item); } }
این یک روش بسیار ساده برای انجام این کار است. ما ابتدا پیام خطا را چاپ می کنیم و در صورتی که آرگومان دومی (item) ارسال شود آن را نیز به صورت جداگانه چاپ می کنیم. طبیعتا راه های بسیار زیادی برای انجام این کار وجود دارد و تابع بالا فقط یکی از چندین حالت ممکن است. حالا می توانیم از این تابع به جای console.log ها استفاده کنیم:
function processTransaction(transaction) { if (transactions.status !== "OPEN") { showErrorMessage("Invalid transaction type!"); return; } if (transaction.type === "PAYMENT") { processPayment(transaction); } else if (transaction.type === "REFUND") { processRefund(transaction); } else { showErrorMessage("Invalid transaction type!", transaction); } }
من پیشنهاد نمی کنم که این کد را بیشتر از این بشکنید چرا که تقریبا از اینجا به بعد هر کاری انجام بدهید اضافه کاری است اما اگر اصرار دارید می توانید وضعیت یک تراکنش را نیز از طریق یک تابع دیگر مشخص کنید. به طور مثال من سه تابع زیر را برای سه حالت مختلف تعریف می کنم:
function isOpen(transaction) { return transaction.status === "OPEN"; } function isPayment(transaction) { return transaction.type === "PAYMENT"; } function isRefund(transaction) { return transaction.type === "REFUND"; }
همانطور که می بینید این سه تابع مسئول بررسی نوع تراکنش (Refund یا Payment) و باز بودن وضعیت تراکنش هستند. حالا با استفاده از این سه تابع می توانیم تابع processTransaction را به شکل زیر بازنویسی کنیم:
function processTransaction(transaction) { if (!isOpen(transaction)) { showErrorMessage("Invalid transaction type!"); return; } if (isPayment(transaction)) { processPayment(transaction); } else if (isRefund(transaction)) { processRefund(transaction); } else { showErrorMessage("Invalid transaction type!", transaction); } }
از نظر من این نوع از نوشتار نیز قابل قبول و خوانا می باشد گرچه ممکن است از نظر بسیاری از افراد زیاده روی کرده باشیم. در جلسه بعدی بیشتر روی این کدها کار خواهیم کرد.
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.