کدنویسی تمیز: factory function و اعتبارسنجی

Clean Coding: Factory Function and Validation

25 خرداد 1400
درسنامه درس 11 از سری کدنویسی تمیز
کدنویسی تمیز: factory function و اعتبارسنجی (قسمت 11)

خطاها در کنار منطق اصلی تابع

تا این جلسه چندین بار کدهایمان را بازنویسی کرده ایم اما نباید از ادامه راه دلسرد شوید. نوشتن کد تمیز و خوانا نیاز به تمرین دارد و بهترین تمرین همین بازنویسی چند باره کدهای بد است. در حال حاضر کدهای ما هنوز یک مشکل جدی دارند؛ ما از خطاها استفاده می کنیم که بسیار عالی هستند اما در حال حاضر ساخت خطا و پرتاب آن دقیقا در کنار کدهایی قرار دارد که مسئولیت پردازش تراکنش ها را بر عهده دارند. مثال:

function processTransactions(transactions) {

  if (isEmpty(transactions)) {

    const error = new Error('No transactions provided!');

    error.code = 1;

    throw error;

  }




  for (const transaction of transactions) {

    try {

      processTransaction(transaction);

    } catch (error) {

      showErrorMessage(error.message, error.item);

    }

  }

}

طبیعتا انجام چنین کاری باعث ترکیب سطوح مختلف کد در یک تابع می شود. روش بهتر انجام این کار این است که مکانیسمی جداگانه برای اعتبارسنجی تراکنش ها داشته باشیم. با این حساب من تابعی به نام validateTransactions را تعریف می کنم و منطق ساخت و پرتاب خطا را در آن می نویسم:

function validateTransactions(transactions) {

  if (isEmpty(transactions)) {

    const error = new Error('No transactions provided!');

    error.code = 1;

    throw error;

  }

}

این متد تراکنش ما را دریافت می کند و در صورتی که isEmpty برقرار باشد (تراکنش خالی باشد) یک خطای جدید ساخته و سپس آن را با کد ۱ پرتاب می کند. ما واقعا به کد ۱ نیازی نداریم و شما می توانید آن را حذف کرده یا به جای آن عدد دیگری قرار بدهید. هدف من از حذف نکردن کد، این است که شما بدانید توانایی انجام چنین کاری را دارید. در مرحله بعدی به تابع processTransactions رفته و قسمت پرتاب خطا را در آن پاک می کنیم و به جای آن، تابع validateTransactions را صدا می زنیم:

function processTransactions(transactions) {

  validateTransactions(transactions);




  for (const transaction of transactions) {

    try {

      processTransaction(transaction);

    } catch (error) {

      showErrorMessage(error.message, error.item);

    }

  }

}

با انجام این کار هم تابع processTransactions را خلوت تر کرده ایم و هم از خطاها استفاده کرده ایم و هم اینکه منطق ساخت و پرتاب خطا با منطق عملیاتی برنامه ترکیب نشده است. در ضمن در جلسه قبلی نیز توضیح دادم که اگر خطایی پرتاب شود، اجرای آن بخش از برنامه قطع خواهد شد بنابراین در صورتی که تراکنش خالی باشد یک خطا توسط تابع validateTransactions پرتاب شده و در تابع processTransactions هیچگاه به حلقه for در آن نمی رسیم اما اگر تراکنش خالی نباشد تابع validateTransactions هیچ کاری انجام نمی دهد و حلقه for به صورت عادی انجام می شود.

در ضمن ممکن است بگویید در تابع validateTransactions چند سطح مختلف از کد را داریم (تابع isEmpty در کنار کدهای سطحی پایینی مانند throw error) اما از نظر من شکستن این تابع ساده به تابعی دیگر، اضافه نویسی صد در صد است. اگر می خواهید چنین کاری بکنید، باید یک کلاس جداگانه برای مدیریت خطا تعریف کنید و به آن پر و بال بدهید تا تمام سیستم مدیریت خطای برنامه خود را داشته باشد.

حالا به تابع processTransaction نگاهی بیندازید:

