Week 6

Separating the user interface from program logic

Let us explore the process of implementing a program and the importance of separating different areas of responsibility. Specifically, we will focus on a program that prompts the user to enter words until they repeat a word.

Sample output

Write a word: carrot Write a word: turnip Write a word: potato Write a word: celery Write a word: potato You wrote the same word twice!

To create this program, we'll need to build it step by step. However, one of the challenges we'll face is determining the best approach to take in solving the problem and deciding how to break it down into smaller subproblems. There is no definitive answer to this question, and it may depend on various factors, such as the problem domain and its concepts or the user interface.

One possible way to begin would be to focus on the user interface and develop a UserInterface class. This class would rely on a Scanner object to read user input. We can pass this object to the user interface so that it can process the input and provide appropriate feedback.

public class UserInterface {
    private Scanner scanner;

    public UserInterface(Scanner scanner) {
        this.scanner = scanner;
    }

    public void start() {
        // do something
    }
}

Creating and starting up a user interface can be done as follows.

public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    UserInterface userInterface = new UserInterface(scanner);
    userInterface.start();
}

Looping and quitting

This program has (at least) two "sub-problems". The first problem is continuously reading words from the user until a certain condition is reached. We can outline this as follows.

public class UserInterface {
    private Scanner scanner;

    public UserInterface(Scanner scanner) {
        this.scanner = scanner;
    }

    public void start() {

        while (true) {
            System.out.print("Enter a word: ");
            String word = scanner.nextLine();

            if (*stop condition*) {
                break;
            }

        }

        System.out.println("You gave the same word twice!");
    }
}

The current version of the program prompts the user for words until a repeat entry is detected. To achieve this, we need to modify the program to check if each entered word has already been submitted. However, we have yet to determine how this functionality will be implemented. Therefore, we will begin by creating an outline for the solution before proceeding with the actual implementation.

public class UserInterface {
    private Scanner scanner;

    public UserInterface(Scanner scanner) {
        this.scanner = scanner;
    }

    public void start() {

        while (true) {
            System.out.print("Enter a word: ");
            String word = scanner.nextLine();

            if (alreadyEntered(word)) {
                break;
            }

        }

        System.out.println("You gave the same word twice!");
    }

    public boolean alreadyEntered(String word) {
        // do something here

        return false;
    }
}

It is a good idea to test the program continuously, so let's make a test version of the method:

public boolean alreadyEntered(String word) {
    if (word.equals("end")) {
        return true;
    }

    return false;
}

Now the loop continues until the input equals the word "end":

Sample output

Enter a word: carrot Enter a word: celery Enter a word: turnip Enter a word: end You gave the same word twice!

The program doesn't completely work yet, but the first sub-problem - quitting the loop when a certain condition has been reached - has now been implemented.

Storing relevant information

Another sub-problem is remembering the already entered words. A list is a good structure for this purpose.

public class UserInterface {
    private Scanner scanner;
    private List<String> words;

    public UserInterface(Scanner scanner) {
        this.scanner = scanner;
        this.words = new ArrayList<String>();
    }

    //...
}

When a new word is entered, it has to be added to the list of words that have been entered before. This is achieved by adding a line that updates our list to the while-loop:

while (true) {
    System.out.print("Enter a word: ");
    String word = scanner.nextLine();

    if (alreadyEntered(word)) {
        break;
    }

    // adding the word to the list of previous words
    this.words.add(word);
}

The whole user interface looks as follows:

public class UserInterface {
    private Scanner scanner;
    private List<String> words;

    public UserInterface(Scanner scanner) {
        this.scanner = scanner;
        this.words = new ArrayList<String>();
    }

    public void start() {

        while (true) {
            System.out.print("Enter a word: ");
            String word = scanner.nextLine();

            if (alreadyEntered(word)) {
                break;
            }

            // adding the word to the list of previous words
            this.words.add(word);

        }

        System.out.println("You gave the same word twice!");
    }

    public boolean alreadyEntered(String word) {
       if (word.equals("end")) {
            return true;
        }

        return false;
    }
}

Before proceeding with the actual implementation of the new functionality, it's a good practice to perform testing to ensure the program still functions correctly. One way to test the program is to include a print statement at the end of the start-method, confirming that the entered words have indeed been added to the list.

// test print to check that everything still works
for (String word: this.words) {
    System.out.println(word);
}

Combining the solutions to sub-problems

Let's change the method alreadyEntered such that it checks whether the entered word is contained in our list of already entered words.

public boolean alreadyEntered(String word) {
    return this.words.contains(word);
}

Now the application works as intended.

Objects as a natural part of problem solving

We just built a solution to a problem where the program reads words from a user until the user enters a word that has already been entered before. Our example input was as follows:

Sample output

Enter a word: carrot Enter a word: celery Enter a word: turnip Enter a word: potato Enter a word: celery You gave the same word twice!

We came up with the following solution:

public class UserInterface {
    private Scanner scanner;
    private List<String> words;

    public UserInterface(Scanner scanner) {
        this.scanner = scanner;
        this.words = new ArrayList<String>();
    }

    public void start() {

        while (true) {
            System.out.print("Enter a word: ");
            String word = scanner.nextLine();

            if (alreadyEntered(word)) {
                break;
            }

            // adding the word to the list of previous words
            this.words.add(word);

        }

        System.out.println("You gave the same word twice!");
    }

    public boolean alreadyEntered(String word) {
       if (word.equals("end")) {
            return true;
        }

        return false;
    }
}

From the perspective of the user interface, the variable 'words' that we created earlier is merely a technical detail. What's more important is that the interface maintains a record of the set of words that the user has already entered. This set represents a distinct concept or abstraction, and it suggests that we can create an object to encapsulate this functionality. Whenever we encounter a clear abstraction like this, it's often useful to consider separating it into its own class.

Word set

Let's make a class called WordSet. After implementing the class, the user interface's start method looks like this:

while (true) {
    String word = scanner.nextLine();

    if (words.contains(word)) {
        break;
    }

    wordSet.add(word);
}

System.out.println("You gave the same word twice!");

From the perspective of the user interface, it's beneficial to encapsulate the functionality of maintaining and checking the set of words in a separate class called Wordset. This class should contain two methods: boolean contains(String word), which checks if the given word is already present in the set, and void add(String word), which adds the word to the set.

Organizing the functionality in this way can greatly improve the readability of the user interface, making it clearer and easier to understand. Below is an outline for the implementation of the 'WordSet' class.

public class WordSet {
    // object variable(s)

    public WordSet() {
        // constructor
    }

    public boolean contains(String word) {
        // implementation of the contains method
        return false;
    }

    public void add(String word) {
        // implementation of the add method
    }
}

Earlier solution as part of implementation

We can implement the set of words by changing our earlier solution, the list, into an object variable:

import java.util.ArrayList;
import java.util.List;

public class WordSet {
    private List<String> words

    public WordSet() {
        this.words = new ArrayList<>();
    }

    public void add(String word) {
        this.words.add(word);
    }

    public boolean contains(String word) {
        return this.words.contains(word);
    }
}

Our solution is now much more elegant, having separated the distinct concept of maintaining a set of words into its own class. This encapsulates all the necessary details inside an object, leaving the user interface looking clean and easy to understand.

Next, we can update the user interface to utilize the WordSet class. Similar to how we passed the Scanner object as a parameter, we can also give the user interface an instance of WordSet. This enables the interface to call the contains and add methods on the set, allowing it to keep track of the words that the user has entered.

public class UserInterface {
    private WordSet wordSet;
    private Scanner scanner;

    public userInterface(WordSet wordSet, Scanner scanner) {
        this.wordSet = wordSet;
        this.scanner = scanner;
    }

    public void start() {

        while (true) {
            System.out.print("Enter a word: ");
            String word = scanner.nextLine();

            if (this.wordSet.contains(word)) {
                break;
            }

            this.wordSet.add(word);
        }

        System.out.println("You gave the same word twice!");
    }
}

Starting the program is now done as follows:

public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    WordSet set = new WordSet();

    UserInterface userInterface = new UserInterface(set, scanner);
    userInterface.start();
}

Changing the implementation of a class

In our current implementation, the class 'WordSet' encapsulates a List. Is this a reasonable approach? It could be, as it allows us to make further changes to the class if necessary. For instance, if we want to save the word set into a file in the future, we can modify the class WordSet without changing the public interfaces used by the UserInterface class.

The key advantage of encapsulation is that changes made inside the WordSet class do not affect the UserInterface class. The UserInterface class accesses WordSet only through its public interfaces (methods), which shields it from any changes made to the implementation of WordSet.

Implementing new functionality: palindromes

In the future, we may want to add new functionalities to the program through the class WordSet. For example, if we wanted to determine how many of the entered words were palindromes, we could add a method called 'palindromes' to the class.

public void start() {

    while (true) {
        System.out.print("Enter a word: ");
        String word = scanner.nextLine();

        if (this.wordSet.contains(word)) {
            break;
        }

        this.wordSet.add(word);
    }

    System.out.println("You gave the same word twice!");
    System.out.println(this.wordSet.palindromes() + " of the words were palindromes.");
}

The user interface remains clean, because counting the palindromes is done inside the WordSet object. The following is an example implementation of the method:

import java.util.ArrayList;
import java.util.List;

public class WordSet {
    private List<String> words;

    public WordSet() {
        this.words = new ArrayList<>();
    }

    public boolean contains(String word) {
        return this.words.contains(word);
    }

    public void add(String word) {
        this.words.add(word);
    }

    public int palindromes() {
        int count = 0;

        for (String word: this.words) {
            if (isPalindrome(word)) {
                count++;
            }
        }

        return count;
    }

    public boolean isPalindrome(String word) {
        int end = word.length() - 1;

        int i = 0;
        while (i < word.length() / 2) {
            // method charAt returns the character at given index
            // as a simple variable
            if(word.charAt(i) != word.charAt(end - i)) {
                return false;
            }

            i++;
        }

        return true;
    }
}

The method 'palindromes' uses the helper method isPalindrome to check whether the word that's given to it as a parameter is, in fact, a palindrome.

Programming tips

In the larger example above, we were following the advice given here.

  • Proceed with small steps

    • Try to separate the program into several sub-problems and work on only one sub-problem at a time
    • Always test that the program code is advancing in the right direction, in other words: test whether the solution to the sub-problem is correct
    • Recognize the conditions that require the program to work differently. In the example above, we needed a different functionality to test whether a word had been already entered before.
  • Write as "clean" code as possible

    • Indent your code
    • Use descriptive method and variable names
    • Don't make your methods too long, not even the main method
    • Do only one thing inside one method
    • Remove all copy-paste code
    • Replace the "bad" and unclean parts of your code with clean code
  • If needed, it's also a good idea to step back and assess the program as a whole. If it's not working, we might need to go back to a previous state where the code was still functional. As a rule, adding more code to a broken program rarely fixes it.

By following these practices, we can make our code more reusable and maintainable. We can also ensure that our programs work as intended and are easier to modify and test.

Loading

From one entity to many parts

Consider a program that prompts the user to enter exam points and converts them into grades. The program also displays the grade distribution using stars. The program terminates when the user inputs an empty string. An example implementation of the program is shown below:

Sample output

Points: 91 Points: 98 Points: 103 Impossible number. Points: 90 Points: 89 Points: 89 Points: 88 Points: 72 Points: 54 Points: 55 Points: 51 Points: 49 Points: 48 Points:

5: *** 4: *** 3: * 2: 1: *** 0: **

As almost all programs, this program can be written into main as one entity. Here is one possibility:

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class Program {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        List<Integer> grades = new ArrayList<>();

        while (true) {
            System.out.print("Points: ");
            String input = scanner.nextLine();
            if (input.equals("")) {
                break;
            }

            int score = Integer.valueOf(input);

            if (score < 0 || score > 100) {
                System.out.println("Impossible number.");
                continue;
            }

            int grade = 0;
            if (score < 50) {
                grade = 0;
            } else if (score < 60) {
                grade = 1;
            } else if (score < 70) {
                grade = 2;
            } else if (score < 80) {
                grade = 3;
            } else if (score < 90) {
                grade = 4;
            } else {
                grade = 5;
            }

            grades.add(grade);
        }

        System.out.println("");
        int grade = 5;
        while (grade >= 0) {
            int stars = 0;
            for (int received: grades) {
                if (received == grade) {
                    stars++;
                }
            }

            System.out.print(grade + ": ");
            while (stars > 0) {
                System.out.print("*");
                stars--;
            }
            System.out.println("");

            grade = grade - 1;
        }
    }
}

To break down the program into more manageable parts, we can identify distinct areas of responsibility within the program. For instance, managing grades, which involves converting scores into grades, could be isolated in a separate class. Similarly, we could create another class specifically for the user interface.

Program logic

The program logic includes parts that are crucial for the execution of the program, like functionalities that store information. Using the previous example, we can separate the parts that store grade information and create a class called GradeRegister. This class will be responsible for keeping track of the number of different grades students have received. We can add grades to the register according to scores and use the register to ask how many people have received a certain grade.

An example class follows:

import java.util.ArrayList;
import java.util.List;


public class GradeRegister {

    private List<Integer> grades;

    public GradeRegister() {
        this.grades = new ArrayList<>();
    }

    public void addGradeBasedOnPoints(int points) {
        this.grades.add(pointsToGrades(points));
    }

    public int numberOfGrades(int grade) {
        int count = 0;
        for (int received: this.grades) {
            if (received == grade) {
                count++;
            }
        }

        return count;
    }

    public static int pointsToGrades(int points) {

        int grade = 0;
        if (points < 50) {
            grade = 0;
        } else if (points < 60) {
            grade = 1;
        } else if (points < 70) {
            grade = 2;
        } else if (points < 80) {
            grade = 3;
        } else if (points < 90) {
            grade = 4;
        } else {
            grade = 5;
        }

        return grade;
    }
}

When the grade register has been separated into a class, we can remove the functionality associated with it from our main program. The main program now looks like this.

import java.util.Scanner;

public class Program {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        GradeRegister register = new GradeRegister();

        while (true) {
            System.out.print("Points: ");
            String input = scanner.nextLine();
            if (input.equals("")) {
                break;
            }

            int score = Integer.valueOf(input);

            if (score < 0 || score > 100) {
                System.out.println("Impossible number.");
                continue;
            }

            register.addGradeBasedOnPoints(score);
        }

        System.out.println("");
        int grade = 5;
        while (grade >= 0) {
            int stars = register.numberOfGrades(grade);
            System.out.print(grade + ": ");
            while (stars > 0) {
                System.out.print("*");
                stars--;
            }
            System.out.println("");

            grade = grade - 1;
        }
    }
}

Separating the program's logic into different classes offers significant benefits for program maintenance. By separating the GradeRegister into its own class, it can be tested independently of the rest of the program. Additionally, the GradeRegister can be copied and used in other programs.

GradeRegister register = new GradeRegister();
register.addGradeBasedOnPoints(51);
register.addGradeBasedOnPoints(50);
register.addGradeBasedOnPoints(49);

System.out.println("Number of students with grade 0 (should be 1): " + register.numberOfGrades(0));
System.out.println("Number of students with grade 0 (should be 2): " + register.numberOfGrades(2));

User interface

In general, each program has its own user interface. Therefore, we will create a separate class called UserInterface and decouple it from the main program. The constructor of the UserInterface class will take two parameters: a grade register for storing the grades and a Scanner object that reads user input.

With a separate user interface in place, the main program that initializes the entire application becomes more straightforward.

import java.util.Scanner;

public class Program {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        GradeRegister register = new GradeRegister();

        UserInterface userInterface = new UserInterface(register, scanner);
        userInterface.start();
    }
}

Let's have a look at how the user interface is implemented. There are two essential parts to the UI: reading the points, and printing the grade distribution.

import java.util.Scanner;

public class UserInterface {

    private GradeRegister register;
    private Scanner scanner;

    public UserInterface(GradeRegister register, Scanner scanner) {
        this.register = register;
        this.scanner = scanner;
    }

    public void start() {
        readPoints();
        System.out.println("");
        printGradeDistribution();
    }

    public void readPoints() {
    }

    public void printGradeDistribution() {
    }
}

It's possible to reuse the code for reading exam points and printing grade distribution from the previous main program with minimal modifications. In the following program, some parts of the code have indeed been copied from the earlier version, while a new method for printing stars has been added to improve the clarity of the code that prints the grade distribution.

import java.util.Scanner;

public class UserInterface {

    private GradeRegister register;
    private Scanner scanner;

    public UserInterface(GradeRegister register, Scanner scanner) {
        this.register = register;
        this.scanner = scanner;
    }

    public void start() {
        readPoints();
        System.out.println("");
        printGradeDistribution();
    }

    public void readPoints() {
        while (true) {
            System.out.print("Points: ");
            String input = scanner.nextLine();
            if (input.equals("")) {
                break;
            }

            int points = Integer.valueOf(input);

            if (points < 0 || points > 100) {
                System.out.println("Impossible number.");
                continue;
            }

            this.register.addGradeBasedOnPoints(points);
        }
    }

    public void printGradeDistribution() {
        int grade = 5;
        while (grade >= 0) {
            int stars = register.numberOfGrades(grade);
            System.out.print(grade + ": ");
            printStars(stars);
            System.out.println("");

            grade = grade - 1;
        }
    }

    public static void printStars(int stars) {
        while (stars > 0) {
            System.out.print("*");
            stars--;
        }
    }
}
Loading
You have reached the end of this section! Continue to the next section: