App 2: Handling Add

Next up, the “Add” use case is a little bit more complicated. Going back to our UI, we know that when a user wants to add a new task, they type things like this with the a command:

a wake up
a brush teeth
a take a long hot shower

So the first thing we need to do is to separate that add command — the letter a — from the rest of the string. Thanks to the power of Scala’s collections methods, we do this with the drop method that’s available on all strings:

"a wake up".drop(2)                   // "wake up"
"a brush teeth".drop(2)               // "brush teeth"
"a take a long hot shower".drop(2)    // "take a long hot shower"

The drop method is available because a String is also a list, specifically a Seq[Char].

This approach seems to work well, giving us a string that we can pass to another function, so that function can add the string to our database. So, looking only at the “add” case, I write this:

case add if add.startsWith("a ") => 
    handleAdd(add.drop(2))

handleAdd

So now I need to write a handleAdd function, and I know three things about it already:

  • It’s going to take a String as input
  • It needs to add/insert that task into our database
  • Because it’s going to access the database, it needs to return a Try

Therefore, I can start to sketch its signature like this:

def handleAdd(task: String): Try

And because I wrote the database code earlier, I know that handleAdd just needs to have a reference to the database object so it can perform the insert. Recall that this function is inside the InputProcessor class, which I defined to take a Database reference, like this:

class InputProcessor(db: Database)

So now handleAdd has access to this db reference.

This is one situation where it’s acceptable for a function to have a side door: when that function is declared inside a class that has a reference like this. (However, if you don’t like this, there are other ways to write this code to make sure a Database reference is passed in through the function’s front door (i.e., its input parameter list)).

Getting back to the handleAdd function, I know that (a) I want to call the insert function on the db reference, and (b) it returns a Try[Unit]. So now I write handleAdd like this:

def handleAdd(taskName: String): Try[Unit] =
    db.insert(taskName)

At this point, this case code in the match expression inside the handleUserInput function could be the end of the case:

case add if add.startsWith("a ") => 
    handleAdd(add.drop(2))

However, because I know from testing the application that I want to see what my tasks look like after I issue my add command, I know that I also need to run the “view” function now. Therefore, I add it after handleAdd:

case add if add.startsWith("a ") => 
    handleAdd(add.drop(2))
    handleView()

And because handleView returns Try[Unit], the return type of this case matches the return type of handleUserInput. So now handleUserInput has these cases in its match expression:

def handleUserInput(input: String): Try[Unit] = input match
    case "q" => 
        Try(System.exit(0))
    case "h" => 
        IOHelper.showHelp()
    case "v" | "l" => 
        handleView()
    case add if add.startsWith("a ") =>     // <== THE NEW CODE
        handleAdd(add.drop(2))
        handleView()
    // more cases here ...