Lesson Monday

In the last lesson, we focused on the importance of keeping our business logic and UI logic separate. We could've updated our numberOfOccurrencesInText() function to do multiple things but this is a bad practice. A function should just do one thing if possible. We want to have separation of concerns, which means each function is concerned about just one thing and doesn't worry about anything else. That means numberOfOccurrencesInText() just cares about counting the number of occurrences of a substring in a string while boldPassage() should bold matches. Writing a function that did both things wouldn't be good even if it results in fewer lines of code.

In this lesson, we're going to discuss another very important programming concept known as DRY, which means Don't Repeat Yourself. There are a lot of good reasons not to repeat yourself:

  • It results in unnecessary repeated code.
  • It's harder to read and reason about because there's extra repeated code to deal with and read.
  • If the code breaks or it needs to be updated, we have to change it in multiple places, not just one.

If you really practice separating concerns and keeping your code DRY, you are making a huge step towards writing amazing code that clearly communicates your intentions. These are some of the most important techniques you can learn as a coder.

For our text analyzer application, that leaves us with a bit of an issue since boldPassage() and numberOfOccurrencesInText() kinda do the same thing. Let's put the functions next to each other:

// numberOfOccurrencesInText()

function numberOfOccurrencesInText(word, text) {
  if ((text.trim().length === 0) || (word.trim().length === 0)) {
    return 0;
  }
  const wordArray = text.split(" ");
  let wordCount = 0;
  wordArray.forEach(function(element) {
    if (element.toLowerCase().includes(word.toLowerCase())) {
      wordCount++;
    }
  });
  return wordCount;
}

// boldPassage()

function boldPassage(word, text) {
  if ((text.trim().length === 0) || (word.trim().length === 0)) {
    return "";
  }
  let htmlString = "<p>";
  let textArray = text.split(" ");
  textArray.forEach(function(element, index) {
    if (element.toLowerCase().includes(word.toLowerCase())) {
      htmlString = htmlString.concat("<b>" + element + "</b>");
    } else {
      htmlString = htmlString.concat(element);
    }
    if (index !== (textArray.length - 1)) {
      htmlString = htmlString.concat(" ");
    }
  });
  return htmlString + "</p>";
}

As you might guess, sometimes separating our code makes it harder not to repeat ourselves. If we'd just put this all in one function, it would be DRY but have poor separation of logic. With good separation of logic, though, it's not as DRY. Often, the best way to handle this is to extract any repeated code into its own function.

Let's identify a small way we can DRY up this code. Note that the first conditional we use is exactly the same. The only difference is what it returns.

We can extract this into its own function. We will put this at the top of our file and call it Utility Logic. You'll see the reason for the name later in this lesson.

scripts.js
// Utility Logic

function noInputtedWord(word, text) {
  return ((text.trim().length === 0) || (word.trim().length === 0));
}

This will return a boolean. If either the word or the text are empty strings, it will return true. Otherwise, it will return false.

Next, we can plug it into the functions above. Here's how we'd do so with numberOfOccurrencesInText():

scripts.js
function numberOfOccurrencesInText(word, text) {
  if (noInputtedWord(word, text)) {
    return 0;
  }
  const wordArray = text.split(" ");
  let wordCount = 0;
  wordArray.forEach(function(element) {
    if (element.toLowerCase().includes(word.toLowerCase())) {
      wordCount++;
    }
  });
  return wordCount;
}

As we can see, instead of checking ((text.trim().length === 0) || (word.trim().length === 0)), it now checks noInputtedWord(word, text).

Hmm... doesn't seem like much of an improvement. Is it really worth it?

Well, imagine if we were using that same code in ten different functions and we realized that we also wanted to account for punctuation. For instance, if someone enters the following: numberOfOccurrencesInText("!", ".");, we want noInputtedWord() to return false, not true.

Would you rather update that code in one place (the noInputtedWord() function) or in ten different functions? Also, what if in the process of updating the code in ten different places, you missed an eleventh place in the code that needed to be updated as well?

