Part 6

Trennung der Benutzungsschnittstelle von der Programmlogik

Betrachten wir den Prozess der Implementierung eines Programms und wie die verschiedenen Verantwortungsbereiche voneinander getrennt werden können. Das Programm fordert die Benutzerin oder den Benutzer auf, Wörter einzugeben, bis ein Wort zweimal eingegeben wird.

Beispielausgabe

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!

Bauen wir dieses Programm Schritt für Schritt auf. Eine der Herausforderungen ist, dass es schwierig ist, zu entscheiden, wie das Problem angegangen werden soll, wie das Problem in kleinere Teilprobleme aufgeteilt werden kann und mit welchem Teilproblem man beginnen sollte. Es gibt keine eindeutige Antwort – manchmal ist es sinnvoll, vom Problemfeld und seinen Konzepten und deren Verbindungen auszugehen, manchmal ist es besser, mit der Benutzungsschnittstelle zu beginnen.

Wir könnten beginnen, die (textbasierte) Benutzungsschnittstelle zu implementieren, indem wir eine Klasse UserInterface erstellen. Die Benutzungsschnittstelle verwendet ein Scanner-Objekt, um Benutzereingaben zu lesen. Dieses Objekt wird der Benutzungsschnittstelle übergeben.

public class UserInterface {
    private Scanner scanner;

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

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

Das Erstellen und Starten einer Benutzungsschnittstelle kann wie folgt erfolgen.

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

Iterieren und Beenden

Dieses Programm hat (mindestens) zwei "Teilprobleme". Das erste Problem besteht darin, kontinuierlich Wörter von der Benutzerin oder dem Benutzer zu lesen, bis eine bestimmte Bedingung erreicht ist. Wir können dies wie folgt skizzieren.

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!");
    }
}

Das Programm fragt weiterhin nach Wörtern, bis die Benutzerin oder der Benutzer ein Wort eingibt, das bereits zuvor eingegeben wurde. Lassen Sie uns das Programm so modifizieren, dass überprüft wird, ob das Wort bereits eingegeben wurde. Wir wissen noch nicht, wie wir diese Funktionalität implementieren sollen, daher bauen wir zunächst eine Skizze dafür.

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;
    }
}

Es ist eine gute Idee, das Programm kontinuierlich zu testen, also lassen Sie uns eine Testversion der Methode erstellen:

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

    return false;
}

Jetzt läuft die Schleife weiter, bis die Eingabe das Wort "end" ist:

Beispielausgabe

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

Das Programm funktioniert noch nicht vollständig, aber das erste Teilproblem – das Beenden der Schleife, wenn eine bestimmte Bedingung erreicht wurde – ist nun implementiert.

Speichern relevanter Informationen

Ein weiteres Teilproblem besteht darin, sich die bereits eingegebenen Wörter zu merken. Eine Liste ist eine gute Struktur für diesen Zweck.

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

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

    //...
}

Wenn ein neues Wort eingegeben wird, muss es der Liste der zuvor eingegebenen Wörter hinzugefügt werden. Dies geschieht, indem wir eine Zeile hinzufügen, die unsere Liste in der while-Schleife aktualisiert:

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);
}

Die gesamte Benutzungsschnittstelle sieht wie folgt aus.

public class UserInterface {
    private Scanner scanner;
    private ArrayList<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;
    }
}

Wiederum ist es eine gute Idee, zu testen, ob das Programm noch funktioniert. Es könnte beispielsweise nützlich sein, am Ende der start-Methode einen Testausdruck hinzuzufügen, um sicherzustellen, dass die eingegebenen Wörter tatsächlich der Liste hinzugefügt wurden.

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

Kombination der Lösungen zu Teilproblemen

Ändern wir die Methode 'alreadyEntered' so, dass sie überprüft, ob das eingegebene Wort in unserer Liste der bereits eingegebenen Wörter enthalten ist.

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

Nun funktioniert die Anwendung wie vorgesehen.

Objekte als natürlicher Teil der Problemlösung

Wir haben gerade eine Lösung für ein Problem erstellt, bei dem das Programm Wörter von der Benutzerin oder dem Benutzer liest, bis ein Wort eingegeben wird, das zuvor bereits eingegeben wurde. Unser Beispiel für die Eingabe sah folgendermaßen aus:

Beispielausgabe

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!

Wir haben folgende Lösung entwickelt:

public class UserInterface {
    private Scanner scanner;
    private ArrayList<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;
    }
}

Aus Sicht der Benutzungsschnittstelle ist die Hilfsvariable 'words' nur ein Detail. Das Hauptmerkmal ist, dass die Benutzungsschnittstelle sich die Menge der eingegebenen Wörter merkt. Die Menge ist ein klar definiertes "Konzept" oder eine Abstraktion. Solche klar definierten Konzepte sind alle potenzielle Objekte: Wenn wir bemerken, dass wir eine solche Abstraktion in unserem Code haben, können wir darüber nachdenken, das Konzept in eine eigene Klasse auszulagern.

WordSet

Lassen Sie uns eine Klasse namens WordSet erstellen. Nachdem die Klasse implementiert wurde, sieht die start-Methode der Benutzungsschnittstelle wie folgt aus:

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

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

    wordSet.add(word);
}

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

Aus der Sicht der Benutzungsschnittstelle sollte die Klasse WordSet die Methode 'boolean contains(String word)' enthalten, die überprüft, ob das gegebene Wort in unserer Menge von Wörtern enthalten ist, und die Methode void add(String word), die das gegebene Wort zur Menge hinzufügt.

Wir stellen fest, dass die Lesbarkeit der Benutzungsschnittstelle stark verbessert wird, wenn sie auf diese Weise geschrieben ist.

Der Entwurf für die Klasse WordSet sieht folgendermaßen aus:

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
    }
}

Frühere Lösung als Teil der Implementierung

Wir können die Menge der Wörter implementieren, indem wir unsere frühere Lösung, die Liste, in eine Objektvariable umwandeln:

import java.util.ArrayList;

public class WordSet {
    private ArrayList<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);
    }
}

Nun ist unsere Lösung recht elegant. Wir haben ein klares Konzept in eine eigene Klasse ausgelagert und unsere Benutzungsschnittstelle sieht sauber aus. Alle "unsauberen Details" wurden ordentlich in einem Objekt gekapselt.

Bearbeiten wir nun die Benutzungsschnittstelle so, dass sie die Klasse WordSet verwendet. Die Klasse wird der Benutzungsschnittstelle als Parameter übergeben, genau wie Scanner.

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!");
    }
}

Das Starten des Programms erfolgt nun wie folgt:

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

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

Änderung der Implementierung einer Klasse

Wir haben eine Situation erreicht, in der die Klasse 'WordSet' eine ArrayList "kapselt". Ist dies sinnvoll? Vielleicht. Dies liegt daran, dass wir andere Änderungen an der Klasse vornehmen können, wenn wir dies wünschen, und bald könnten wir in eine Situation geraten, in der die Menge der Wörter beispielsweise in einer Datei gespeichert werden muss. Wenn wir all diese Änderungen innerhalb der Klasse WordSet vornehmen, ohne die Namen der Methoden zu ändern, die die Benutzungsschnittstelle verwendet, müssen wir die eigentliche Benutzungsschnittstelle überhaupt nicht ändern.

Der Hauptpunkt hier ist, dass Änderungen innerhalb der Klasse WordSet keine Auswirkungen auf die Klasse UserInterface haben. Dies liegt daran, dass die Benutzungsschnittstelle WordSet über die Methoden verwendet, die sie bereitstellt – diese werden als ihre öffentliche Schnittstelle bezeichnet.

Implementierung neuer Funktionalitäten: Palindrome

In Zukunft möchten wir das Programm möglicherweise so erweitern, dass die Klasse WordSet einige neue Funktionalitäten bietet. Wenn wir beispielsweise wissen möchten, wie viele der eingegebenen Wörter Palindrome sind, könnten wir eine Methode namens 'palindromes' in das Programm einfügen.

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.");
}

Die Benutzungsschnittstelle bleibt sauber, weil das Zählen der Palindrome innerhalb des WordSet-Objekts durchgeführt wird. Im Folgenden finden Sie eine Beispielimplementierung der Methode.

import java.util.ArrayList;

public class WordSet {
    private ArrayList<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;
    }
}

Die Methode 'palindromes' verwendet die Hilfsmethode 'isPalindrome', um zu überprüfen, ob das ihr übergebene Wort tatsächlich ein Palindrom ist.

Programmier-Tipps

Im obigen größeren Beispiel haben wir den hier gegebenen Ratschlägen gefolgt.

  • Gehen Sie in kleinen Schritten vor

    • Versuchen Sie, das Programm in mehrere Teilprobleme zu unterteilen und arbeiten Sie jeweils nur an einem Teilproblem
    • Testen Sie immer, ob der Programmcode in die richtige Richtung voranschreitet, also: Testen Sie, ob die Lösung des Teilproblems korrekt ist
    • Erkennen Sie die Bedingungen, die erfordern, dass das Programm anders funktioniert. Im obigen Beispiel brauchten wir eine andere Funktionalität, um zu überprüfen, ob ein Wort bereits eingegeben wurde.
  • Schreiben Sie so "sauberen" Code wie möglich

    • Indentieren Sie Ihren Code
    • Verwenden Sie beschreibende Methoden- und Variablennamen
    • Machen Sie Ihre Methoden nicht zu lang, nicht einmal die Hauptmethode
    • Tun Sie nur eine Sache innerhalb einer Methode
    • Entfernen Sie allen Copy-Paste-Code
    • Ersetzen Sie die "schlechten" und unsauberen Teile Ihres Codes durch sauberen Code
  • Falls nötig, betrachten Sie das Programm aus einer übergeordneten Perspektive und bewerten Sie es als Ganzes. Wenn das nicht funktioniert, könnte es eine gute Idee sein, in einen früheren Zustand zurückzukehren, in dem der Code noch funktionierte. Als Fazit könnte man sagen, dass ein nicht funktionsfähiges Programm eher selten durch das Hinzufügen von mehr Code repariert wird.

Programmiererinnen und Programmierer folgen diesen Konventionen, um die Programmierung zu erleichtern. Das Befolgen dieser Regeln erleichtert auch das Lesen von Programmen, deren Pflege und das Bearbeiten in Teams.

Loading
Loading

Von einer Einheit zu vielen Teilen

Betrachten wir ein Programm, das die Benutzerin oder den Benutzer auffordert, Prüfungspunkte einzugeben und diese in Noten umzuwandeln. Schließlich gibt das Programm die Verteilung der Noten als Sterne aus. Das Programm stoppt das Einlesen von Eingaben, wenn die Benutzerin oder der Benutzer eine leere Zeichenkette eingibt. Ein Beispielprogramm sieht wie folgt aus:

Beispielausgabe

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: **

Wie fast alle Programme kann dieses Programm in main als eine Einheit geschrieben werden. Hier ist eine Möglichkeit.

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

public class Program {

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

        ArrayList<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;
        }
    }
}

Trennen wir das Programm in kleinere Teile. Dies kann geschehen, indem mehrere diskrete Verantwortungsbereiche innerhalb des Programms identifiziert werden. Das Verfolgen der Noten, einschließlich der Umwandlung von Punkten in Noten, könnte in einer eigenen Klasse erfolgen. Zusätzlich könnten wir eine neue Klasse für die Benutzungsschnittstelle erstellen.

Programmlogik

Die Programmlogik umfasst Teile, die für die Ausführung des Programms entscheidend sind, wie beispielsweise Funktionalitäten, die Informationen speichern. Aus dem vorherigen Beispiel können wir die Teile trennen, die für die Speicherung der Noteninformationen verantwortlich sind. Aus diesen können wir eine Klasse namens 'GradeRegister' machen, die dafür verantwortlich ist, die Anzahl der verschiedenen Noten, die Studierende erhalten haben, zu verfolgen. Im Register können wir Noten entsprechend den Punkten hinzufügen. Außerdem können wir das Register verwenden, um zu fragen, wie viele Personen eine bestimmte Note erhalten haben.

Ein Beispiel für eine Klasse folgt.

import java.util.ArrayList;

public class GradeRegister {

    private ArrayList<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;
    }
}

Nachdem das Notenregister in eine Klasse ausgelagert wurde, können wir die damit verbundene Funktionalität aus unserem Hauptprogramm entfernen. Das Hauptprogramm sieht nun so aus:

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;
        }
    }
}

Das Trennen der Programmlogik bietet einen großen Vorteil für die Wartung des Programms. Da die Programmlogik – in diesem Fall das GradeRegister – eine eigene Klasse ist, kann sie auch unabhängig von den anderen Teilen des Programms getestet werden. Wenn Sie möchten, könnten Sie die Klasse GradeRegister kopieren und in Ihren anderen Programmen verwenden. Unten ist ein Beispiel für einen einfachen manuellen Test – dieses Experiment bezieht sich nur auf einen kleinen Teil der Funktionalität des Registers.

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);

Textbasierte Benutzungsschnittstelle

Typischerweise hat jedes Programm eine eigene Benutzungsschnittstelle. Wir werden die Klasse UserInterface erstellen und sie vom Hauptprogramm trennen. Die Benutzungsschnittstelle erhält zwei Parameter in ihrem Konstruktor: ein Notenregister zum Speichern der Noten und ein Scanner-Objekt, das zum Lesen der Eingaben verwendet wird.

Da wir jetzt über eine separate Benutzungsschnittstelle verfügen, wird das Hauptprogramm, das das gesamte Programm initialisiert, sehr klar.

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();
    }
}

Schauen wir uns an, wie die Benutzungsschnittstelle implementiert ist. Es gibt zwei wesentliche Teile der Benutzungsschnittstelle: das Lesen der Punkte und das Ausgeben der Notenverteilung.

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() {
    }
}

Wir können den Code zum Lesen von Prüfungspunkten und zur Ausgabe der Notenverteilung nahezu unverändert aus dem vorherigen Hauptprogramm übernehmen. Im unten stehenden Programm wurden Teile des Codes tatsächlich aus dem früheren Hauptprogramm kopiert, und es wurde eine neue Methode zum Drucken von Sternen erstellt – dies klärt die Methode, die zum Drucken der Notenverteilung verwendet wird.

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
Loading
Sie haben das Ende dieses Abschnitts erreicht! Weiter zum nächsten Abschnitt: