Java Arena
Step 5 of 8

Logic apart from the views

Step 4's ToursView kept the number of free places itself, in an instance variable of the view. That's fine while one view is all that needs it. It gets awkward when a second view needs the same data, or when you want to check the rules of the application without clicking through a window.

Module 49 moved the work of a window into a plain class that the handlers call, with no JavaFX in it. With several views, that class becomes more: it's where the application's data and rules, its logic, live, and every view gets the same object of it. The views only show what the logic says, and forward what the user does to it. Module 25 split text user interfaces the same way: the logic in one class, the reading and printing in another.

The logic first

A savings jar for a goal, such as a bike for 120 euros:

Main.java
public class Main {
    public static void main(String[] args) {
        SavingsJar jar = new SavingsJar("a bike", 120);
        System.out.println(jar.putIn(50));
        System.out.println(jar.putIn(-5));
        System.out.println(jar);
        System.out.println(jar.missing());
        jar.putIn(80);
        System.out.println(jar);
    }
}
SavingsJar.java
public class SavingsJar {
    private String goal;
    private int target;
    private int saved;

    public SavingsJar(String goal, int target) {
        this.goal = goal;
        this.target = target;
    }

    public boolean putIn(int amount) {
        if (amount < 1) {
            return false;
        }
        this.saved += amount;
        return true;
    }

    public int getSaved() {
        return this.saved;
    }

    public int missing() {
        return Math.max(0, this.target - this.saved);
    }

    @Override
    public String toString() {
        if (this.missing() == 0) {
            return "Goal reached: " + this.goal + "!";
        }
        return "Saved " + this.saved + " of " + this.target + " for " + this.goal;
    }
}
Output
true
false
Saved 50 of 120 for a bike
70
Goal reached: a bike!

This program has no window at all, and it can try every rule: a negative amount is refused, the missing amount never goes below 0, and the text changes when the goal is reached. The challenges of this step check logic classes the same way, by calling their methods.

Two views, one jar

Now the window. The views get the jar through their constructors, and both get the same jar object:

Main.java
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage stage) {
        SavingsJar jar = new SavingsJar("a bike", 120);
        DepositView deposit = new DepositView(jar);
        ProgressView progress = new ProgressView(jar);

        Button toDeposit = new Button("Deposit");
        Button toProgress = new Button("Progress");
        HBox menu = new HBox(10, toDeposit, toProgress);
        menu.setPadding(new Insets(10));

        BorderPane frame = new BorderPane();
        frame.setTop(menu);
        frame.setCenter(deposit.getView());
        toDeposit.setOnAction(event -> frame.setCenter(deposit.getView()));
        toProgress.setOnAction(event -> frame.setCenter(progress.getView()));

        stage.setScene(new Scene(frame));
        stage.setTitle("Savings jar");
        stage.show();
    }

    public static void main(String[] args) {
        launch(Main.class);
    }
}
DepositView.java
import javafx.geometry.Insets;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;

public class DepositView {
    private SavingsJar jar;
    private VBox view;

    public DepositView(SavingsJar jar) {
        this.jar = jar;
        TextField amount = new TextField();
        amount.setPromptText("Euros");
        Button putIn = new Button("Put in");
        Label message = new Label("");
        putIn.setOnAction(event -> {
            try {
                if (this.jar.putIn(Integer.valueOf(amount.getText()))) {
                    message.setText("Thanks! " + this.jar.getSaved() + " euros saved so far.");
                    amount.clear();
                } else {
                    message.setText("Put in at least 1 euro.");
                }
            } catch (NumberFormatException e) {
                message.setText("Type a whole number of euros.");
            }
        });
        this.view = new VBox(8, amount, putIn, message);
        this.view.setPadding(new Insets(10));
    }

    public Parent getView() {
        return this.view;
    }
}
ProgressView.java
import javafx.geometry.Insets;
import javafx.scene.Parent;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;

public class ProgressView {
    private SavingsJar jar;

    public ProgressView(SavingsJar jar) {
        this.jar = jar;
    }

    public Parent getView() {
        VBox view = new VBox(8, new Label(this.jar.toString()), new Label("Still missing: " + this.jar.missing() + " euros"));
        view.setPadding(new Insets(10));
        return view;
    }
}
SavingsJar.java
public class SavingsJar {
    private String goal;
    private int target;
    private int saved;

    public SavingsJar(String goal, int target) {
        this.goal = goal;
        this.target = target;
    }

    public boolean putIn(int amount) {
        if (amount < 1) {
            return false;
        }
        this.saved += amount;
        return true;
    }

    public int getSaved() {
        return this.saved;
    }

    public int missing() {
        return Math.max(0, this.target - this.saved);
    }

    @Override
    public String toString() {
        if (this.missing() == 0) {
            return "Goal reached: " + this.goal + "!";
        }
        return "Saved " + this.saved + " of " + this.target + " for " + this.goal;
    }
}
The window

After: Type "50" in text field 1, click "Put in", type "15" in text field 1, click "Put in", then click "Progress"

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

The window as text
Window "Savings jar"
  BorderPane
    top: HBox spacing 10, padding 10
      Button "Deposit"
      Button "Progress"
    center: VBox spacing 8, padding 10
      Label "Saved 65 of 120 for a bike"
      Label "Still missing: 55 euros"

Here's who does what:

The diagram as text
  • Main, a class
  • DepositView, a class
    • public constructor DepositView(jar: SavingsJar)
    • public method getView(): Parent
  • ProgressView, a class
    • public constructor ProgressView(jar: SavingsJar)
    • public method getView(): Parent
  • SavingsJar, a class
    • private field goal: String
    • private field target: int
    • private field saved: int
    • public constructor SavingsJar(goal: String, target: int)
    • public method putIn(amount: int): boolean
    • public method getSaved(): int
    • public method missing(): int
    • public method toString(): String

Connections:

  • Each Main has one DepositView object.
  • Each Main has one ProgressView object.
  • Each DepositView has one SavingsJar object.
  • Each ProgressView has one SavingsJar object.
  • SavingsJar holds the data and the rules: how much is saved, that an amount must be at least 1, when the goal is reached, and how the progress reads.
  • DepositView turns what the user typed into a number (a question of the user interface: a text field holds text), forwards it to putIn, and shows a message from the answer. It doesn't decide which amounts are allowed: the jar does.
  • ProgressView only shows what the jar says. It builds a new VBox at each call of getView(), so each visit reads the jar again and shows the latest amounts. That's step 4's first way to build a view, and here it's the right one: the data that lasts is in the jar, not in the view.
  • Main creates the jar and the views, gives each view the jar, and switches between them.

Since both views refer to the same SavingsJar object, a deposit made in one view is what the other one shows. Had each view made a jar of its own, Progress would always say 0.

The split pays off in other ways too. A rule changes in one place: if the jar should refuse amounts over 500, only putIn changes, and every view follows. A new view, such as a list of deposits, needs no change in the views that exist. And the rules can be checked without a window, as the program at the top of the step did.

Your task

Challenge 1 of 3

A travel agency books seats on its excursions. Write the logic, the class Excursion in Excursion.java (no JavaFX in it):

  • a constructor Excursion(String name, int seats): at first every seat is free
  • book(int count): when count is at least 1 and at most the number of free seats, books that many seats and returns true; otherwise books nothing and returns false
  • free(): returns the number of free seats
  • toString(): returns Island tour: 37 of 40 seats free (with the name, the free seats and all seats), or Island tour: sold out when no seat is free

Main.java is a test drive in the console. The check runs:

Excursion trip = new Excursion("Island tour", 40);
System.out.println(trip.book(3));
System.out.println(trip.book(0));
System.out.println(trip);

and expects:

true
false
Island tour: 37 of 40 seats free

Challenge 2

The agency's booking view. Excursion (the class from the first task) and Main are ready. Write the class BookingView in BookingView.java:

  • a constructor BookingView(Excursion excursion)
  • getView(), which returns a Parent: a VBox with the spacing 8 and no padding, holding a label with the excursion's toString(), a text field with the prompt text Seats, the button Book and an empty label for messages. Make them once, in the constructor.

Book reads the number of seats from the field, and:

  • when the text isn't a whole number, the message is Type the number of seats in digits.
  • when book returns true, the message is Seats booked: 3. (with the number), and the first label shows the excursion's new toString()
  • when book returns false, the message is Can't book that. Free seats: 37. (with the number of free seats)

Book never clears the field: it keeps the text that was typed, whatever the outcome.

The rules about which numbers can be booked are in Excursion: the view only forwards the number. The task card shows the window after typing 3 and clicking Book.

Challenge 3

The agency runs two excursions. Excursion and BookingView (from the tasks before) are ready. Write start in Main:

  • two Excursion objects: Island tour with 40 seats and Lake cruise with 25 seats, and a BookingView for each, made once
  • a BorderPane whose top is an HBox with the spacing 10 and the padding 10 on every side, holding the buttons Island tour and Lake cruise
  • each button puts the BookingView of its excursion in the center; at the start, the center shows the Island tour's
  • the window's title is Excursions

The task card shows the window after booking 2 seats for the Lake cruise.