Also, sometimes you'll find that you're able to DRY up a much larger chunk of code. For instance, imagine that we have many different functions that make sure that every input is an actual English word. That involves a lot of different steps. Having a separate function do the work makes a lot of sense. So while this is a very small example of DRYing our code, it illustrates the basic principle of extracting repeated code into its own function.

These kinds of functions are sometimes known as helper or utility functions. You should look for these kinds of opportunities to DRY up your code wherever possible. While the example here is a very small one, it illustrates how we can keep our code DRY with helper functions while still keeping our business and user interface logic separate.

There's one more thing we need to do to get our application working. We've written our boldPassage() function but we aren't calling it yet. It needs to be called when the form is submitted. We can do this with a single added line of code in our UI logic:

scripts.js
...

$(document).ready(function(){
  $("form#word-counter").submit(function(event){
    event.preventDefault();
    const passage = $("#text-passage").val();
    const word = $("#word").val();
    const wordCount = wordCounter(passage);
    const occurrencesOfWord = numberOfOccurrencesInText(word, passage);
    $("#total-count").html(wordCount);
    $("#selected-count").html(occurrencesOfWord);

    // New line of code below.
    $("#bolded-passage").html(boldPassage(word, passage));
  });
});

Look how nice and clean that is! No logic cluttering up this section of the code at all. Instead, it's totally separated out. Even though it is a function that deals with UI logic, it doesn't directly alter the DOM. It just returns a string of HTML. That makes it easy to test and easy to separate out. Then we can just call the function when we need it to get an HTML string and we use jQuery to actually add it to the DOM.

Here's what our full scripts.js file looks like now:

scripts.js
// Utility Logic

function noInputtedWord(word, text) {
  return ((text.trim().length === 0) || (word.trim().length === 0));
}

// Business Logic

function wordCounter(text) {
  if (text.trim().length === 0) {
    return 0;
  }
  let wordCount = 0;
  const wordArray = text.split(" ");
  wordArray.forEach(function(element) {
    if (!Number(element)) {
      wordCount++;
    }
  });
  return wordCount;
}

function numberOfOccurrencesInText(word, text) {
  if (noInputtedWord(word, text)) {
    return 0;
  }
  const wordArray = text.split(" ");
  let wordCount = 0;
  wordArray.forEach(function(element) {
    if (element.toLowerCase().includes(word.toLowerCase())) {
      wordCount++;
    }
  });
  return wordCount;
}

// UI Logic

function boldPassage(word, text) {
  if (noInputtedWord(word, text)) {
    return "";
  }
  let htmlString = "<p>";
  let textArray = text.split(" ");
  textArray.forEach(function(element, index) {
    if (element.toLowerCase().includes(word.toLowerCase())) {
      htmlString = htmlString.concat("<b>" + element + "</b>");
    } else {
      htmlString = htmlString.concat(element);
    }
    if (index !== (textArray.length - 1)) {
      htmlString = htmlString.concat(" ");
    }
  });
  return htmlString + "</p>";
}

$(document).ready(function(){
  $("form#word-counter").submit(function(event){
    event.preventDefault();
    const passage = $("#text-passage").val();
    const word = $("#word").val();
    const wordCount = wordCounter(passage);
    const occurrencesOfWord = numberOfOccurrencesInText(word, passage);
    $("#total-count").html(wordCount);
    $("#selected-count").html(occurrencesOfWord);
    $("#bolded-passage").html(boldPassage(word, passage));
  });
});

Our code is nicely separated and we even added a little function to DRY things up a bit. Too bad we can't use our little helper function on wordCounter(), too. Or can we? In a future lesson, we'll refactor noInputtedWord() so we can use it in the wordCounter() function, too.

Terminology

Don't Repeat Yourself: Known as DRY for short. This is another essential programming concept. We should avoid repeating code where possible. There are a lot of good reasons not to repeat yourself:

  • It results in unnecessary repeated code.
  • It's harder to read and reason about because there's extra repeated code to deal with and read.
  • If the code breaks or it needs to be updated, we have to change it in multiple places, not just one.

Helper/Utility function: A function, often small, with reusable code. We can use helper functions to DRY up our code by removing repeated code, moving it into the helper function, and then calling that helper function wherever that code is needed. Much less repetition!

Lesson 7 of 15
Last updated April 7, 2021