function processTransaction(transaction) {

  if (!isOpen(transaction)) {

    const error = new Error('Invalid transaction type.');

    throw error;

  }




  if (!isPayment(transaction) && !isRefund(transaction)) {

    const error = new Error('Invalid transaction type!');

    error.item = transaction;

    throw error;

  }




  if (usesTransactionMethod(transaction, 'CREDIT_CARD')) {

    processCreditCardTransaction(transaction);

  } else if (usesTransactionMethod(transaction, 'PAYPAL')) {

    processPayPalTransaction(transaction);

  } else if (usesTransactionMethod(transaction, 'PLAN')) {

    processPlanTransaction(transaction);

  }

}

همانطور که می بینید این تابع نیز مانند تابع دیگر ما ساخت و پرتاب خطا را درون خودش انجام می دهد بنابراین باید این بخش را نیز استخراج کرده و درون تابع خودش قرار بدهیم. من برای این کار تابع دیگری به نام validateTransaction (بدون s جمع در انتهای آن) را تعریف می کنم:

function validateTransaction(transaction) {

  if (!isOpen(transaction)) {

    const error = new Error('Invalid transaction type.');

    throw error;

  }




  if (!isPayment(transaction) && !isRefund(transaction)) {

    const error = new Error('Invalid transaction type!');

    error.item = transaction;

    throw error;

  }

}

در ابتدا باید تراکنش را دریافت کنیم و سپس دو شرط if جداگانه را داریم. چرا؟ به دلیل اینکه تابع  processTransaction دو بخش برای ساخت خطا دارد و من می خواهم هر دو بخش را در یک تابع قرار بدهم. شرط اول باز نبودن تراکنش (بسته بودن آن) می باشد. اگر تراکنش بسته باشد دیگر نیازی به پردازش ندارد بنابراین یک خطا را ساخته و پرتاب می کنیم. شرط دوم این است که تراکنش نه از نوع پرداخت و نه از نوع پس دادن پول مشتری باشد. در این حالت باز هم یک خطا ساخته و آن را پرتاب می کنیم. تنها کار باقی مانده صدا زدن این تابع در processTransaction است:

function processTransaction(transaction) {

  validateTransaction(transaction);




  if (usesTransactionMethod(transaction, 'CREDIT_CARD')) {

    processCreditCardTransaction(transaction);

  } else if (usesTransactionMethod(transaction, 'PAYPAL')) {

    processPayPalTransaction(transaction);

  } else if (usesTransactionMethod(transaction, 'PLAN')) {

    processPlanTransaction(transaction);

  }

}

مدیریت خطا هدف واحدی دارد

نکته مهمی که در رابطه با مدیریت خطا وجود دارد این است: هر کد مدیریت خطا (هر بلوک try & catch) یک هدف واحد داشته و کار واحدی را انجام می دهد. به طور مثال در تابع main چنین کدی را داریم:

try {

  processTransactions(transactions);

} catch (error) {

  showErrorMessage(error.message);

}

این کد دقیقا همان نکته مهم را نشان می دهد، در بخش try یک عملیات واحد را انجام می دهیم و در بخش catch خطاهای احتمالی را دریافت می کنیم. حالا به تابع processTransactions نگاهی بیندازید:

function processTransactions(transactions) {

  validateTransactions(transactions);




  for (const transaction of transactions) {

    try {

      processTransaction(transaction);

    } catch (error) {

      showErrorMessage(error.message, error.item);

    }

  }

}

در این تابع بلوک try & catch ما درون یک حلقه for قرار دارد و دیگر یک عملیات واحد نیست. گرچه که نوشتن کد بدین شکل اشکالی ندارد اما بهتر است از آن دوری شود. یک راه حل ساده این است که بلوک try & catch را از متد processTransactions حذف کنیم:

function processTransactions(transactions) {

  validateTransactions(transactions);




  for (const transaction of transactions) {

    processTransaction(transaction);

  }

}

در مرحله بعدی این بلوک را به متد processTransaction منتقل می کنیم:

function processTransaction(transaction) {

  try {

    validateTransaction(transaction);




    if (usesTransactionMethod(transaction, 'CREDIT_CARD')) {

      processCreditCardTransaction(transaction);

    } else if (usesTransactionMethod(transaction, 'PAYPAL')) {

      processPayPalTransaction(transaction);

    } else if (usesTransactionMethod(transaction, 'PLAN')) {

      processPlanTransaction(transaction);

    }

  } catch (error) {

    showErrorMessage(error.message, error.item)

  }

}

این یک روش برای حل این مشکل است. در حال حاضر کد ما فقط عملیات واحدی را انجام می دهد و در یک حلقه for گردش نمی کند. البته در حال حاضر کد ما از حالت خوانایی خود خارج شده است و با اینکه رها کردن آن به همین شکل کار بدی نیست اما من پیشنهاد می دهم قسمت شرط های if را از آن استخراج کرده و درون یک متد جداگانه قرار بدهیم:

function processByMethod(transaction) {

  if (usesTransactionMethod(transaction, 'CREDIT_CARD')) {

    processCreditCardTransaction(transaction);

  } else if (usesTransactionMethod(transaction, 'PAYPAL')) {

    processPayPalTransaction(transaction);

  } else if (usesTransactionMethod(transaction, 'PLAN')) {

    processPlanTransaction(transaction);

  }

}

حالا می توانیم این متد را در processTransaction و پس از اعتبارسنجی اجرا کنیم:

function processTransaction(transaction) {

  try {

    validateTransaction(transaction);




    processByMethod(transaction);

  } catch (error) {

    showErrorMessage(error.message, error.item);

  }

}

با انجام این کار کدهای ما خوانا تر می شوند. البته این نظر من است و در نهایت نظر شخصی شما است که مشخص می کند شما کدهایتان را چطور خواهید نوشت. اگر با من جلو آمده باشید باید تمام کدهایتان به شکل زیر باشد:

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',

    },

  ];




  try {

    processTransactions(transactions);

  } catch (error) {

    showErrorMessage(error.message);

  }

}




function processTransactions(transactions) {

  validateTransactions(transactions);




  for (const transaction of transactions) {

    processTransaction(transaction);

  }

}




function validateTransactions(transactions) {

  if (isEmpty(transactions)) {

    const error = new Error('No transactions provided!');

    error.code = 1;

    throw error;

  }

}




function isEmpty(transactions) {

  return !transactions || transactions.length === 0;

}




function showErrorMessage(message, item) {

  console.log(message);

  if (item) {

    console.log(item);

  }

}




function processTransaction(transaction) {

  try {

    validateTransaction(transaction);




    processByMethod(transaction);

  } catch (error) {

    showErrorMessage(error.message, error.item);

  }

}




function isOpen(transaction) {

  return transaction.status === 'OPEN';

}




function validateTransaction(transaction) {

  if (!isOpen(transaction)) {

    const error = new Error('Invalid transaction type.');

    throw error;

  }




  if (!isPayment(transaction) && !isRefund(transaction)) {

    const error = new Error('Invalid transaction type!');

    error.item = transaction;

    throw error;

  }

}




function processByMethod(transaction) {

  if (usesTransactionMethod(transaction, 'CREDIT_CARD')) {

    processCreditCardTransaction(transaction);

  } else if (usesTransactionMethod(transaction, 'PAYPAL')) {

    processPayPalTransaction(transaction);

  } else if (usesTransactionMethod(transaction, 'PLAN')) {

    processPlanTransaction(transaction);

  }

}




function usesTransactionMethod(transaction, method) {

  return transaction.method === method;

}




function isPayment(transaction) {

  return transaction.type === 'PAYMENT';

}




function isRefund(transaction) {

  return transaction.type === 'REFUND';

}




function processCreditCardTransaction(transaction) {

  if (isPayment(transaction)) {

    processCreditCardPayment();

  } else if (isRefund(transaction)) {

    processCreditCardRefund();

  }

}




function processPayPalTransaction(transaction) {

  if (isPayment(transaction)) {

    processPayPalPayment();

  } else if (isRefund(transaction)) {

    processPayPalRefund();

  }

}




