DEV Community

Cover image for wk7: java cli app retro
Ashley D
Ashley D

Posted on

wk7: java cli app retro

2 weeks ago, we were tasked with using Java and OOP concepts to build a command line-application that could read a dictionary text file, run a variety of searches, log history, and allow addition and deletion of words (my project link: Sassy Dictionary).

Thanks to the retro I had with my mentor post-completion and feedback from my instructor, I got to dive further into 4 key concepts: code structure, abstraction and BiFunctions, resource management, and validation.

Code Structure

Variable Naming and Final Variable

To ensure the code is more readable, use variables to refer to values used in calculations and methods. In the Sassy class, we used use {int) (Math.random() * array.length) to randomize a message to display to users based.

My message categories were either arrays of 3 or 4 messages. To ensure clarity, I created two variables to reference that, and then used those variables instead of the number 3 or 4 in my methods.

For example, with a set of 3 welcome messages, the randomizer method would look like:

 private static final int  messageCount3 = 3;
    private static final int  messageCount4 = 4;

 public static void sayRandomWelcome() {
        System.out.println(welcomeMessages[(int) (Math.random() * 3  messageCount3)]);
}
Enter fullscreen mode Exit fullscreen mode

Notice how the variable makes it more readable vs having the number 3 there.

You may also see how it’s notated as final. It’s best practice to add “final” to variables, class fields, and method arguments when you want to communicate that the value should not be changed to prevent accidental modification.
In my project, there were a few variables declared where no additional calculations or manipulations were performed on them, so they could have been changed to final as well.

Keep the Main Class Clean

The Main class is typically where the methods defined in other classes are called. You want to avoid defining methods within the Main. In my Main DictionaryApp class, you can see a lengthy while (true) loop method that allows the menu to persist, handles user input, and also asks if help is still needed.

However, this method can be moved to the InputHandler class and called in the Main. We use a parameter to pass along the listOfWords variable defined in the main, so it’s accessible when it’s defined in the input class.

\\ Main class
InputHandler.menuLoop(listOfWords)


\\ InputHandler class
public static void menuLoop (List<Word> listOfWords) {
        while (true) {do this
}
Enter fullscreen mode Exit fullscreen mode

Keep it efficient

We want to avoid running code unnecessarily. An example is how in my while (true) loop, the first line initializes the word reader at the start of each loop to read the text file. This was done to ensure the text file is read after words after deleted or added. However, an easier way is to just add this logic within the addAWord and deleteAWord methods. These would be the lines to add to each:

// addAWord: add at bottom of the if statement 
WordWriter.addWord(word, definition, partOfSpeech, exampleUsage);
listOfWords.add(new Word(word, definition, partOfSpeech, exampleUsage));

//deleteAWord: add at bottom of the try statement
listOfWords.clear();
listOfWords.addAll(filteredWordsWithoutDeleted);
Enter fullscreen mode Exit fullscreen mode

You can see how the logic applied to listOfWords mirrors the logic of how we’re adding/deleting words back to the dictionary txt file. With deleting, we are clearing the listOfWords and then adding back the filtered words that no longer include the deleted one.

Abstraction and BiFunctions

Abstraction is the idea of looking at the methods and seeing what they share in common that we can refactor. In my MenuFinderMethods, we see that the methods all have a lambda return function that determines how the arrayList of words is filtered to return specific search results as well as the prompt to users.

We can just have one findAWord method instead of 5 separate ones. We can use a parameter of type BiFunction. A BiFunction takes two arguments (in this case type Word, and type String) and returns a result (type Boolean). Our BiFunction represents the lambda return method which was used to create that filtered list of words.

We call it by writing filterFunction.apply and this is followed by the parameter arguments of the word instance and searchTerm. When findAWordis called in MenuInteractions, the BiFunction is defined in the arguments list with code on how it will specifically interact with the word instances and searchTerm based on the specific search type the user wants to do.

//findAWord method: defined in MenuFinder Methods

public static void findAWord(final Scanner scanner, final List<Word> listOfWords, final BiFunction<Word, String, Boolean> filterFunction, final String prompt) {
    System.out.println(prompt);
…
    List<Word> filteredListOfWords = listOfWords.stream().filter(word -> filterFunction.apply(word, searchTerm)).toList();
[rest of if/else code logic here]
}
Enter fullscreen mode Exit fullscreen mode
//findAWord called in MenuInteractions
switch (choice) {

            case 1:
                // Find words by word match logic
                echoChoice (choice);
                MenuFinderMethods.findAWord(scanner, listOfWords, (word, searchTerm) -> word.getWord().toLowerCase().equals(searchTerm.toLowerCase()), "What word do you want to search by?");
                break;
            case 2:
                // Find words by definition logic
                echoChoice (choice);
                MenuFinderMethods.findAWord(scanner, listOfWords, (word, searchTerm) -> word.getDefinition().toLowerCase().contains(searchTerm.toLowerCase()), "What definition do you want to search by?");
                break;
            case 3:
                // Find all words that start with logic
                echoChoice (choice);
                MenuFinderMethods.findAWord(scanner, listOfWords, (word, searchTerm) -> word.getWord().toLowerCase().startsWith(searchTerm.toLowerCase()), "What prefix do you want to search by?");
                break;
Enter fullscreen mode Exit fullscreen mode

To keep it DRY, we also make the prompt a parameter, and when it’s called in Menu Interactions- the prompt is provided in the parenthesis as a string argument, and this will be displayed to users when said option is selected.

Resource Management

With the Scanner and FileWriter, these are resources where it can be beneficial to create multiple instances of. For example, if we only have one global instance of scanner and another developer accidentally calls a .close() method on it, this would prevent the rest of the code from functioning. Using separate instances helps avoid these potential conflicts. An added benefit is you can customize each instance independently by specifying different input sources.

Validation

When creating methods, there can sometimes be edge cases. In the case of user input for searching, adding, and deleting words- we do not want to accept input that is blank, not a string, or the \n (from pressing the Enter key). To do this, we can define a method with a boolean return type that checks that the user input is none of those aforementioned types. It returns true or false.

 public static boolean isValidWord(String word) {
        return word != null && !word.trim().isEmpty() && word.matches("^[a-zA-Z]*$") && !word.equals("\n");
    }
Enter fullscreen mode Exit fullscreen mode

Then, we can call that method in an if Statement. In Java, when we write if (isValidWord(searchTerm), you’re effectively saying "if isValidWord(searchTerm) returns true, then do something." In the code block below, if the word is valid, then we have another nested if/else that validates if the search term exists and if so- then prints out the list of words.
Otherwise, if the word is not valid, then the user is given an error message.

 if (isValidWord(searchTerm)) {
        if (filteredListOfWords.isEmpty()) {
            System.out.println(Sassy.sayCantFind());
        } else {
            System.out.println(Sassy.sayFoundIt());
            for (Word word : filteredListOfWords) {
                System.out.println(word);
            }
        }
    } else {
        System.out.println("Invalid word. Please enter a valid word.");
    }
Enter fullscreen mode Exit fullscreen mode

Top comments (0)