Factory Function and Validation By albro

Factory Function and Validation

Writing clean and readable code requires practice, and the best practice is rewriting bad code many times. Currently our codes still have a serious problem; We use errors, which are great, but currently the error generation and throwing is right next to the code that is responsible for processing transactions. Example:

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

Naturally, doing such a thing will combine different levels of code in one function. A better way to do this is to have a separate mechanism for validating transactions. I define a function called validateTransactions and write the creation and error throwing logic in it:

function validateTransactions(transactions) {
  if (isEmpty(transactions)) {
    const error = new Error('No transactions provided!');
    error.code = 1;
    throw error;
  }
}

This method receives our transaction and if isEmpty is true (the transaction is empty) it creates a new error and then throws it with code 1. We don't really need the code 1 and you can remove it or replace it with another number. My point in not removing the code is so that you know you have the ability to do so. In the next step, we go to the processTransactions function and delete the error throwing part in it and call the validateTransactions function instead:

function processTransactions(transactions) {
  validateTransactions(transactions);


for (const transaction of transactions) { try { processTransaction(transaction); } catch (error) { showErrorMessage(error.message, error.item); } } }

By doing this, we have made the processTransactions function quieter and we have used errors, and the logic of creating and throwing errors has not been combined with the operational logic of the program. In addition, if an error is thrown, the execution of that part of the program will be interrupted, so if the transaction is empty, an error will be thrown by the validateTransactions function, and in the processTransactions function, we will never reach the for loop in it, but if the transaction is not empty, the validateTransactions function will do nothing. does not do and the for loop is done normally.

In addition, you may say that we have several different levels of code in the validateTransactions function (the isEmpty function in addition to low-level codes such as throw error), but in my opinion, breaking this simple function into another function is 100% overwriting. If you want to do this, you should define a separate class for error handling and give it full wings to have the entire error handling system of your application.

Now take a look at the processTransaction function:

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

As you can see, this function, like our other function, creates and throws an error within itself, so we must extract this section and put it inside its own function. For this I define another function called validateTransaction (without the s plus at the end):

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

First we need to get the transaction and then we have two separate if conditions. Why? Because the processTransaction function has two parts to create the error and I want to put both parts in one function. The first condition is that the transaction is not open (it is closed). If the transaction is closed, it no longer needs to be processed, so we create and throw an error. The second condition is that the transaction is neither a payment type nor a refund type. In this case, we make an error again and throw it. The only thing left is to call this function in 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); } }

Error management has a single goal

The important thing about error handling is this: each error handling code (each try - catch block) has a single purpose and performs a single task. For example, we have the following code in the main function:

try {
  processTransactions(transactions);
} catch (error) {
  showErrorMessage(error.message);
}

This code shows exactly the same important point, in the try section we perform a single operation and in the catch section we receive possible errors. Now take a look at the processTransactions function:

function processTransactions(transactions) {
  validateTransactions(transactions);


for (const transaction of transactions) { try { processTransaction(transaction); } catch (error) { showErrorMessage(error.message, error.item); } } }

In this function, our try - catch block is inside a for loop and is no longer a single operation. Although there is nothing wrong with writing code in this way, it is better to avoid it. A simple solution is to remove the try - catch block from the processTransactions method:

function processTransactions(transactions) {
  validateTransactions(transactions);


for (const transaction of transactions) { processTransaction(transaction); } }

In the next step, we pass this block to the processTransaction method:

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

This is one way to solve this problem. Currently, our code only performs a single operation and does not loop through a for loop. Of course, at the moment our code is out of readability, and although it is not a bad thing to leave it like this, I suggest to extract the part of the if conditions from it and put it in a separate method:

function processByMethod(transaction) {
  if (usesTransactionMethod(transaction, 'CREDIT_CARD')) {
    processCreditCardTransaction(transaction);
  } else if (usesTransactionMethod(transaction, 'PAYPAL')) {
    processPayPalTransaction(transaction);
  } else if (usesTransactionMethod(transaction, 'PLAN')) {
    processPlanTransaction(transaction);
  }
}

Now we can run this method in processTransaction after validation:

function processTransaction(transaction) {
  try {
    validateTransaction(transaction);
    processByMethod(transaction);
  } catch (error) {
    showErrorMessage(error.message, error.item);
  }
}