function processPlanTransaction(transaction) {

  if (isPayment(transaction)) {

    processPlanPayment();

  } else if (isRefund(transaction)) {

    processPlanRefund();

  }

}




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);

}

Factory Function و Polymorphism

در حال حاضر کدهای ما تا حد خوبی تمیز و استاندارد هستند اما هنوز هم شرط های زیادی را داریم و قسمتی از کدهایمان تکراری است. Factory Function ها و Polymorphism راه حل این مشکل ما هستند اما قبل از اینکه بخواهیم از آن ها استفاده کنیم باید بدانیم تعریفشان چیست و چه کاری انجام می دهند.

Factory Function ها توابعی هستند که مسئول ساخت داده خاصی می باشند. یعنی چه؟ به مثال زیر توجه کنید:

function buildUser(name, age) {

  return {

    name: name,

    age: age,

  };

}

factory function ها معمولا یک شیء یا آرایه یا هر چیز دیگری را تولید کنند. در مثال بالا تابعی را داریم که نام و سن کاربر را گرفته و سپس یک شیء را با این مقادیر می سازد. به این تابع یک factory function می گوییم چرا که مسئول تولید یک شیء بر اساس داده های ورودی می باشد.

از طرف دیگر Polymorphism بخشی از برنامه نویسی شیء گرا است و در قسمت آینده به طور مفصل به آن می پردازیم اما به صورت خلاصه بدانید که polymorphism به گرفتن شکل های مختلف گفته می شود و در فارسی به آن «چندریختگی» می گوییم. یعنی تابع یا شیء ای را داشته باشیم که همیشه به یک شکل و بدون تغییر مورد استفاده قرار می گیرد اما نتیجه لزوما یکی نخواهد بود بلکه بر اساس مولفه دیگری نحوه رفتار آن تابع را مشخص می کنیم. به مثال ساده زیر نگاهی بیندازید:

function buildUserByType(name, type) {

    let greetFn;

    if (type === 'friendly') {

        greetFn = function () {

            console.log('Hi, nice to meet you!');

        };

    } else if (type === 'unfriendly') {

        greetFn = function () {

            console.log('Hm? What do you want?');

        };

    }




    return {

        name: name,

        greet: greetFn,

    };

}




const friendlyUser = buildUserByType('Amir', 'friendly');

friendlyUser.greet(); // Hi, nice to meet you!




const unfriendlyUser = buildUserByType('Amir', 'unfriendly');

unfriendlyUser.greet(); // Hm? What do you want?

ما در این مثال factory function ها و همچنین چند ریختگی را در هم ترکیب کرده ایم. این تابع نام کاربر و سپس پارامتر دیگری به نام type را می گیرد که نوع کاربر را مشخص می کند (friendly یا دوستانه و unfriendly یا غیر دوستانه). حالا بر اساس آرگومان type یکی از دو factory function های ما اجرا می شوند که کارشان ساخت یک تابع است. همانطور که گفتم factory function ها چیزی را تولید می کنند که می تواند یک شیء، یک آرایه یا یک تابع و غیره باشد. اگر دقت کنید هر کدام از این توابع برگردانده شده نتیجه متفاوتی دارد (رشته متفاوتی را log می کند). در نهایت شیء ای که برگردانده می شود دارای name و greet است. در انتهای این کد، یک کاربر با یک نام را دوبار به این تابع پاس داده ایم اما به دلیل تفاوت آرگومان دوم، نتایج متفاوتی را دریافت کرده ایم.

ما می توانیم از همین موضوع در کدهای خودمان استفاده کنیم تا تکرار کدها را از بین ببریم، یعنی تابع پردازش تراکنش یکی باشد و تنها رفتارش در مقابل PAYMENT یا REFUND متفاوت باشد. در حال حاضر متدی به نام processByMethod در کدهای ما وجود دارد که انواع مختلف توابع را صدا می زند:

function processByMethod(transaction) {

  if (usesTransactionMethod(transaction, 'CREDIT_CARD')) {

    processCreditCardTransaction(transaction);

  } else if (usesTransactionMethod(transaction, 'PAYPAL')) {

    processPayPalTransaction(transaction);

  } else if (usesTransactionMethod(transaction, 'PLAN')) {

    processPlanTransaction(transaction);

  }

}

ما می خواهیم برای حل این مشکل یک factory function را بسازیم که یک شیء polymorphic یا چند ریخته را برمی گرداند. درون این شیء توابع مختلفی وجود خواهد داشت که بر اساس ورودی اجرا می شوند اما ظاهر شیء و نام آن همیشه ثابت است و کد تمیزتر می شود. توابع در جاوا اسکریپت و همچنین بسیاری از زبان های برنامه نویسی callable یا قابل فراخوانی هستند اما لزوما اینطور نیست! یعنی توانایی صدا زده شدن و همچنین صدا زده نشدن را دارند! برای انجام این کار ابتدا تابع processByMethod را حذف می کنیم و به جای آن تابع جدیدی به نام getTransactionProcessors را به شکل زیر تعریف می نماییم. البته محتوای تابع processByMethod را درون این تابع جدید قرار می دهیم:

function getTransactionProcessors(transaction) {

  let processors = {

    processPayment: null,

    processRefund: null,

  };




  if (usesTransactionMethod(transaction, 'CREDIT_CARD')) {

    processCreditCardTransaction(transaction);

  } else if (usesTransactionMethod(transaction, 'PAYPAL')) {

    processPayPalTransaction(transaction);

  } else if (usesTransactionMethod(transaction, 'PLAN')) {

    processPlanTransaction(transaction);

  }

}

کدهای قبلی processByMethod درون این تابع جدید قرار داده شده است اما من شیء ای به نام  processors را ساخته ام که دو خصوصیت processPayment و processRefund را دارد. این خصوصیت ها قرار است تعریف تابع را در خودشان داشته باشند. چطور؟ مثلا ما می دانیم که متدی به نام processCreditCardPayment در کدهایمان وجود دارد که مسئول پردازش پرداخت ها با کارت اعتباری است:

function processCreditCardPayment(transaction) {

  console.log(

    'Processing credit card payment for amount: ' + transaction.amount

  );

}

با این حساب ما می توانیم در متد getTransactionProcessors به چنین شکلی عمل کنیم:

function getTransactionProcessors(transaction) {

  let processors = {

    processPayment: null,

    processRefund: null,

  };




  if (usesTransactionMethod(transaction, 'CREDIT_CARD')) {

    processors.processPayment = processCreditCardPayment;

  } else if (usesTransactionMethod(transaction, 'PAYPAL')) {

    processPayPalTransaction(transaction);

  } else if (usesTransactionMethod(transaction, 'PLAN')) {

    processPlanTransaction(transaction);

  }

}

من کد درون اولین شرط if را بازنویسی کرده ام. همانطور که می بینید حالا خصوصیت processPayment به متد processCreditCardPayment اشاره می کند. توجه داشته باشید که ما متد processCreditCardPayment را صدا نزده ایم بلکه فقط به آن اشاره کرده ایم (در جلوی نام آن هیچ پرانتزی نیست). چرا این کار را انجام می دهیم؟ همانطور که گفتم factory function ها چیزی را می سازند و برایمان برمی گردانند نه اینکه عملیات خاصی را روی داده ها انجام بدهند. با این حساب باید این کار را برای دیگر متد ها نیز انجام بدهیم و در نهایت شیء processors را برگردانیم:

function getTransactionProcessors(transaction) {

    let processors = {

      processPayment: null,

      processRefund: null,

    };




    if (usesTransactionMethod(transaction, 'CREDIT_CARD')) {

      processors.processPayment = processCreditCardPayment;

      processors.processRefund = processCreditCardRefund;

    } else if (usesTransactionMethod(transaction, 'PAYPAL')) {

      processors.processPayment = processPayPalPayment;

      processors.processRefund = processPayPalRefund;

    } else if (usesTransactionMethod(transaction, 'PLAN')) {

      processors.processPayment = processPlanPayment;

      processors.processRefund = processPlanRefund;

    }




    return processors;

  }

حالا با صدا زدن این متد شیء processors را دریافت می کنیم. در مرحله بعدی به متد processTransaction می رویم و از این متد استفاده می کنیم:

  function processTransaction(transaction) {

    try {

      validateTransaction(transaction);




      const processors = getTransactionProcessors(transaction);




      if (isPayment(transaction)) {

        processors.processPayment(transaction);

      } else {

        processors.processRefund(transaction);

      }

    } catch (error) {

      showErrorMessage(error.message, error.item);

    }

  }

یعنی پس از اعتبارسنجی توسط متد validateTransaction متد getTransactionProcessors را صدا می زنیم و نتیجه اش را درون متغیری به نام processors ذخیره می کنیم. حالا اگر تراکنش از نوع PAYMENT یا پرداخت باشد،‌ متد processPayment از شیء‌ processors را صدا می زنیم و در غیر این صورت حتما تراکنش از نوع REFUND است بنابراین processRefund را صدا می زنیم. من قبلا هم گفتم که بهتر است بلوک های try & catch شما کمی خلوت بوده و یک عملیات خاص را انجام دهند. به همین دلیل این قسمت از کدها را به تابع جداگانه ای منتقل می کنیم. من برای این کار یک تابع جدید به نام processWithProcessor را تعریف می کنم و همین منطق را درون آن می نویسیم:

  function processWithProcessor(transaction) {

    const processors = getTransactionProcessors(transaction);




    if (isPayment(transaction)) {

      processors.processPayment(transaction);

    } else {

      processors.processRefund(transaction);

    }

  }

این همان منطقی است که قبلا نوشته بودیم بنابراین من آن را دوباره توضیح نمی دهم. در مرحله بعدی به متد processTransaction برمی گردیم و processWithProcessor را در آن صدا می زنیم:

  function processTransaction(transaction) {

    try {

      validateTransaction(transaction);

      processWithProcessor(transaction);

    } catch (error) {

      showErrorMessage(error.message, error.item);

    }

  }

در صورتی که شما دوست ندارید مثل من عمل کنید، هیچ اشکالی ندارد که کدها را به حالت قبلی برگردانید (تابع جداگانه ای به نام processWithProcessor را تعریف نکنید).

پارامترهای پیش فرض

آخرین روشی که به آن اشاره کرده بودم، استفاده از default parameters یا پارامترهای پیش فرض است. این روش نیازی به توضیحات طولانی ندارد چرا که تمام شما می دانید پارامترهای پیش فرض چه هستند اما من یک مثال از آن را در کد خودمان به شما نشان می دهم. ما در حال حاضر تابعی به نام showErrorMessage را داریم:

function showErrorMessage(message, item) {

  console.log(message);

  if (item) {

    console.log(item);

  }

}

همانطور که مشخص است برخی اوقات فقط یک پیام را با استفاده از این متد چاپ می کنیم و در برخی مواقع دیگر یک داده را نیز همراه آن چاپ خواهیم کرد. این کد هیچ مشکلی ندارد اما می تواند بهتر باشد. به طور مثال واقعا نیازی به شرط if اضافی درون آن نداریم و می توانیم از پارامترهای پیش فرض برای حل این مشکل استفاده کنیم. چطور؟ به کد زیر نگاه کنید:

function showErrorMessage(message, item = {}) {

  console.log(message);

  console.log(item);

}

طبیعتا اگر بخواهیم item را همیشه چاپ کنیم، ممکن است به مشکل برخورد کنیم چرا که item ممکن است اصلا پاس داده نشده و وجود نداشته باشد. برای حل این مشکل item را به صورت پیش فرض روی یک شیء خالی قرار می دهیم تا همیشه مقدار داشته باشد و حالا می توانیم message و همچنین item را همیشه log کنیم. حتی بهتر، می توانیم هر دو را در یک دستور قرار بدهیم:

function showErrorMessage(message, item = {}) {

  console.log(message, item);

}

این بسته به سلیقه خودتان دارد اما در هر حال با انجام این کار کد خود را بسیار تمیز تر کرده ایم.

رشته های ساده یا ثابت ها؟

