Java Arena
Step 4 of 5

Logic in a class of its own

The handlers so far did small things. In a bigger program, a handler that works everything out by itself gets long, and the only way to try what it computes is to click through the window. A better shape, common in programs of every size: a plain class does the work and knows nothing about windows, and the handlers only read the window, call the class, and show what it answers.

A color mixer

A paint shop's window mixes the three primary colors. The mixing is in ColorMixer, a class without a single import from JavaFX:

Main.java
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage stage) {
        ColorMixer mixer = new ColorMixer();
        Label mix = new Label("Mix: nothing yet");
        Button red = new Button("Red");
        Button yellow = new Button("Yellow");
        Button blue = new Button("Blue");
        Button clear = new Button("Clear");
        red.setOnAction(event -> {
            mixer.add("red");
            mix.setText("Mix: " + mixer.result());
        });
        yellow.setOnAction(event -> {
            mixer.add("yellow");
            mix.setText("Mix: " + mixer.result());
        });
        blue.setOnAction(event -> {
            mixer.add("blue");
            mix.setText("Mix: " + mixer.result());
        });
        clear.setOnAction(event -> {
            mixer.clear();
            mix.setText("Mix: " + mixer.result());
        });

        stage.setScene(new Scene(new VBox(10, new HBox(5, red, yellow, blue), clear, mix)));
        stage.setTitle("Color mixer");
        stage.show();
    }

    public static void main(String[] args) {
        launch(Main.class);
    }
}
ColorMixer.java
public class ColorMixer {
    private boolean red;
    private boolean yellow;
    private boolean blue;

    public void add(String color) {
        if (color.equals("red")) {
            this.red = true;
        } else if (color.equals("yellow")) {
            this.yellow = true;
        } else if (color.equals("blue")) {
            this.blue = true;
        }
    }

    public void clear() {
        this.red = false;
        this.yellow = false;
        this.blue = false;
    }

    public String result() {
        if (this.red && this.yellow && this.blue) {
            return "brown";
        }
        if (this.red && this.yellow) {
            return "orange";
        }
        if (this.yellow && this.blue) {
            return "green";
        }
        if (this.red && this.blue) {
            return "purple";
        }
        if (this.red) {
            return "red";
        }
        if (this.yellow) {
            return "yellow";
        }
        if (this.blue) {
            return "blue";
        }
        return "nothing yet";
    }
}
The window

After: Click "Red", then click "Yellow"

This window is wider than the screen: scroll it sideways, or read it as text below.

The window as text
Window "Color mixer"
  VBox spacing 10
    HBox spacing 5
      Button "Red"
      Button "Yellow"
      Button "Blue"
    Button "Clear"
    Label "Mix: orange"
  • start makes one ColorMixer, and every handler uses it. mixer never changes, so the lambdas may use it (step 2); the mixer object's own variables change.
  • Each handler does the same two things: it tells the mixer what happened, and it shows the mixer's answer in the label. None of them knows how colors mix.
  • ColorMixer knows nothing about buttons or labels. It takes a color's name and answers with a name.

Logic you can check without a window

Since ColorMixer doesn't need a window, a program can use it without one:

Main.java
public class Main {
    public static void main(String[] args) {
        ColorMixer mixer = new ColorMixer();
        System.out.println(mixer.result());
        mixer.add("blue");
        mixer.add("yellow");
        System.out.println(mixer.result());
        mixer.add("red");
        System.out.println(mixer.result());
    }
}
ColorMixer.java
public class ColorMixer {
    private boolean red;
    private boolean yellow;
    private boolean blue;

    public void add(String color) {
        if (color.equals("red")) {
            this.red = true;
        } else if (color.equals("yellow")) {
            this.yellow = true;
        } else if (color.equals("blue")) {
            this.blue = true;
        }
    }

    public void clear() {
        this.red = false;
        this.yellow = false;
        this.blue = false;
    }

    public String result() {
        if (this.red && this.yellow && this.blue) {
            return "brown";
        }
        if (this.red && this.yellow) {
            return "orange";
        }
        if (this.yellow && this.blue) {
            return "green";
        }
        if (this.red && this.blue) {
            return "purple";
        }
        if (this.red) {
            return "red";
        }
        if (this.yellow) {
            return "yellow";
        }
        if (this.blue) {
            return "blue";
        }
        return "nothing yet";
    }
}
Output
nothing yet
green
brown

That's what makes the shape worth it. Every case of the mixing can be tried by calling methods, as the checks of the next challenge do, or with JUnit tests (module 26), with no clicking. The window then needs only a few clicks to show that its handlers call the right methods. And the same ColorMixer could serve a text user interface (module 25) or a different window, unchanged.

One handler for several buttons

The three color buttons do the same thing with a different color. A handler is an object, so one handler can be kept in a variable and given to all three. The event tells which button was clicked: event.getSource() returns it. Its type is Object, so the handler casts it to a Button (module 36):

EventHandler<ActionEvent> addColor = event -> {
    Button clicked = (Button) event.getSource();
    mixer.add(clicked.getText().toLowerCase());
    mix.setText("Mix: " + mixer.result());
};
red.setOnAction(addColor);
yellow.setOnAction(addColor);
blue.setOnAction(addColor);

Naming the handler's type, EventHandler<ActionEvent>, needs two more imports: javafx.event.ActionEvent and javafx.event.EventHandler. The buttons' texts Red, Yellow and Blue turn into the mixer's names with toLowerCase().

Your task

Challenge 1 of 3

A sign-up page checks whether a username is free, against the names already taken. Write the class UsernameChecker, without anything from JavaFX, with:

  • a constructor that takes no parameters: at first, no name is taken
  • addTaken(String name), which marks a name as taken
  • isFree(String name), which returns true when the name has at least 3 characters and isn't taken
  • message(String name), which returns At least 3 characters for a name of fewer than 3 characters, the name and is taken for a taken one (ada is taken), and the name and is free otherwise (ada_l is free)

Capital letters don't matter when comparing names: after addTaken("Ada"), ADA and ada are taken too. The message shows the name as it was given. The check runs:

UsernameChecker checker = new UsernameChecker();
checker.addTaken("admin");
checker.addTaken("Ada");
System.out.println(checker.message("ad"));
System.out.println(checker.message("ADA"));
System.out.println(checker.message("ada_l"));
System.out.println(checker.isFree("Admin") + " " + checker.isFree("linus"));

and expects:

At least 3 characters
ADA is taken
ada_l is free
false true

Challenge 2

Now the sign-up window, with UsernameChecker from the last challenge ready in its own file. The window is built already: a text field with the prompt text Username, the label At least 3 characters, and the disabled button Sign up. The names admin, ada and linus are taken. Make it work, with the checker doing the checking:

  • After every change of the text, the label shows the checker's message for the text, and the button is enabled only when the checker says the name is free.
  • Sign up marks the name in the field as taken, sets the label to Signed up: and the name (Signed up: grace), and disables the button.

The task card shows the window after typing grace and clicking Sign up.

Challenge 3

A pomodoro timer goes through a fixed plan of steps: Work 1, Short break, Work 2, Short break, Work 3, Short break, Work 4, Long break, and then from Work 1 again. Write both parts:

  • In PomodoroCycle.java, the class PomodoroCycle, without anything from JavaFX: a constructor that starts at Work 1, current(), which returns the name of the current step, next(), which moves to the next step, and restart(), which goes back to Work 1.
  • In Main.java, the window is built already: the label Now: Work 1, and under it the buttons Next and Start over. Make a PomodoroCycle in start. Next moves the cycle to its next step and Start over restarts it; after either one, the label shows Now: and the current step, like Now: Short break.

The task card shows the window after seven clicks on Next.