By doing this, our codes become more readable. Of course, this is my opinion and ultimately it is your personal opinion that determines how you will write your codes. If you follow me, all your code should be as follows:

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 and Polymorphism

Currently, our codes are pretty clean and standard, but we still have many conditions and some of our codes are repetitive. Factory Functions and Polymorphism are our solution to this problem, but before we want to use them, we must know what they are defined and what they do.

Factory Functions are functions that are responsible for creating specific data. what does it mean? Consider the following example:

function buildUser(name, age) {
  return {
    name: name,
    age: age,
  };
}

Factory functions usually produce an object or array or whatever. In the example above, we have a function that takes the user's name and age and then creates an object with these values. We call this function a factory function because it is responsible for producing an object based on input data.

On the other hand, polymorphism is a part of object-oriented programming and I will discuss it in detail in the next part, but in summary, know that polymorphism is called taking different forms. That is, to have a function or an object that is always used in the same way without change, but the result will not necessarily be the same, but based on another component, we determine how that function behaves. Take a look at the simple example below:

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('Albro', 'friendly'); friendlyUser.greet(); // Hi, nice to meet you!


const unfriendlyUser = buildUserByType('Albro', 'unfriendly'); unfriendlyUser.greet(); // Hm? What do you want?

In this example, we have combined factory functions as well as Polymorphism. This function takes the name of the user and then another parameter called type that specifies the type of user (friendly and unfriendly). Now, based on the type argument, one of our two factory functions are executed, whose job is to create a function. As I said, factory functions produce something that can be an object, an array, or a function, etc. If you look carefully, each of these functions returns a different result (logs a different string). Finally, the returned object has name and greet. At the end of this code, we have passed a user with the same name twice to this function, but due to the difference of the second argument, we have received different results.

We can use the same thing in our codes to eliminate code duplication, that is, the transaction processing function is the same and only its behavior is different in front of PAYMENT or REFUND. There is currently a method called processByMethod in our code that calls various types of functions:

function processByMethod(transaction) {
  if (usesTransactionMethod(transaction, 'CREDIT_CARD')) {
    processCreditCardTransaction(transaction);
  } else if (usesTransactionMethod(transaction, 'PAYPAL')) {
    processPayPalTransaction(transaction);
  } else if (usesTransactionMethod(transaction, 'PLAN')) {
    processPlanTransaction(transaction);
  }
}

We want to solve this problem by creating a factory function that returns a polymorphic object. Inside this object, there will be various functions that are executed based on the input, but the appearance of the object and its name will always be fixed, and the code will be cleaner. Functions in JavaScript, as well as many programming languages, are callable, but not necessarily so! That is, they have the ability to be called and also not to be called! To do this, we first remove the processByMethod function and instead define a new function called getTransactionProcessors as follows. Of course, we put the content of the processByMethod function inside this new function:

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

The previous code of processByMethod is placed inside this new function, but I have created an object called processors which has two properties processPayment and processRefund. These features are supposed to have the function definition in themselves. how about For example, we know that there is a method called processCreditCardPayment in our code that is responsible for processing credit card payments:

function processCreditCardPayment(transaction) {
  console.log(
    'Processing credit card payment for amount: ' + transaction.amount
  );
}

With this account, we can act in the getTransactionProcessors method as follows:

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

I have rewritten the code inside the first if condition. As you can see, now the processPayment attribute points to the processCreditCardPayment method. Note that we didn't call the processCreditCardPayment method, we just pointed to it (no parentheses in front of its name). Why do we do this? As I said, factory functions create something and return it to us, rather than performing specific operations on the data. With this account, we must do this for other methods as well and finally return the processors object:

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

Now by calling this method, we get the processors object. In the next step, we go to the processTransaction method and use this method:

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

That is, after validation by the validateTransaction method, we call the getTransactionProcessors method and store the result in a variable called processors. Now, if the transaction is of PAYMENT type, we call the processPayment method of the processors object, and otherwise, the transaction must be of REFUND type, so we call processRefund. I already said that it is better for your try - catch blocks to be a bit quiet and perform a specific operation. For this reason, we transfer this part of the codes to a separate function. For this, I define a new function called processWithProcessor and write the same logic inside it:

  function processWithProcessor(transaction) {
    const processors = getTransactionProcessors(transaction);


if (isPayment(transaction)) { processors.processPayment(transaction); } else { processors.processRefund(transaction); } }

This is the same logic we wrote earlier so I won't explain it again. In the next step, we return to the processTransaction method and call processWithProcessor in it:

  function processTransaction(transaction) {
    try {
      validateTransaction(transaction);
      processWithProcessor(transaction);
    } catch (error) {
      showErrorMessage(error.message, error.item);
    }
  }

If you don't want to do like me, it's okay to revert the code to the way it was before (don't define a separate function called processWithProcessor).

Default parameters

The last method I mentioned is using default parameters. This method doesn't need a long explanation because you all know what the default parameters are, but I'll show you an example of it in our own code. We already have a function called showErrorMessage:

function showErrorMessage(message, item) {
  console.log(message);
  if (item) {
    console.log(item);
  }
}

As it is known, sometimes we only print a message using this method and sometimes we will print a data along with it. This code has nothing wrong with it, but it could be better. For example, we don't really need the extra if condition inside it, and we can use the default parameters to solve this problem. how about Look at the code below:

function showErrorMessage(message, item = {}) {
  console.log(message);
  console.log(item);
}

Naturally, if we want to always print the item, we may face problems because the item may not be passed at all and may not exist. To solve this problem, we set the item to an empty object by default so that it always has a value, and now we can always log the message as well as the item. Even better, we can put both in the same statement:

function showErrorMessage(message, item = {}) {
  console.log(message, item);
}

This is up to your taste, but in any case, we've made our code much cleaner by doing this.

Simple strings or constants?

Currently throughout our code we always use strings like "PAYPAL" or "CREDIT_CARD" to check the transaction type. This work is not interesting in terms of programming conventions because it increases the possibility of errors in the program. how about Because you write the strings manually and manually writing the strings increases the possibility of error. For example, you may use PAYPL or PAPAL instead of PAYPAL in a part of the program.

If your programming language supports ENUMs, it is better to use them to store these strings, but if your programming language, like JavaScript, does not have something called ENUM, we can use constants (const):

const TYPE_CREDIT_CARD = 'CREDIT_CARD';
// ...
if (transaction.type === TYPE_CREDIT_CARD) { ... }

By doing this, there is no need to manually type the strings and the possibility of error is reduced. Now all your code should look like this:

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

Also keep in mind that in real projects we do not write all these codes in one file and the crowdedness of this file should not worry you. In real projects, we usually divide the codes into separate files.



[hive: @albro]



0
0
0.000
5 comments
avatar

Congratulations!


You have obtained a vote from CHESS BROTHERS PROJECT

✅ Good job. Your post has been appreciated and has received support from CHESS BROTHERS ♔ 💪


♟ We invite you to use our hashtag #chessbrothers and learn more about us.

♟♟ You can also reach us on our Discord server and promote your posts there.

♟♟♟ Consider joining our curation trail so we work as a team and you get rewards automatically.

♞♟ Check out our @chessbrotherspro account to learn about the curation process carried out daily by our team.


🥇 If you want to earn profits with your HP delegation and support our project, we invite you to join the Master Investor plan. Here you can learn how to do it.


Kindly

The CHESS BROTHERS team

0
0
0.000
avatar

Thanks for your contribution to the STEMsocial community. Feel free to join us on discord to get to know the rest of us!

Please consider delegating to the @stemsocial account (85% of the curation rewards are returned).

You may also include @stemsocial as a beneficiary of the rewards of this post to get a stronger support. 
 

0
0
0.000
avatar

Congratulations @albro! You have completed the following achievement on the Hive blockchain And have been rewarded with New badge(s)

You distributed more than 700 upvotes.
Your next target is to reach 800 upvotes.

You can view your badges on your board and compare yourself to others in the Ranking
If you no longer want to receive notifications, reply to this comment with the word STOP

0
0
0.000
avatar

This is a hive-archeology proxy comment meant as a proxy for upvoting good content that is past it's initial pay-out window.

image.png

Pay-out for this comment is configured as followed:

roleaccountpercentagenote
curator-0.0%curation rewards disabled
author@albro95.0%
dev@croupierbot2.5%author of hive-archology
dev@emrebeyler2.5%author of lighthive
0
0
0.000