2019年8月13日 星期二

Take advantage of variables

I think those two concepts (maybe call them philosophy) are the most useful techniques in Object-Oriented Design.

  • DRY: Extract repeated or similar functions and reuse them.
  • Single Responsibility: Keep your small chunk of code simple.

I would like to add one more thing that I usually call three of them "Good Code Smell"

  • Take advantage of variables.

We normally write code directly like this:

function checkScore {
  if (score > 50) {
    // do something
  }
}

When the condition becomes complicated:

function checkScore {
  if (score > 50 && score <= 100) {
    // do something
  }
}

More complicated:

function checkScore {
  if (score > 50 && score <= 100 && seconds > 60 && seconds <= 120) {
    // do something
  }
}

Well, all of them look ok, but how about this one:

function checkTenPercentOff() {
  if (
    // check qualified user
    user.title == 'Gold Member' && user.points > 1000 &&
    // check cart is not empty
    cartItems != null && cartItems != undefined && cartItems.length > 0 &&
    // check each item's quantity
    checkQuantity(cartItems) &&
    // check special sales period
    now > new Date('2019-09-01T00:00:00') && now <= new Date('2019-12-31T00:00:00')
  ) {
    // do 10% off for the user
  }
}

This is a monster if statement although it may still simple compared with real requirements from an SRS.

Now, we can take advantage of variable names.

function checkTenPercentOff() {
  // Look, we even don't need comments if we have good variable names
  var isQualifiedUser = user.title == 'Gold Member' && user.points > 1000;
  var isNotEmptyCart = cartItems != null && cartItems != undefined && cartItems.length > 0;
  var allItemsHaveQuantity = checkQuantity(cartItems);
  var isSpecialSalesPeriod = now > Date('2019-09-01T00:00:00') && now <= new Date('2019-12-31T00:00:00');

  var isQualifiedForTenPercentOff =
    isQualifiedUser &&
    isNotEmptyCart &&
    allItemsHaveQuantity &&
    isSpecialSalesPeriod;

  if (isQualifiedForTenPercentOff) {
    // do 10% off for the user
  }
}

It looks much clear and easy to read now.

Remember the DRY and Single Responsibility? How about this:

// extract variables as parameters, then you don't hard-code a function.

function checkQualifiedUser(title, points) {
  var isQualifiedUser = user.title == title && user.points > points;
  return isQualifiedUser;
}

function checkNotEmptyCart(cartItems) {
  var isNotEmptyCart = cartItems != null && cartItems != undefined && cartItems.length > 0;
  return isNotEmptyCart;
}

function checkAllItemsHaveQuantity(cartItems) {
  var allItemsHaveQuantity = checkQuantity(cartItems);
  return allItemsHaveQuantity;
}

function checkSpecialSalesPeriod(startDate, endDate) {
  var isSpecialSalesPeriod = now > startDate && now <= endDate;
  return isSpecialSalesPeriod;
}

function checkTenPercentOff() {
  var isQualifiedForTenPercentOff =
    checkQualifiedUser('Gold Member', 1000) &&
    checkNotEmptyCart(cartItems) &&
    checkAllItemsHaveQuantity(cartItems) &&
    checkSpecialSalesPeriod(new Date('2019-09-01T00:00:00'), new Date('2019-12-31T00:00:00'));

  if (isQualifiedForTenPercentOff) {
    // do 10% off for the user
  }
}

function checkFifteenPercentOff() {
  var isQualifiedForFifteenPercentOff =
    checkQualifiedUser('Diamond Member', 2000) &&
    checkNotEmptyCart(cartItems) &&
    checkAllItemsHaveQuantity(cartItems) &&
    checkSpecialSalesPeriod(new Date('2020-01-01T00:00:00'), new Date('2020-06-31T00:00:00'));

  if (isQualifiedForFifteenPercentOff) {
    // do 15% off for the user
  }
}

Now, we have reusable(DRY) and short(Single Responsibility) functions and clear variable names(Take advantage of variables)

2019年7月23日 星期二

Reuse, but don't make a huge function

  • The concept of DRY is super simple - do not duplicate codes if there are already in the program. The basic practice is using a function to do a particular action and let other places call the function if they need.
      // bad
      const name1 = 'Bob'
      const msg1 = 'Hi'
      console.log(`${name1} says: ${msg1}`)
    
      const name2 = 'Tom'
      const msg2 = `How's it going?`
      console.log(`${name2} says: ${msg2}`)
    
      // good
      function say(name, message) {
        console.log(`${name} says: ${message}`)
      }
    
      say('Bob', 'Hi')
      say('Tom', `How's it going?`)
    
  • However, once the requirement changed and we modify the function, of course, all function calls will be affected. That's good we save time.
      function say(from, to, message) {
        console.log(`${from} says ${to}: ${message}`)
      }
    
      say('Bob', 'John', 'Hi')
      say('Tom', 'Peter', `How's it going?`)
    
  • It is very common that we use parameters as inputs to let a function deal with for different situations.
      function say(from, to, message) {
        if (to !== null && to !== undefined) {
          console.log(`${from} says ${to}: ${message}`)
        } else {
          console.log(`${from} says: ${message}`)
        }
      }
    
      say('Bob', 'John', 'Hi')
      say('Tom', null, `How's it going?`)
    
  • You know, the requirement might change many times because customers are indecisive. Even if they don't, we still have to do refactoring in order to have better code quality. That means we might change codes.
      function say(from, to, message, isQuestion) {
        const ending = isQuestion ? '?' : '.'
        if (to !== null && to !== undefined) {
          console.log(`${from} says ${to}: ${message}${ending}`)
        } else {
          console.log(`${from} says: ${message}${ending}`)
        }
      }
    
      say('Bob', null, 'Hi', false)
      say('Tom', 'Peter', `How's it going`, true)
    
  • You will realize that functions have more complex parameters and conditions are very hard to modify. It calls "high coupling" if there are too many places rely on a heavy code.
      // the combinations you need to test in order to make sure your function works:
      say('name', null, 'message', false)
      say('name', 'name', 'message', false)
      say('name', null, 'message', true)
      say('name', 'name', 'message', true)
    
      // the combinations you may not expect that will ruin your function:
      say(null, 'name', 'message', false)
      say('name', 'name', null, true)
    
  • Therefore, when you are trying to DRY your code, be careful that don't put too many codes into one function. The function will be super complex and hard to maintain. It may be a good idea to have small functions instead.
      // there are actually 4 scenarios, so it may be a good idea to have 4 functions
      // new function names are more semantic
      // there are no magical boolean flag and optional parameters (reduce parameter mistakes)
      // we get rid of if conditions (reduce the complexity)
      // once one of the functions changed, we won't affect other functions
    
      function talk(name, message) {
        console.log(`${name} says: ${message}.`)
      }
    
      function askQuestion(name, message) {
        console.log(`${name} says: ${message}?`)
      }
    
      function talkToSomeone(name, someone, message) {
        console.log(`${name} says to ${someone}: ${message}.`)
      }
    
      function askQuestionToSomeone(name, someone, message) {
        console.log(`${name} says to ${someone}: ${message}?`)
      }
    
      talk('Bob', 'Hi')
      askQuestion('Tom', `How's it going`)
      talkToSomeone('Bob', 'John', 'Hi')
      askQuestionToSomeone('Tom', 'Peter', `How's it going`)
    
  • This example is overly simple, and the naming actually sucks, but I hope I give you a general idea of how to organize your functions.