در حال حاضر در طول کدهایمان همیشه از رشته هایی مانند "PAYPAL" یا "CREDIT_CARD" برای بررسی نوع تراکنش استفاده می کنیم. این کار از نظر قراردادهای برنامه نویسی کار جالبی نیست چرا که احتمال خطا در برنامه را بالا می برد. چطور؟ به دلیل اینکه رشته ها را به صورت دستی می نویسید و دستی نوشتن رشته ها احتمال خطا را بالا می برد. مثلا ممکن است در بخشی از برنامه به جای PAYPAL از PAYPL یا PAPAL استفاده کنید.

اگر زبان برنامه نویسی شما از ENUM ها پشتیبانی می کند بهتر است از آن ها برای ذخیره این رشته ها استفاده کنید اما اگر زبان برنامه نویسی شما مانند جاوا اسکریپت چیزی به نام ENUM ندارد می توانیم از ثابت ها (CONST) استفاده نماییم:

const TYPE_CREDIT_CARD = 'CREDIT_CARD';

// ...

if (transaction.type === TYPE_CREDIT_CARD) { ... }

با انجام این کار نیازی به تایپ کردن دستی رشته ها نیست و احتمال خطا پایین می آید. در حال حاضر تمام کدهای شما باید بدین شکل باشند:

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',

    },

  ];




  try {

    processTransactions(transactions);

  } catch (error) {

    showErrorMessage(error.message);

  }

}




function processTransactions(transactions) {

  validateTransactions(transactions);




  for (const transaction of transactions) {

    processTransaction(transaction);

  }

}




function validateTransactions(transactions) {

  if (isEmpty(transactions)) {

    const error = new Error('No transactions provided!');

    error.code = 1;

    throw error;

  }

}




function isEmpty(transactions) {

  return !transactions || transactions.length === 0;

}




function showErrorMessage(message, item = {}) {

  console.log(message);

  console.log(item);

}




function processTransaction(transaction) {

  try {

    validateTransaction(transaction);

    processWithProcessor(transaction);

  } catch (error) {

    showErrorMessage(error.message, error.item);

  }

}




function isOpen(transaction) {

  return transaction.status === 'OPEN';

}




function validateTransaction(transaction) {

  if (!isOpen(transaction)) {

    const error = new Error('Invalid transaction type.');

    throw error;

  }




  if (!isPayment(transaction) && !isRefund(transaction)) {

    const error = new Error('Invalid transaction type!');

    error.item = transaction;

    throw error;

  }

}




function processWithProcessor(transaction) {

  const processors = getTransactionProcessors(transaction);




  if (isPayment(transaction)) {

    processors.processPayment(transaction);

  } else {

    processors.processRefund(transaction);

  }

}




function getTransactionProcessors(transaction) {

  let processors = {

    processPayment: null,

    processRefund: null,

  };

  if (usesTransactionMethod(transaction, 'CREDIT_CARD')) {

    processors.processPayment = processCreditCardPayment;

    processors.processRefund = processCreditCardRefund;

  } else if (usesTransactionMethod(transaction, 'PAYPAL')) {

    processors.processPayment = processPayPalPayment;

    processors.processRefund = processPayPalRefund;

  } else if (usesTransactionMethod(transaction, 'PLAN')) {

    processors.processPayment = processPlanPayment;

    processors.processRefund = processPlanRefund;

  }

  return processors;

}




function usesTransactionMethod(transaction, method) {

  return transaction.method === method;

}




function isPayment(transaction) {

  return transaction.type === 'PAYMENT';

}




function isRefund(transaction) {

  return transaction.type === 'REFUND';

}




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);

}

همچنین در نظر داشته باشید که ما در پروژه های واقعی تمام این کدها را درون یک فایل نمی نویسیم و شلوغی این فایل نباید شما را نگران کند. در پروژه های واقعی معمولا کدها را درون فایل های جداگانه تقسیم می کنیم. در جلسه بعد به سراغ کلاس ها و تمیزنویسی کد در برنامه نویسی شیء گرا می رویم. در آن فصل بیشتر در مورد polymorphism صحبت خواهیم کرد.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری کدنویسی تمیز توصیه می‌کند:
نویسنده شوید

دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.