3.04 Beispielanwendung Notenumwandler

Motivation

Trennung von Logik und GUI

Wir wollen uns hier als Beispiel eine Anwendung ansehen, hinter der bereits eine gewisse Programmlogik steckt. Außerdem soll hier deutlich werden, dass wir diese Programmlogik und die GUI bei der Implementierung möglichst trennen sollten. Dafür gibt es verschiedene Argumente.

Stelle Dir zum Beispiel vor, Du möchtest in einem kleinen Team eine Anwendung erstellen. Dann könnte sich ein Teil des Teams mit der Logik der Anwendung beschäftigen. während ein anderer Teil sich einzig um die GUI kümmert. Das setzt natürlich voraus, dass Ihr Euer Vorgehen vorher gut geplant habt – zum Beispiel mit einem Entwurfsdiagramm!
Außerdem läuft man Gefahr, die gesamte Anwendung in einer einzigen riesigen Klasse zu schreiben und viele Aspekte der Objektorientierung zu vernachlässigen, wenn man GUI und Programmlogik nicht trennt.

Ziel unserer Anwendung

Ein Fenster mit TextField, Button und Label.
So soll unsere Anwendung aussehen.

Unsere kleine Beispielanwendung soll in der Lage sein, eine Schulnote die in Punkten von 0 bis 15 gegeben ist in Text umzuwandeln. Über ein Text-Field werden die Punkte eingegeben. Klickt man auf einen Button, wird die Note in Worten in einem Label angezeigt.

Dabei wird folgende Tabelle zugrunde gelegt:

Punkte Note in Worten
15 sehr gut plus
14 sehr gut
13 sehr gut minus
12 gut plus
11 gut
10 gut minus
9 befriedigend plus
8 befriedigend
7 befriedigend minus
6 ausreichend plus
5 ausreichend
4 ausreichend minus
3 mangelhaft plus
2 mangelhaft
1 mangelhaft minus
0 ungenügend

Implementierung

Schritt 1: Die Programmlogik

Wir werden zunächst eine Klasse Notenrechner erstellen, die vollkommen unabhängig von einer GUI funktioniert. Diese Klasse soll dem folgenden Diagramm genügen:

Klassendiagramm von Notenrechner.Die beiden Attribute sind unterstrichen, wodurch kenntlich gemacht wird, dass es Klassenattribute sind. Der Zusatz {readOnly} gibt an, dass sie final sind. Es handelt sich also um zwei Klassenkonstanten. Diese sind zwar nicht zwingend nötig, aber gleich werden wir sehen, welchen Zweck sie haben.

Die Methode getNote wandelt eine Punktzahl in die Noten sehr gut, gut, befriedigend, ausreichend, mangelhaft und ungenügend um. Mit getTendenz erhalten wir ggf. die Tendenz plus oder minus. Die Methode getUmwandlung liefert die komplette Umwandlung gemäß der Tabelle von oben.

Eine mögliche Implementierung sieht wie folgt aus:

public class Notenrechner {
  
  // Klassenkonstanten
  public static final int MAX_PUNKTE = 15;
  public static final int MIN_PUNKTE = 0;

  // Methoden
  public String getNote(int pPunkte){
    switch((pPunkte+2)/3){
    case 5: return "sehr gut"; 		
    case 4: return "gut"; 			
    case 3: return "befriedigend"; 	
    case 2: return "ausreichend";
    case 1: return "mangelhaft";
    case 0: return "ungenügend";
    default: return "FEHLER!";		
    }
  }

  public String getTendenz(int pPunkte){
    if (pPunkte == 0){
      return "";
    }else{
      switch(pPunkte%3){
      case 0: return " plus";
      case 1: return " minus";
      default: return "";
      }
    }
  }
  
  public String getUmwanldung(int pPunkte){
    return getNote(pPunkte)+getTendenz(pPunkte);
  }
}

Sehen wir uns ein paar Punkte dazu an:

  • Die beiden Klassenkonstanten werden mit als static final deklariert und mit den entsprechenden Werten initialisiert.
  • Bei getNote bietet sich eine switch-case-Anweisung an. Natürlich hätten wir auch einfach alle 16 Fälle jeweils als einzelne Cases angeben können. Hier wurde die Methode aber ein wenig kompakter implementiert.
    In der switch-Anweisung wird zunächst auf die gegebene Punktzahl 2 addiert. Danach wird der Wert ganzzahlig durch 3 geteilt. Ist zum Beispiel pPunkte ein Wert von 13 bis 15, dann ist pPunkte+2 ein Wert von 15 bis 17. Teilt man einen dieser Werte ganzzahlig durch 3, erhält man 5.
    Dieses Vorgehen bietet sich an, da in der Regel drei verschiedene Punktzahlen zu derselben Note führen.
  • In der Methode getTendenz wird (neben einer if-Abfrage) ebenfalls eine switch-Anweisung verwendet. Hier muss allerdings der Rest von pPunkte bei Divison durch 3 betrachtet werden. Dies bietet sich an, da sich die Tendenz immer in einem Zyklus von 3 Punkten wiederholt.
  • Die Methode getUmwandlung greift auf die beiden zuvor erstellen Methoden zurück.

Die Umwandlung der Note wird auf mehrere Methoden verteilt, um auch hier die Möglichkeit aufzuzeigen, dass man selbst das Erstellen einer solchen Klassen in einem Team auf mehrere Schultern verteilen könnte.

Schritt 2: Testen ohne GUI

Wir können nun zunächst ohne GUI testen, ob unsere Klasse funktioniert:

public class Notentest {

  public static void main(String[] args) {
    
    Notenrechner meinRechner = new Notenrechner();
    
    for (int punkte=Notenrechner.MAX_PUNKTE; punkte>=Notenrechner.MIN_PUNKTE; punkte=punkte-1){
      System.out.println(meinRechner.getUmwanldung(punkte));
    }

  }

}

In einer Schleife durchlaufen wir alle erlaubten Punktzahlen. An dieser Stelle greifen wir auf die Klassenkonstanten zurück. Die Idee dahinter ist, unsere Anwendung flexibel für mögliche spätere Anpassungen zu machen.
Vielleicht gibt es von Bundesland zu Bundesland verschiedene Punktzahlen. Dann müssen wir nur unsere Klasse Notenrechner anpassen, können diesen Test aber so stehen lassen wie er ist!

Wir erhalten bei diesem Test übrigens die folgende Ausgabe:

sehr gut plus
sehr gut
sehr gut minus
gut plus
gut
gut minus
befriedigend plus
befriedigend
befriedigend minus
ausreichend plus
ausreichend
ausreichend minus
mangelhaft plus
mangelhaft
mangelhaft minus
ungenügend

Damit scheint unsere Programmlogik also in Ordnung zu sein.

Schritt 3: Erstellen der GUI

Sehen wir uns nun an, wie unsere Klasse Notenrechner in eine JavaFX-Anwendung integriert werden könnte:

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;


public class NotenumwandlerFX extends Application{

  // Attribut
  Notenrechner meinRechner;

  // Konstruktor
  public NotenumwandlerFX() {
    meinRechner = new Notenrechner();
  }

  public static void main(String[] args) {
    launch(args);
  }

  public void start(Stage meineStage) throws Exception {

    // Fensterkomponenten und Layout

    meineStage.setTitle("Notenumwandler");

    Button meinButton = new Button();
    meinButton.setText("Umrechnen!");

    final TextField meinTextField = new TextField();
    meinTextField.setMaxWidth(50);
    meinTextField.setAlignment(Pos.CENTER_RIGHT);
    meinTextField.setText("15");		

    final Label meinLabel = new Label();
    meinLabel.setText("sehr gut plus");	
    meinLabel.setMinWidth(150);
    meinLabel.setMaxWidth(150);

    BorderPane root = new BorderPane();
    root.setLeft(meinTextField);
    BorderPane.setAlignment(meinTextField, Pos.CENTER);

    root.setCenter(meinButton);
    BorderPane.setAlignment(meinButton, Pos.CENTER);

    root.setRight(meinLabel);
    BorderPane.setAlignment(meinLabel, Pos.CENTER);

    Scene meineScene = new Scene(root, 350, 100);
    meineStage.setScene(meineScene);
    meineStage.show();	


    // Ereignisse

    meinButton.addEventHandler(ActionEvent.ACTION, 			
        new EventHandler<ActionEvent>() {
      public void handle(ActionEvent meinEvent) {
        meinLabel.setText(meinRechner.getUmwanldung(Integer.parseInt( meinTextField.getText() )));					
      }
    });

  }

}

Der Aufbau des Layouts und das Erstellen der Fensterkomponenten enthält fast nichts neues für uns. Lediglich die Breiten der Labels wurden mit setMinWidth und setMaxWidth etwas eingeschränkt, damit sie nicht während des Ablaufs der Anwendung ihre Breite ändern. Das kann vorkommen, da die verschiedenen Noten in Worten unterschiedlich lang sind.

Eine Instanz meinRechner der Klasse Notenrechner wurde hier als Attribut unserer Klasse NotenumwandlerFX hinzugefügt. Im Konstruktor der Klasse wird die Instanz erstellt. Dadurch kann dann in der Ereignisbehandlung auf meinRechner zugegriffen werden.
Konkret wird bei Klicken des Buttons die Methode getUmwandlung aufgerufen. Sie erhält als Parameter die Zahl, die im Text-Field steht. Da die Methode getText() der Klasse TextField aber einen String liefert, muss dieser durch Interger.parsteInt noch in einen int-Wert umgewandelt werden.
Das Ergebnis von getUmwandlung wird dann im Label angezeigt.

Wir hätten unseren Notenrechner übrigens auch in der start-Methode erstellen können:

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;


public class NotenumwandlerFX extends Application{

  public static void main(String[] args) {
    launch(args);
  }

  public void start(Stage meineStage) throws Exception {
    
    // Unser Notenrechner kann auch hier erzeugt werden.
    final Notenrechner meinRechner = new Notenrechner();
    
    // Fensterkomponenten und Layout

    meineStage.setTitle("Notenumwandler");

    Button meinButton = new Button();
    meinButton.setText("Umrechnen!");

    final TextField meinTextField = new TextField();
    meinTextField.setMaxWidth(50);
    meinTextField.setAlignment(Pos.CENTER_RIGHT);
    meinTextField.setText("15");		

    final Label meinLabel = new Label();
    meinLabel.setText("sehr gut plus");	
    meinLabel.setMinWidth(150);
    meinLabel.setMaxWidth(150);

    BorderPane root = new BorderPane();
    root.setLeft(meinTextField);
    BorderPane.setAlignment(meinTextField, Pos.CENTER);

    root.setCenter(meinButton);
    BorderPane.setAlignment(meinButton, Pos.CENTER);

    root.setRight(meinLabel);
    BorderPane.setAlignment(meinLabel, Pos.CENTER);

    Scene meineScene = new Scene(root, 350, 100);
    meineStage.setScene(meineScene);
    meineStage.show();	
    

    // Ereignisse

    meinButton.addEventHandler(ActionEvent.ACTION, 			
        new EventHandler<ActionEvent>() {
      public void handle(ActionEvent meinEvent) {
        meinLabel.setText(meinRechner.getUmwanldung(Integer.parseInt( meinTextField.getText() )));					
      }
    });

  }

}

Damit wird die Verwendung des Konstruktors überflüssig. In der ersten Variante wollte ich aber noch einmal die Trennung von Programmlogik und GUI unterstreichen und habe daher die Instanz im Konstruktor erstellt.