ProjektBaut auf Katas: Listen auf
image: teaser.webp image_alt: "Drei Kassen als Zeitleisten von 9:00 bis 9:15: farbige Blöcke nebeneinander, je eine Kundin oder ein Kunde, die Zahl darin die Artikel, ein langer Block für 59 Artikel, ein kurzer für einen." kind: projekt duration: 3 Doppelstunden languages: [ruby, python, javascript, dart, java] curriculum: "Sek I: Algorithmisches Problemlösen (Grundlagen der Objektorientierung); Oberstufe: objektorientierte Modellierung (UML-Klassendiagramme), objektorientierte Programmierung" topics: [objektorientierung] requires: [kata-lists]

Klassen entwerfen: die Expresskasse

Ein Supermarkt überlegt, eine seiner drei Kassen zur Expresskasse zu machen. Du entwirfst dafür Klassen, zeichnest sie als Klassendiagramm und programmierst eine Simulation, die einen Samstagvormittag nachspielt: mit Kundinnen und Kunden, Kassen, die sich merken, wann sie frei sind, und Kassen, die erben und doch anders arbeiten. Am Ende weißt du, ob sich die Expresskasse lohnt.

1. Die Geschichte

Die Filialleitung eines Supermarkts erzählt:

»Samstags zwischen neun und elf ist bei uns am meisten los. Wir haben drei Kassen, und die Schlangen reichen manchmal bis zu den Kühlregalen. Viele kaufen nur ein paar Sachen und stehen trotzdem hinter vollen Einkaufswagen. Ich überlege, eine der drei Kassen zur Expresskasse zu machen, für höchstens zehn Artikel. Oder ich stelle statt der dritten Kasse zwei Selbstbedienungskassen auf: Da scannen die Leute selbst, das dauert pro Artikel länger, aber Bezahlen geht schneller. Ausprobieren kann ich das an einem Samstag nicht, ohne dass sich alle ärgern.«

Ein Programm kann es ausprobieren: Es spielt den Vormittag nach, einmal mit jeder Aufteilung der Kassen, und misst, wie lange die Leute warten. So ein Programm heißt Simulation. Die ersten objektorientierten Programme waren solche Simulationen: Die Sprache Simula entstand in den 1960er-Jahren am Norwegischen Rechenzentrum in Oslo, um Schlangen an Schaltern, Häfen und Fabriken nachzuspielen, und Simula 67 brachte Klassen, Objekte und Vererbung mit, die heute fast jede Sprache hat.

Eine Simulation kann nicht alles nachspielen, was in der Geschichte vorkommt. Sie braucht die Gegenstände, mit denen sie arbeitet, und von denen gibt es meist viele: viele Kundinnen und Kunden, mehrere Kassen. Ein guter Anfang sind die Nomen der Geschichte, und die Verben dazu, denn sie sagen, was mit den Gegenständen geschieht.

Aufgabe 1Klasse oder Attribut?

In der Geschichte stehen die Nomen Kasse, Kundin, Zahl der Artikel und Expresskasse. Drei davon werden Klassen, weil es von ihnen mehrere gibt und jede eigene Werte und eigenes Verhalten hat. Welches wird keine Klasse, sondern ein Attribut einer anderen?

Klasse oder Attribut?

Hier geht es weiter, sobald du deine Antwort oben abgegeben hast.

Kundin und Kasse werden Klassen, die Expresskasse auch. Eine Klasse ist der Bauplan für viele gleichartige Objekte: Jede Kundin ist ein Objekt der Klasse Kundin, mit ihren eigenen Werten. Die Zahl der Artikel ist so ein Wert, ein Attribut: Jede Kundin hat ihre eigene. Die Verben werden Methoden, das, was ein Objekt tun kann: Eine Kasse bedient eine Kundin. Die Kühlregale kommen im Programm nicht vor; die Simulation tut nichts mit ihnen.

2. Einrichten

Lege im Workspace einen Ordner supermarkt an und darin eine Datei supermarkt.uml für das Klassendiagramm. Die UML-Werkstatt zeigt das Diagramm neben der Datei, sobald du oben rechts auf das Symbol mit dem Diagramm klickst, und unterstreicht, was sie nicht versteht. Alles, was du in eine .uml-Datei schreiben kannst, steht im Handbuch unter Klassendiagramme als Text.

Für den Code kommt supermarket.rb dazu:

mkdir -p ~/supermarkt
touch ~/supermarkt/supermarkt.uml ~/supermarkt/supermarket.rb

Für den Code kommt supermarket.py dazu:

mkdir -p ~/supermarkt
touch ~/supermarkt/supermarkt.uml ~/supermarkt/supermarket.py

Für den Code kommt supermarket.js dazu:

mkdir -p ~/supermarkt
touch ~/supermarkt/supermarkt.uml ~/supermarkt/supermarket.js

Für den Code kommt supermarket.dart dazu:

mkdir -p ~/supermarkt
touch ~/supermarkt/supermarkt.uml ~/supermarkt/supermarket.dart

In Java steht jede Klasse in einer eigenen Datei, die heißt wie sie. Für die Kundinnen und Kunden kommt Customer.java dazu:

mkdir -p ~/supermarkt
touch ~/supermarkt/supermarkt.uml ~/supermarkt/Customer.java

3. Die erste Klasse

Im Diagramm

Ein Klassendiagramm (UML, Unified Modeling Language) zeigt jede Klasse als Kasten mit drei Fächern: oben ihr Name, in der Mitte die Attribute, unten die Methoden. In der UML-Werkstatt schreibst du es als Text; so sieht eine Klasse für eine Lampe aus:

# Eine Lampe, als Beispiel
class Lamp
  - color: String
  - brightness: int
  + Lamp(color: String)
  + toggle(): void

Jedes Attribut steht in einer eigenen Zeile, name: Typ, die Methoden mit ihren Parametern in Klammern und dem Typ dessen, was sie zurückgeben; void heißt, sie geben nichts zurück. Eine Methode, die heißt wie die Klasse und keinen Typ hat, ist der Konstruktor: Er legt ein neues Objekt an und bekommt die Werte, die es von Anfang an braucht.

Das Zeichen davor sagt, wer es sehen darf: - ist privat, nur die Klasse selbst kommt heran, + ist öffentlich. Attribute sind fast immer privat. Von außen fragt man ein Objekt über seine Methoden; so kann es selbst aufpassen, dass seine Werte stimmen. Das heißt Kapselung.

Die Typen schreibst du wie in Java, int, double, boolean, String, egal in welcher Sprache du nachher programmierst: Ein Klassendiagramm beschreibt einen Entwurf, keinen Code. Die Namen schreibst du so wie nachher im Code, dann findest du jede Methode aus dem Diagramm dort wieder.

Eine Kundin oder ein Kunde bringt in die Simulation zwei Werte mit: wie viele Artikel im Wagen liegen, und wann sie an den Kassen ankommt, in Sekunden nach neun Uhr.

Aufgabe 2Die Kundin im Diagramm

Schreib in supermarkt/supermarkt.uml die Klasse Customer mit den Attributen items und arrival, beide int und privat, und mit einem öffentlichen Konstruktor, der beide bekommt.

Tipp

Eine Klasse beginnt mit class Customer; alles, was zu ihr gehört, steht darunter eingerückt, eine Zeile je Attribut oder Methode.

Nächster Tipp

Ein Attribut ist - name: Typ, der Konstruktor + Customer(…) mit den Parametern in der Klammer, name: Typ, durch Komma getrennt.

Klassendiagramm oder ER-Modell?

Wer ein ER-Modell kennt, etwa aus der Filmdatenbank, sieht Ähnliches: Kästen mit Namen und Werten, Linien mit Zahlen daran. Die beiden beschreiben aber Verschiedenes:

ER-Modell Klassendiagramm
beschreibt Daten, die gespeichert werden Objekte in einem laufenden Programm
enthält Entitäten, Attribute, Beziehungen Klassen, Attribute, Methoden, Assoziationen, Vererbung
am Ende bleibt alles in der Datenbank sind die Objekte weg, sobald das Programm endet
wird zu Tabellen Klassen im Code

Ein ER-Modell sagt, was man sich merken muss, ein Klassendiagramm, wer was tut. Die Kundinnen und Kunden dieser Simulation muss sich niemand merken: Es zählt, was beim Nachspielen passiert, und danach nur eine Zahl, die mittlere Wartezeit. Darum braucht die Simulation keine Datenbank. Ein Kassensystem im echten Supermarkt hat beides: Artikel und Preise stehen in einer Datenbank, und das Programm an der Kasse arbeitet mit Objekten. Zwischen beiden vermittelt oft ein ORM (Object-Relational Mapping): Es liest Zeilen einer Tabelle als Objekte und schreibt Objekte als Zeilen zurück.

Im Code

In Ruby beginnt eine Klasse mit class und endet mit end. Der Konstruktor heißt initialize; Customer.new ruft ihn auf. Was mit @ beginnt, ist ein Attribut des Objekts, eine Instanzvariable, und von außen nicht zu sehen. attr_reader schreibt eine Methode, die es zurückgibt. Eine Kundin mit ihren Artikeln:

class Customer
  attr_reader :items

  def initialize(items)
    @items = items
  end
end

anna = Customer.new(12)
puts anna.items

In Python beginnt eine Klasse mit class, eingerückt steht alles, was dazu gehört. Der Konstruktor heißt __init__; Customer(…) ruft ihn auf. Jede Methode bekommt als Erstes self, das Objekt selbst, und was in self._items steht, gehört diesem Objekt. Der Unterstrich sagt: nur für die Klasse selbst. @property macht aus einer Methode etwas, das man liest wie ein Attribut, aber nicht ändern kann. Eine Kundin mit ihren Artikeln:

class Customer:
    def __init__(self, items):
        self._items = items

    @property
    def items(self):
        return self._items


anna = Customer(12)
print(anna.items)

In JavaScript beginnt eine Klasse mit class. Der Konstruktor heißt constructor; new Customer(…) ruft ihn auf. Felder mit # vor dem Namen sind privat, von außen kommt niemand heran. Ein get liest man wie ein Feld, ändern kann man es so nicht. Eine Kundin mit ihren Artikeln:

class Customer {
  #items;

  constructor(items) {
    this.#items = items;
  }

  get items() {
    return this.#items;
  }
}

const anna = new Customer(12);
console.log(anna.items);

In Dart beginnt eine Klasse mit class. Der Konstruktor heißt wie die Klasse; Customer(this._items) setzt das Feld gleich mit dem Wert, den er bekommt. Ein Name, der mit _ beginnt, ist privat. Ein get liest man wie ein Feld, ändern kann man es so nicht. Eine Kundin mit ihren Artikeln:

class Customer {
  final int _items;

  Customer(this._items);

  int get items => _items;
}

void main() {
  final anna = Customer(12);
  print(anna.items);
}

In Java steht die Klasse Customer in Customer.java. Attribute stehen oben, mit Typ und private. Der Konstruktor heißt wie die Klasse und hat keinen Rückgabetyp; new Customer(…) ruft ihn auf. this.items ist das Attribut, items allein der Parameter. Eine öffentliche Methode, die nur einen Wert zurückgibt, heißt Getter. Eine Kundin mit ihren Artikeln:

public class Customer {
    private int items;

    public Customer(int items) {
        this.items = items;
    }

    public int getItems() {
        return items;
    }
}

Im Diagramm stehen solche Getter meist nicht: Dass man ein Attribut lesen kann, gehört zum Code, nicht zum Entwurf.

Jetzt bist du dran: Eine Kundin hat auch eine Ankunftszeit. Gib dem Konstruktor einen zweiten Parameter, merk dir die Zeit in einem zweiten privaten Attribut und schreib die Methode, die sie zurückgibt, wie bei den Artikeln.
Tipp

Alles, was die Artikel haben, braucht die Ankunftszeit auch: einen Parameter in initialize, eine Zeile @arrival = arrival und einen Namen hinter attr_reader.

Lösung anzeigen
class Customer
  attr_reader :items, :arrival

  def initialize(items, arrival)
    @items = items
    @arrival = arrival
  end
end
Tipp

Alles, was die Artikel haben, braucht die Ankunftszeit auch: einen Parameter in __init__, eine Zeile self._arrival = arrival und eine @property.

Lösung anzeigen
class Customer:
    def __init__(self, items, arrival):
        self._items = items
        self._arrival = arrival

    @property
    def items(self):
        return self._items

    @property
    def arrival(self):
        return self._arrival
Tipp

Alles, was die Artikel haben, braucht die Ankunftszeit auch: ein Feld #arrival;, einen Parameter im constructor, eine Zeile this.#arrival = arrival; und ein get.

Lösung anzeigen
class Customer {
  #items;
  #arrival;

  constructor(items, arrival) {
    this.#items = items;
    this.#arrival = arrival;
  }

  get items() {
    return this.#items;
  }

  get arrival() {
    return this.#arrival;
  }
}
Tipp

Alles, was die Artikel haben, braucht die Ankunftszeit auch: ein Feld _arrival, einen zweiten Parameter this._arrival im Konstruktor und ein get.

Lösung anzeigen
class Customer {
  final int _items;
  final int _arrival;

  Customer(this._items, this._arrival);

  int get items => _items;
  int get arrival => _arrival;
}
Tipp

Alles, was die Artikel haben, braucht die Ankunftszeit auch: ein Attribut private int arrival;, einen zweiten Parameter im Konstruktor, eine Zeile this.arrival = arrival; und einen Getter getArrival().

Lösung anzeigen
public class Customer {
    private int items;
    private int arrival;

    public Customer(int items, int arrival) {
        this.items = items;
        this.arrival = arrival;
    }

    public int getItems() {
        return items;
    }

    public int getArrival() {
        return arrival;
    }
}
Aufgabe 3Die Kundin im Code

Schreib in supermarket.rb die Klasse Customer: Der Konstruktor bekommt die Zahl der Artikel und die Ankunftszeit, items und arrival geben sie zurück.

Aufgabe 3Die Kundin im Code

Schreib in supermarket.py die Klasse Customer: Der Konstruktor bekommt die Zahl der Artikel und die Ankunftszeit, die Properties items und arrival geben sie zurück.

Aufgabe 3Die Kundin im Code

Schreib in supermarket.js die Klasse Customer: Der Konstruktor bekommt die Zahl der Artikel und die Ankunftszeit, die Getter items und arrival geben sie zurück.

Aufgabe 3Die Kundin im Code

Schreib in supermarket.dart die Klasse Customer: Der Konstruktor bekommt die Zahl der Artikel und die Ankunftszeit, die Getter items und arrival geben sie zurück.

Aufgabe 3Die Kundin im Code

Schreib in Customer.java die Klasse Customer: Der Konstruktor bekommt die Zahl der Artikel und die Ankunftszeit, getItems() und getArrival() geben sie zurück.

Ausprobieren: Leg unten in supermarket.rb zwei Kundinnen an und gib ihre Artikel aus, ruby supermarket.rb im Ordner supermarkt. Versuch danach, die Artikel zu ändern, anna.items = 3: Was sagt Ruby dazu?
Ausprobieren: Leg unten in supermarket.py zwei Kundinnen an und gib ihre Artikel aus, python3 supermarket.py im Ordner supermarkt. Versuch danach, die Artikel zu ändern, anna.items = 3: Was sagt Python dazu?
Ausprobieren: Leg unten in supermarket.js zwei Kundinnen an und gib ihre Artikel aus, node supermarket.js im Ordner supermarkt. Versuch danach, das private Feld zu lesen, console.log(anna.#items): Was sagt JavaScript dazu?
Ausprobieren: Leg in main zwei Kundinnen an und gib ihre Artikel aus, dart supermarket.dart im Ordner supermarkt. Versuch danach, die Artikel zu ändern, anna.items = 3;: Was sagt Dart dazu?
Ausprobieren: Schreib eine Klasse Main in Main.java mit public static void main(String[] args), leg darin zwei Kundinnen an und gib ihre Artikel aus. Starte sie im Terminal im Ordner supermarkt mit java Main.java. Ändere danach in Main die Artikel direkt, anna.items = 3;: Was sagt Java dazu?

4. Zustand und Verhalten

Eine Kasse bedient eine Kundin nach der anderen. Wie lange das dauert, hängt an den Artikeln: Die Kassiererin scannt jeden in 3 Sekunden, und Bezahlen dauert eine Minute. Eine Kundin mit 10 Artikeln braucht also 60 + 3 · 10 = 90 Sekunden.

Ein Objekt merkt sich nicht nur, was es von Anfang an bekommt. Eine Kasse ist mal frei, mal beschäftigt, und das ändert sich mit jeder Kundin: Das ist ihr Zustand, und er ändert sich durch ihre Methoden, ihr Verhalten.

Vermutung 4Wie lange wartet sie?

An einer leeren Kasse kommt in Sekunde 0 eine Kundin mit 10 Artikeln an, in Sekunde 30 eine mit 5 Artikeln:

first = Customer.new(10, 0)
second = Customer.new(5, 30)

Wie viele Sekunden wartet die zweite, bis sie drankommt?

Wie lange wartet sie?
Vermutung 4Wie lange wartet sie?

An einer leeren Kasse kommt in Sekunde 0 eine Kundin mit 10 Artikeln an, in Sekunde 30 eine mit 5 Artikeln:

first = Customer(10, 0)
second = Customer(5, 30)

Wie viele Sekunden wartet die zweite, bis sie drankommt?

Wie lange wartet sie?
Vermutung 4Wie lange wartet sie?

An einer leeren Kasse kommt in Sekunde 0 eine Kundin mit 10 Artikeln an, in Sekunde 30 eine mit 5 Artikeln:

const first = new Customer(10, 0);
const second = new Customer(5, 30);

Wie viele Sekunden wartet die zweite, bis sie drankommt?

Wie lange wartet sie?
Vermutung 4Wie lange wartet sie?

An einer leeren Kasse kommt in Sekunde 0 eine Kundin mit 10 Artikeln an, in Sekunde 30 eine mit 5 Artikeln:

final first = Customer(10, 0);
final second = Customer(5, 30);

Wie viele Sekunden wartet die zweite, bis sie drankommt?

Wie lange wartet sie?
Vermutung 4Wie lange wartet sie?

An einer leeren Kasse kommt in Sekunde 0 eine Kundin mit 10 Artikeln an, in Sekunde 30 eine mit 5 Artikeln:

Customer first = new Customer(10, 0);
Customer second = new Customer(5, 30);

Wie viele Sekunden wartet die zweite, bis sie drankommt?

Wie lange wartet sie?

Hier geht es weiter, sobald du deine Antwort oben abgegeben hast.

Sie wartet 60 Sekunden. Die erste Kundin ist von Sekunde 0 bis 90 an der Reihe; die zweite kommt in Sekunde 30 und kommt in Sekunde 90 dran. Mehr muss die Kasse dafür nicht wissen als eine Zahl: ab wann sie wieder frei ist. Kommt eine Kundin, fängt die Kasse mit ihr an, sobald sie frei ist, aber nicht, bevor die Kundin da ist: Ist die Kasse schon länger frei, wartet niemand, und die Kasse stand eine Weile leer. Danach ist sie frei, wenn diese Kundin fertig ist.

Im Diagramm bekommt die Kasse dafür ein Attribut und zwei Methoden: eine, die sagt, wie lange eine Kundin braucht, und eine, die sie bedient und zurückgibt, wie lange sie gewartet hat.

class Checkout
  - free_at: int
  + Checkout()
  + service_time(customer: Customer): int
  + serve(customer: Customer): int

Ruby schreibt Namen aus zwei Wörtern klein mit Unterstrich: free_at, service_time.

class Checkout
  - free_at: int
  + Checkout()
  + service_time(customer: Customer): int
  + serve(customer: Customer): int

Python schreibt Namen aus zwei Wörtern klein mit Unterstrich: free_at, service_time.

class Checkout
  - freeAt: int
  + Checkout()
  + serviceTime(customer: Customer): int
  + serve(customer: Customer): int

Eine neue Kasse ist sofort frei, ab Sekunde 0. Die Methode, die bedient, bekommt eine Kundin als Parameter und fragt sie nach ihren Werten: Die Kasse kennt die Kundin nur für diesen einen Aufruf.

Jetzt bist du dran: Schreib die Klasse für die Kasse. Sie merkt sich, ab wann sie frei ist, und man kann es abfragen; sie sagt, wie lange eine Kundin braucht; und sie bedient eine Kundin: Sie fängt an, sobald sie frei und die Kundin da ist, merkt sich, wann sie danach frei ist, und gibt zurück, wie viele Sekunden die Kundin gewartet hat.
Tipp

@free_at = 0 in initialize und attr_reader :free_at. service_time rechnet 60 + 3 * customer.items.

Nächster Tipp

In serve ist der Anfang der spätere von zwei Zeitpunkten: start = [customer.arrival, @free_at].max. Danach ist die Kasse ab start + service_time(customer) frei, und gewartet hat die Kundin start - customer.arrival.

Lösung anzeigen
class Checkout
  attr_reader :free_at

  def initialize
    @free_at = 0
  end

  def service_time(customer)
    60 + 3 * customer.items
  end

  def serve(customer)
    start = [customer.arrival, @free_at].max
    @free_at = start + service_time(customer)
    start - customer.arrival
  end
end
Tipp

self._free_at = 0 in __init__ und eine @property free_at. service_time rechnet 60 + 3 * customer.items.

Nächster Tipp

In serve ist der Anfang der spätere von zwei Zeitpunkten: start = max(customer.arrival, self._free_at). Danach ist die Kasse ab start + self.service_time(customer) frei, und gewartet hat die Kundin start - customer.arrival.

Lösung anzeigen
class Checkout:
    def __init__(self):
        self._free_at = 0

    @property
    def free_at(self):
        return self._free_at

    def service_time(self, customer):
        return 60 + 3 * customer.items

    def serve(self, customer):
        start = max(customer.arrival, self._free_at)
        self._free_at = start + self.service_time(customer)
        return start - customer.arrival
Tipp

Ein Feld #freeAt = 0; und ein get freeAt(). serviceTime rechnet 60 + 3 * customer.items.

Nächster Tipp

In serve ist der Anfang der spätere von zwei Zeitpunkten: const start = Math.max(customer.arrival, this.#freeAt);. Danach ist die Kasse ab start + this.serviceTime(customer) frei, und gewartet hat die Kundin start - customer.arrival.

Lösung anzeigen
class Checkout {
  #freeAt = 0;

  get freeAt() {
    return this.#freeAt;
  }

  serviceTime(customer) {
    return 60 + 3 * customer.items;
  }

  serve(customer) {
    const start = Math.max(customer.arrival, this.#freeAt);
    this.#freeAt = start + this.serviceTime(customer);
    return start - customer.arrival;
  }
}
Tipp

Ein Feld int _freeAt = 0; und ein get freeAt. serviceTime rechnet 60 + 3 * customer.items.

Nächster Tipp

In serve ist der Anfang der spätere von zwei Zeitpunkten: die Ankunft oder _freeAt, je nachdem, was größer ist. Danach ist die Kasse ab start + serviceTime(customer) frei, und gewartet hat die Kundin start - customer.arrival.

Lösung anzeigen
class Checkout {
  int _freeAt = 0;

  int get freeAt => _freeAt;

  int serviceTime(Customer customer) => 60 + 3 * customer.items;

  int serve(Customer customer) {
    final start = customer.arrival > _freeAt ? customer.arrival : _freeAt;
    _freeAt = start + serviceTime(customer);
    return start - customer.arrival;
  }
}
Tipp

In Checkout.java ein Attribut private int freeAt = 0; und ein Getter getFreeAt(). serviceTime rechnet 60 + 3 * customer.getItems().

Nächster Tipp

In serve ist der Anfang der spätere von zwei Zeitpunkten: int start = Math.max(customer.getArrival(), freeAt);. Danach ist die Kasse ab start + serviceTime(customer) frei, und gewartet hat die Kundin start - customer.getArrival().

Lösung anzeigen
public class Checkout {
    private int freeAt = 0;

    public int getFreeAt() {
        return freeAt;
    }

    public int serviceTime(Customer customer) {
        return 60 + 3 * customer.getItems();
    }

    public int serve(Customer customer) {
        int start = Math.max(customer.getArrival(), freeAt);
        freeAt = start + serviceTime(customer);
        return start - customer.getArrival();
    }
}
Aufgabe 5Die Kasse

Schreib die Klasse Checkout. Eine neue Kasse ist ab Sekunde 0 frei; free_at sagt, ab wann sie frei ist. service_time(customer) gibt 60 + 3 Sekunden je Artikel zurück. serve(customer) fängt an, sobald die Kasse frei und die Kundin da ist, merkt sich, wann die Kasse danach frei ist, und gibt zurück, wie viele Sekunden die Kundin gewartet hat. Die Prüfung legt für jeden Fall eine neue Kasse an.

Aufgabe 5Die Kasse

Schreib die Klasse Checkout. Eine neue Kasse ist ab Sekunde 0 frei; free_at sagt, ab wann sie frei ist. service_time(customer) gibt 60 + 3 Sekunden je Artikel zurück. serve(customer) fängt an, sobald die Kasse frei und die Kundin da ist, merkt sich, wann die Kasse danach frei ist, und gibt zurück, wie viele Sekunden die Kundin gewartet hat. Die Prüfung legt für jeden Fall eine neue Kasse an.

Aufgabe 5Die Kasse

Schreib die Klasse Checkout. Eine neue Kasse ist ab Sekunde 0 frei; freeAt sagt, ab wann sie frei ist. serviceTime(customer) gibt 60 + 3 Sekunden je Artikel zurück. serve(customer) fängt an, sobald die Kasse frei und die Kundin da ist, merkt sich, wann die Kasse danach frei ist, und gibt zurück, wie viele Sekunden die Kundin gewartet hat. Die Prüfung legt für jeden Fall eine neue Kasse an.

Aufgabe 5Die Kasse

Schreib die Klasse Checkout. Eine neue Kasse ist ab Sekunde 0 frei; freeAt sagt, ab wann sie frei ist. serviceTime(customer) gibt 60 + 3 Sekunden je Artikel zurück. serve(customer) fängt an, sobald die Kasse frei und die Kundin da ist, merkt sich, wann die Kasse danach frei ist, und gibt zurück, wie viele Sekunden die Kundin gewartet hat. Die Prüfung legt für jeden Fall eine neue Kasse an.

Aufgabe 5Die Kasse

Schreib die Klasse Checkout. Eine neue Kasse ist ab Sekunde 0 frei; getFreeAt() sagt, ab wann sie frei ist. serviceTime(customer) gibt 60 + 3 Sekunden je Artikel zurück. serve(customer) fängt an, sobald die Kasse frei und die Kundin da ist, merkt sich, wann die Kasse danach frei ist, und gibt zurück, wie viele Sekunden die Kundin gewartet hat. Die Prüfung legt für jeden Fall eine neue Kasse an.

5. Der Supermarkt

Der Supermarkt hat mehrere Kassen, und er kennt sie die ganze Zeit, nicht nur für einen Aufruf. So eine dauerhafte Verbindung zwischen Objekten zweier Klassen heißt Assoziation. Im Diagramm ist sie eine Linie zwischen den Kästen, mit einem Namen und an jedem Ende einer Multiplizität: wie viele Objekte der Klasse an diesem Ende zu einem Objekt am anderen gehören. Ein Beispiel aus der Schule:

class School
  - name: String
  + School(name: String)

class Room
  - number: String
  + Room(number: String)

association rooms: School 1 -- 1..* Room

Eine Schule hat einen oder mehrere Räume, 1..* an Room; jeder Raum gehört zu genau einer Schule, 1 an School. Die Zahl steht also an der Klasse, deren Anzahl sie nennt. Weitere Multiplizitäten sind 0..1 (keins oder eins), * (beliebig viele) und Bereiche wie 2..4. Die Räume stehen nicht noch einmal als Attribut im Kasten der Schule: Die Linie sagt es schon. Im Code wird daraus trotzdem ein Attribut, eine Liste.

Eine Kundin dagegen bekommt die Kasse nur als Parameter, für einen Aufruf: Das ist keine Assoziation, die Kasse kennt sie danach nicht mehr.

Der Supermarkt nimmt die Kundinnen in der Reihenfolge an, in der sie kommen, und schickt jede an eine seiner Kassen. Dabei zählt er mit, wie lange alle zusammen gewartet haben und wie viele es waren, damit er am Ende die mittlere Wartezeit sagen kann, und die ist eine Kommazahl, double.

Aufgabe 6Der Supermarkt im Diagramm

Ergänze das Diagramm um die Klasse Supermarket mit einem Konstruktor, der eine Liste von Kassen bekommt, checkouts: List<Checkout>, und den Methoden arrive(customer: Customer): int und average_wait(): double. Dazu Checkout mit - free_at: int, einem Konstruktor ohne Parameter, service_time(customer: Customer): int und serve(customer: Customer): int. Verbinde Supermarket und Checkout mit einer Assoziation checkouts, deren Multiplizitäten sagen: Ein Supermarkt hat eine oder mehrere Kassen, jede Kasse gehört zu genau einem Supermarkt.

Tipp

Eine Multiplizität steht an der Klasse, um deren Anzahl es geht: Supermarket 1 -- 1..* Checkout heißt, an einer Kasse hängt genau ein Supermarkt, an einem Supermarkt mindestens eine Kasse.

Aufgabe 6Der Supermarkt im Diagramm

Ergänze das Diagramm um die Klasse Supermarket mit einem Konstruktor, der eine Liste von Kassen bekommt, checkouts: List<Checkout>, und den Methoden arrive(customer: Customer): int und average_wait(): double. Dazu Checkout mit - free_at: int, einem Konstruktor ohne Parameter, service_time(customer: Customer): int und serve(customer: Customer): int. Verbinde Supermarket und Checkout mit einer Assoziation checkouts, deren Multiplizitäten sagen: Ein Supermarkt hat eine oder mehrere Kassen, jede Kasse gehört zu genau einem Supermarkt.

Tipp

Eine Multiplizität steht an der Klasse, um deren Anzahl es geht: Supermarket 1 -- 1..* Checkout heißt, an einer Kasse hängt genau ein Supermarkt, an einem Supermarkt mindestens eine Kasse.

Aufgabe 6Der Supermarkt im Diagramm

Ergänze das Diagramm um die Klasse Supermarket mit einem Konstruktor, der eine Liste von Kassen bekommt, checkouts: List<Checkout>, und den Methoden arrive(customer: Customer): int und averageWait(): double. Dazu Checkout mit - freeAt: int, einem Konstruktor ohne Parameter, serviceTime(customer: Customer): int und serve(customer: Customer): int. Verbinde Supermarket und Checkout mit einer Assoziation checkouts, deren Multiplizitäten sagen: Ein Supermarkt hat eine oder mehrere Kassen, jede Kasse gehört zu genau einem Supermarkt.

Tipp

Eine Multiplizität steht an der Klasse, um deren Anzahl es geht: Supermarket 1 -- 1..* Checkout heißt, an einer Kasse hängt genau ein Supermarkt, an einem Supermarkt mindestens eine Kasse.

Aufgabe 6Der Supermarkt im Diagramm

Ergänze das Diagramm um die Klasse Supermarket mit einem Konstruktor, der eine Liste von Kassen bekommt, checkouts: List<Checkout>, und den Methoden arrive(customer: Customer): int und averageWait(): double. Dazu Checkout mit - freeAt: int, einem Konstruktor ohne Parameter, serviceTime(customer: Customer): int und serve(customer: Customer): int. Verbinde Supermarket und Checkout mit einer Assoziation checkouts, deren Multiplizitäten sagen: Ein Supermarkt hat eine oder mehrere Kassen, jede Kasse gehört zu genau einem Supermarkt.

Tipp

Eine Multiplizität steht an der Klasse, um deren Anzahl es geht: Supermarket 1 -- 1..* Checkout heißt, an einer Kasse hängt genau ein Supermarkt, an einem Supermarkt mindestens eine Kasse.

Aufgabe 6Der Supermarkt im Diagramm

Ergänze das Diagramm um die Klasse Supermarket mit einem Konstruktor, der eine Liste von Kassen bekommt, checkouts: List<Checkout>, und den Methoden arrive(customer: Customer): int und averageWait(): double. Dazu Checkout mit - freeAt: int, einem Konstruktor ohne Parameter, serviceTime(customer: Customer): int und serve(customer: Customer): int. Verbinde Supermarket und Checkout mit einer Assoziation checkouts, deren Multiplizitäten sagen: Ein Supermarkt hat eine oder mehrere Kassen, jede Kasse gehört zu genau einem Supermarkt.

Tipp

Eine Multiplizität steht an der Klasse, um deren Anzahl es geht: Supermarket 1 -- 1..* Checkout heißt, an einer Kasse hängt genau ein Supermarkt, an einem Supermarkt mindestens eine Kasse.

Jetzt bist du dran: Schreib die Klasse für den Supermarkt. Der Konstruktor bekommt eine Liste von Kassen. Kommt eine Kundin, geht sie an die Kasse, die am frühesten frei ist; sind zwei gleich früh frei, an die, die in der Liste zuerst steht. Die Kasse bedient sie, und der Supermarkt gibt die Wartezeit zurück. Dazu eine Methode für die mittlere Wartezeit aller, die bisher da waren.
Tipp

Drei Attribute: die Liste @checkouts, @total_wait = 0 und @customers = 0. Die mittlere Wartezeit ist @total_wait.to_f / @customers; ohne to_f rechnet Ruby mit ganzen Zahlen und lässt die Nachkommastellen weg.

Nächster Tipp

In arrive merkt sich best = nil die beste Kasse bisher. Eine Schleife über @checkouts nimmt eine Kasse, wenn es noch keine gibt oder sie früher frei ist als best: best = checkout if best.nil? || checkout.free_at < best.free_at. Mit < statt <= bleibt bei Gleichstand die erste.

Lösung anzeigen
class Supermarket
  def initialize(checkouts)
    @checkouts = checkouts
    @total_wait = 0
    @customers = 0
  end

  def arrive(customer)
    best = nil
    @checkouts.each do |checkout|
      best = checkout if best.nil? || checkout.free_at < best.free_at
    end
    wait = best.serve(customer)
    @total_wait += wait
    @customers += 1
    wait
  end

  def average_wait
    @total_wait.to_f / @customers
  end
end
Tipp

Drei Attribute: die Liste self._checkouts, self._total_wait = 0 und self._customers = 0. Die mittlere Wartezeit ist self._total_wait / self._customers; / gibt in Python immer eine Kommazahl.

Nächster Tipp

In arrive merkt sich best = None die beste Kasse bisher. Eine Schleife über self._checkouts nimmt eine Kasse, wenn es noch keine gibt oder sie früher frei ist als best: if best is None or checkout.free_at < best.free_at:. Mit < statt <= bleibt bei Gleichstand die erste.

Lösung anzeigen
class Supermarket:
    def __init__(self, checkouts):
        self._checkouts = checkouts
        self._total_wait = 0
        self._customers = 0

    def arrive(self, customer):
        best = None
        for checkout in self._checkouts:
            if best is None or checkout.free_at < best.free_at:
                best = checkout
        wait = best.serve(customer)
        self._total_wait += wait
        self._customers += 1
        return wait

    def average_wait(self):
        return self._total_wait / self._customers
Tipp

Drei Felder: die Liste #checkouts, #totalWait = 0 und #customers = 0. Die mittlere Wartezeit ist this.#totalWait / this.#customers.

Nächster Tipp

In arrive merkt sich let best = null; die beste Kasse bisher. Eine Schleife for (const checkout of this.#checkouts) nimmt eine Kasse, wenn es noch keine gibt oder sie früher frei ist als best: if (best === null || checkout.freeAt < best.freeAt). Mit < statt <= bleibt bei Gleichstand die erste.

Lösung anzeigen
class Supermarket {
  #checkouts;
  #totalWait = 0;
  #customers = 0;

  constructor(checkouts) {
    this.#checkouts = checkouts;
  }

  arrive(customer) {
    let best = null;
    for (const checkout of this.#checkouts) {
      if (best === null || checkout.freeAt < best.freeAt) {
        best = checkout;
      }
    }
    const wait = best.serve(customer);
    this.#totalWait += wait;
    this.#customers += 1;
    return wait;
  }

  averageWait() {
    return this.#totalWait / this.#customers;
  }
}
Tipp

Drei Felder: die Liste final List<Checkout> _checkouts;, int _totalWait = 0; und int _customers = 0;. Die mittlere Wartezeit ist _totalWait / _customers; / gibt in Dart immer ein double.

Nächster Tipp

In arrive merkt sich Checkout? best; die beste Kasse bisher, am Anfang null. Eine Schleife for (final checkout in _checkouts) nimmt eine Kasse, wenn es noch keine gibt oder sie früher frei ist als best: if (best == null || checkout.freeAt < best.freeAt). Mit < statt <= bleibt bei Gleichstand die erste. Danach ist best! sicher eine Kasse.

Lösung anzeigen
class Supermarket {
  final List<Checkout> _checkouts;
  int _totalWait = 0;
  int _customers = 0;

  Supermarket(this._checkouts);

  int arrive(Customer customer) {
    Checkout? best;
    for (final checkout in _checkouts) {
      if (best == null || checkout.freeAt < best.freeAt) {
        best = checkout;
      }
    }
    final wait = best!.serve(customer);
    _totalWait += wait;
    _customers += 1;
    return wait;
  }

  double averageWait() => _totalWait / _customers;
}
Tipp

In Supermarket.java, mit import java.util.List; oben: drei Attribute, die Liste private List<Checkout> checkouts;, private int totalWait = 0; und private int customers = 0;. Die mittlere Wartezeit ist (double) totalWait / customers; ohne (double) teilt Java ganze Zahlen und lässt die Nachkommastellen weg.

Nächster Tipp

In arrive merkt sich Checkout best = null; die beste Kasse bisher. Eine Schleife for (Checkout checkout : checkouts) nimmt eine Kasse, wenn es noch keine gibt oder sie früher frei ist als best: if (best == null || checkout.getFreeAt() < best.getFreeAt()). Mit < statt <= bleibt bei Gleichstand die erste.

Lösung anzeigen
import java.util.List;

public class Supermarket {
    private List<Checkout> checkouts;
    private int totalWait = 0;
    private int customers = 0;

    public Supermarket(List<Checkout> checkouts) {
        this.checkouts = checkouts;
    }

    public int arrive(Customer customer) {
        Checkout best = null;
        for (Checkout checkout : checkouts) {
            if (best == null || checkout.getFreeAt() < best.getFreeAt()) {
                best = checkout;
            }
        }
        int wait = best.serve(customer);
        totalWait += wait;
        customers += 1;
        return wait;
    }

    public double averageWait() {
        return (double) totalWait / customers;
    }
}
Aufgabe 7Der Supermarkt

Schreib die Klasse Supermarket. Der Konstruktor bekommt eine Liste von Kassen. arrive(customer) schickt die Kundin an die Kasse, die am frühesten frei ist, bei Gleichstand an die erste in der Liste, lässt sie bedienen und gibt ihre Wartezeit zurück. average_wait() gibt die mittlere Wartezeit aller zurück, die bisher kamen, als Kommazahl.

Aufgabe 7Der Supermarkt

Schreib die Klasse Supermarket. Der Konstruktor bekommt eine Liste von Kassen. arrive(customer) schickt die Kundin an die Kasse, die am frühesten frei ist, bei Gleichstand an die erste in der Liste, lässt sie bedienen und gibt ihre Wartezeit zurück. average_wait() gibt die mittlere Wartezeit aller zurück, die bisher kamen, als Kommazahl.

Aufgabe 7Der Supermarkt

Schreib die Klasse Supermarket. Der Konstruktor bekommt eine Liste von Kassen. arrive(customer) schickt die Kundin an die Kasse, die am frühesten frei ist, bei Gleichstand an die erste in der Liste, lässt sie bedienen und gibt ihre Wartezeit zurück. averageWait() gibt die mittlere Wartezeit aller zurück, die bisher kamen, als Kommazahl.

Aufgabe 7Der Supermarkt

Schreib die Klasse Supermarket. Der Konstruktor bekommt eine Liste von Kassen. arrive(customer) schickt die Kundin an die Kasse, die am frühesten frei ist, bei Gleichstand an die erste in der Liste, lässt sie bedienen und gibt ihre Wartezeit zurück. averageWait() gibt die mittlere Wartezeit aller zurück, die bisher kamen, als Kommazahl.

Aufgabe 7Der Supermarkt

Schreib die Klasse Supermarket. Der Konstruktor bekommt eine Liste von Kassen. arrive(customer) schickt die Kundin an die Kasse, die am frühesten frei ist, bei Gleichstand an die erste in der Liste, lässt sie bedienen und gibt ihre Wartezeit zurück. averageWait() gibt die mittlere Wartezeit aller zurück, die bisher kamen, als Kommazahl.

6. Expresskasse und Selbstbedienung

Vererbung

Eine Expresskasse ist eine Kasse wie jede andere: Sie merkt sich, wann sie frei ist, und bedient genauso schnell. Nur nimmt sie niemanden mit mehr als zehn Artikeln. Eine Selbstbedienungskasse nimmt alle, aber die Leute scannen selbst, 6 Sekunden je Artikel, und bezahlen in 30 Sekunden, weil dort nur Karten gehen.

Was beide mit der Kasse gemeinsam haben, muss niemand noch einmal schreiben: Sie erben von ihr. Alles, was die Kasse hat, haben sie auch; was bei ihnen anders ist, schreiben sie selbst. Ein Beispiel von der Straße:

class Vehicle
  - wheels: int
  + Vehicle(wheels: int)
  + seats(): int

class Bicycle extends Vehicle
  + Bicycle()
  + seats(): int

Im Diagramm zeigt ein Pfeil mit hohler Spitze von der erbenden Klasse zu der, von der sie erbt. Steht eine Methode in beiden, gilt die der erbenden Klasse: Sie überschreibt die andere. Ein Fahrrad hat Räder wie jedes Fahrzeug, aber seine eigene Zahl von Sitzen. Gibt es von einer Klasse selbst keine Objekte, nur von denen, die von ihr erben, ist sie abstrakt: class Vehicle (abstract). Eine normale Kasse ist selbst eine Kasse, sie bleibt also, wie sie ist.

Damit der Supermarkt weiß, wen er wohin schicken darf, fragt er jede Kasse, ob sie eine Kundin nimmt. Die normale Kasse sagt immer ja; die Expresskasse überschreibt das.

Aufgabe 8Expresskasse und Selbstbedienung im Diagramm

Ergänze das Diagramm: Checkout bekommt die Methode accepts?(customer: Customer): boolean. ExpressCheckout und SelfCheckout erben von Checkout; die Expresskasse überschreibt accepts?, die Selbstbedienungskasse service_time.

Tipp

In ExpressCheckout und SelfCheckout steht nur, was bei ihnen anders ist: je eine Methode. Alles andere erben sie.

Aufgabe 8Expresskasse und Selbstbedienung im Diagramm

Ergänze das Diagramm: Checkout bekommt die Methode accepts(customer: Customer): boolean. ExpressCheckout und SelfCheckout erben von Checkout; die Expresskasse überschreibt accepts, die Selbstbedienungskasse service_time.

Tipp

In ExpressCheckout und SelfCheckout steht nur, was bei ihnen anders ist: je eine Methode. Alles andere erben sie.

Aufgabe 8Expresskasse und Selbstbedienung im Diagramm

Ergänze das Diagramm: Checkout bekommt die Methode accepts(customer: Customer): boolean. ExpressCheckout und SelfCheckout erben von Checkout; die Expresskasse überschreibt accepts, die Selbstbedienungskasse serviceTime.

Tipp

In ExpressCheckout und SelfCheckout steht nur, was bei ihnen anders ist: je eine Methode. Alles andere erben sie.

Aufgabe 8Expresskasse und Selbstbedienung im Diagramm

Ergänze das Diagramm: Checkout bekommt die Methode accepts(customer: Customer): boolean. ExpressCheckout und SelfCheckout erben von Checkout; die Expresskasse überschreibt accepts, die Selbstbedienungskasse serviceTime.

Tipp

In ExpressCheckout und SelfCheckout steht nur, was bei ihnen anders ist: je eine Methode. Alles andere erben sie.

Aufgabe 8Expresskasse und Selbstbedienung im Diagramm

Ergänze das Diagramm: Checkout bekommt die Methode accepts(customer: Customer): boolean. ExpressCheckout und SelfCheckout erben von Checkout; die Expresskasse überschreibt accepts, die Selbstbedienungskasse serviceTime.

Tipp

In ExpressCheckout und SelfCheckout steht nur, was bei ihnen anders ist: je eine Methode. Alles andere erben sie.

Vermutung 9Drei Kassen, ein Aufruf

Eine Kundin mit 20 Artikeln, und eine Schleife über drei Kassen:

customer = Customer.new(20, 0)
[Checkout.new, ExpressCheckout.new, SelfCheckout.new].each do |checkout|
  puts checkout.service_time(customer)
end

Was gibt sie aus?

Drei Kassen, ein Aufruf
Vermutung 9Drei Kassen, ein Aufruf

Eine Kundin mit 20 Artikeln, und eine Schleife über drei Kassen:

customer = Customer(20, 0)
for checkout in [Checkout(), ExpressCheckout(), SelfCheckout()]:
    print(checkout.service_time(customer))

Was gibt sie aus?

Drei Kassen, ein Aufruf
Vermutung 9Drei Kassen, ein Aufruf

Eine Kundin mit 20 Artikeln, und eine Schleife über drei Kassen:

const customer = new Customer(20, 0);
for (const checkout of [new Checkout(), new ExpressCheckout(), new SelfCheckout()]) {
  console.log(checkout.serviceTime(customer));
}

Was gibt sie aus?

Drei Kassen, ein Aufruf
Vermutung 9Drei Kassen, ein Aufruf

Eine Kundin mit 20 Artikeln, und eine Schleife über drei Kassen:

final customer = Customer(20, 0);
for (final checkout in [Checkout(), ExpressCheckout(), SelfCheckout()]) {
  print(checkout.serviceTime(customer));
}

Was gibt sie aus?

Drei Kassen, ein Aufruf
Vermutung 9Drei Kassen, ein Aufruf

Eine Kundin mit 20 Artikeln, und eine Schleife über drei Kassen:

Customer customer = new Customer(20, 0);
for (Checkout checkout : List.of(new Checkout(), new ExpressCheckout(), new SelfCheckout())) {
    System.out.println(checkout.serviceTime(customer));
}

Was gibt sie aus?

Drei Kassen, ein Aufruf

Hier geht es weiter, sobald du deine Antwort oben abgegeben hast.

Es gibt 120, 120 und 150 aus. Die Schleife weiß nur, dass Kassen in der Liste stehen; welche Rechnung läuft, entscheidet jedes Objekt selbst, nach seiner Klasse. Die Expresskasse hat keine eigene, also gilt die geerbte; die Selbstbedienungskasse hat ihre eigene. Das heißt Polymorphie: derselbe Aufruf, verschiedenes Verhalten. Auch die Methode, die bedient, steht nur in der Kasse und rechnet in der Selbstbedienungskasse doch mit deren Zeit, weil sie dort die überschriebene Methode aufruft.

Jetzt bist du dran: Gib der Kasse die Methode, die sagt, ob sie eine Kundin nimmt, und schreib die Expresskasse und die Selbstbedienungskasse; jede überschreibt eine Methode. Der Supermarkt schickt eine Kundin dann nur an Kassen, die sie nehmen.
Tipp

class ExpressCheckout < Checkout erbt von Checkout. In Checkout gibt accepts?(customer) true zurück; die Expresskasse schreibt ihre eigene, die customer.items <= 10 zurückgibt. Eine Methode, die mit Ja oder Nein antwortet, endet in Ruby mit ?.

Nächster Tipp

SelfCheckout überschreibt nur service_time. In der Schleife von arrive überspringt next unless checkout.accepts?(customer) eine Kasse, die die Kundin nicht nimmt.

Lösung anzeigen
class Checkout
  ~ …
  def accepts?(customer)
    true
  end
  ~ …
end

class ExpressCheckout < Checkout
  def accepts?(customer)
    customer.items <= 10
  end
end

class SelfCheckout < Checkout
  def service_time(customer)
    30 + 6 * customer.items
  end
end

Und in arrive, als erste Zeile der Schleife:

next unless checkout.accepts?(customer)
Tipp

class ExpressCheckout(Checkout): erbt von Checkout. In Checkout gibt accepts(self, customer) True zurück; die Expresskasse schreibt ihre eigene, die customer.items <= 10 zurückgibt.

Nächster Tipp

SelfCheckout überschreibt nur service_time. In der Schleife von arrive kommt eine Kasse nur in Frage, wenn checkout.accepts(customer) gilt: if checkout.accepts(customer) and (best is None or …):.

Lösung anzeigen
class Checkout:
    ~ …
    def accepts(self, customer):
        return True
    ~ …


class ExpressCheckout(Checkout):
    def accepts(self, customer):
        return customer.items <= 10


class SelfCheckout(Checkout):
    def service_time(self, customer):
        return 30 + 6 * customer.items

Und in arrive, in der Schleife:

if checkout.accepts(customer) and (best is None or checkout.free_at < best.free_at):
Tipp

class ExpressCheckout extends Checkout erbt von Checkout. In Checkout gibt accepts(customer) true zurück; die Expresskasse schreibt ihre eigene, die customer.items <= 10 zurückgibt.

Nächster Tipp

SelfCheckout überschreibt nur serviceTime. In der Schleife von arrive kommt eine Kasse nur in Frage, wenn checkout.accepts(customer) gilt: if (checkout.accepts(customer) && (best === null || …)).

Lösung anzeigen
class Checkout {
  ~ …
  accepts(customer) {
    return true;
  }
  ~ …
}

class ExpressCheckout extends Checkout {
  accepts(customer) {
    return customer.items <= 10;
  }
}

class SelfCheckout extends Checkout {
  serviceTime(customer) {
    return 30 + 6 * customer.items;
  }
}

Und in arrive, in der Schleife:

if (checkout.accepts(customer) && (best === null || checkout.freeAt < best.freeAt)) {
Tipp

class ExpressCheckout extends Checkout erbt von Checkout. In Checkout gibt accepts(Customer customer) true zurück; die Expresskasse schreibt ihre eigene, mit @override davor, die customer.items <= 10 zurückgibt.

Nächster Tipp

SelfCheckout überschreibt nur serviceTime. In der Schleife von arrive kommt eine Kasse nur in Frage, wenn checkout.accepts(customer) gilt: if (checkout.accepts(customer) && (best == null || …)).

Lösung anzeigen
class Checkout {
  ~ …
  bool accepts(Customer customer) => true;
  ~ …
}

class ExpressCheckout extends Checkout {
  @override
  bool accepts(Customer customer) => customer.items <= 10;
}

class SelfCheckout extends Checkout {
  @override
  int serviceTime(Customer customer) => 30 + 6 * customer.items;
}

Und in arrive, in der Schleife:

if (checkout.accepts(customer) && (best == null || checkout.freeAt < best.freeAt)) {
Tipp

public class ExpressCheckout extends Checkout in ExpressCheckout.java erbt von Checkout. In Checkout gibt accepts(Customer customer) true zurück; die Expresskasse schreibt ihre eigene, mit @Override davor, die customer.getItems() <= 10 zurückgibt.

Nächster Tipp

SelfCheckout in SelfCheckout.java überschreibt nur serviceTime. In der Schleife von arrive kommt eine Kasse nur in Frage, wenn checkout.accepts(customer) gilt: if (checkout.accepts(customer) && (best == null || …)).

Lösung anzeigen

In Checkout.java:

public boolean accepts(Customer customer) {
    return true;
}

ExpressCheckout.java und SelfCheckout.java:

public class ExpressCheckout extends Checkout {
    @Override
    public boolean accepts(Customer customer) {
        return customer.getItems() <= 10;
    }
}
public class SelfCheckout extends Checkout {
    @Override
    public int serviceTime(Customer customer) {
        return 30 + 6 * customer.getItems();
    }
}

Und in arrive, in der Schleife:

if (checkout.accepts(customer) && (best == null || checkout.getFreeAt() < best.getFreeAt())) {
Aufgabe 10Expresskasse und Selbstbedienung

Gib Checkout die Methode accepts?(customer), die true zurückgibt. Schreib ExpressCheckout, die nur Kundinnen mit höchstens zehn Artikeln nimmt, und SelfCheckout, bei der eine Kundin 30 Sekunden und 6 je Artikel braucht; beide erben von Checkout. arrive schickt eine Kundin nur an eine Kasse, die sie nimmt.

Aufgabe 10Expresskasse und Selbstbedienung

Gib Checkout die Methode accepts(customer), die True zurückgibt. Schreib ExpressCheckout, die nur Kundinnen mit höchstens zehn Artikeln nimmt, und SelfCheckout, bei der eine Kundin 30 Sekunden und 6 je Artikel braucht; beide erben von Checkout. arrive schickt eine Kundin nur an eine Kasse, die sie nimmt.

Aufgabe 10Expresskasse und Selbstbedienung

Gib Checkout die Methode accepts(customer), die true zurückgibt. Schreib ExpressCheckout, die nur Kundinnen mit höchstens zehn Artikeln nimmt, und SelfCheckout, bei der eine Kundin 30 Sekunden und 6 je Artikel braucht; beide erben von Checkout. arrive schickt eine Kundin nur an eine Kasse, die sie nimmt.

Aufgabe 10Expresskasse und Selbstbedienung

Gib Checkout die Methode accepts(customer), die true zurückgibt. Schreib ExpressCheckout, die nur Kundinnen mit höchstens zehn Artikeln nimmt, und SelfCheckout, bei der eine Kundin 30 Sekunden und 6 je Artikel braucht; beide erben von Checkout. arrive schickt eine Kundin nur an eine Kasse, die sie nimmt.

Aufgabe 10Expresskasse und Selbstbedienung

Gib Checkout die Methode accepts(customer), die true zurückgibt. Schreib ExpressCheckout, die nur Kundinnen mit höchstens zehn Artikeln nimmt, und SelfCheckout, bei der eine Kundin 30 Sekunden und 6 je Artikel braucht; beide erben von Checkout. arrive schickt eine Kundin nur an eine Kasse, die sie nimmt.

In Dart und Java hat jede Variable einen Typ. Die Liste des Supermarkts ist eine Liste von Checkout, und doch darf eine Expresskasse darin stehen: Sie ist eine Kasse, weil sie erbt. Aufgerufen wird trotzdem ihre eigene Methode.

7. Der Samstag

Jetzt spielt das Programm den Vormittag nach. Lade die Kundinnen und Kunden in den Ordner: ein ausgedachter, aber typischer Samstag von neun bis elf, in customers.csv. In jeder Zeile steht, wann jemand an den Kassen ankommt, in Sekunden nach neun, und wie viele Artikel im Wagen liegen.

cd ~/supermarkt
wget https://workspace.hackschule.de/supermarkt/customers.csv
arrival,items
35,19
66,41
67,6

Die Simulation kommt in eine eigene Datei. Sie liest die Zeilen, legt für jede eine Kundin an und schickt sie in den Supermarkt; am Ende gibt sie die mittlere Wartezeit aus.

Lösch vorher die Zeilen zum Ausprobieren unten in supermarket.rb, sonst laufen sie bei jedem Start mit. require_relative lädt die Klassen aus supermarket.rb. Die Simulation, in simulation.rb:

require_relative 'supermarket'

def simulate(checkouts)
  supermarket = Supermarket.new(checkouts)
  File.readlines('customers.csv').drop(1).each do |line|
    arrival, items = line.split(',').map(&:to_i)
    supermarket.arrive(Customer.new(items, arrival))
  end
  supermarket.average_wait
end

puts simulate([Checkout.new, Checkout.new, Checkout.new])

Starte sie im Ordner supermarkt mit ruby simulation.rb.

Lösch vorher die Zeilen zum Ausprobieren unten in supermarket.py, sonst laufen sie bei jedem Start mit. from supermarket import … lädt die Klassen aus supermarket.py. Die Simulation, in simulation.py:

from supermarket import Customer, Checkout, ExpressCheckout, SelfCheckout, Supermarket


def simulate(checkouts):
    supermarket = Supermarket(checkouts)
    with open('customers.csv') as f:
        for line in f.readlines()[1:]:
            arrival, items = [int(x) for x in line.split(',')]
            supermarket.arrive(Customer(items, arrival))
    return supermarket.average_wait()


print(simulate([Checkout(), Checkout(), Checkout()]))

Starte sie im Ordner supermarkt mit python3 simulation.py.

Lösch vorher die Zeilen zum Ausprobieren unten in supermarket.js und schreib stattdessen eine Zeile, die die Klassen für andere Dateien freigibt:

module.exports = { Customer, Checkout, ExpressCheckout, SelfCheckout, Supermarket };

require lädt sie in der Simulation, in simulation.js:

const fs = require('fs');
const { Customer, Checkout, ExpressCheckout, SelfCheckout, Supermarket } = require('./supermarket');

function simulate(checkouts) {
  const supermarket = new Supermarket(checkouts);
  for (const line of fs.readFileSync('customers.csv', 'utf8').trim().split('\n').slice(1)) {
    const [arrival, items] = line.split(',').map(Number);
    supermarket.arrive(new Customer(items, arrival));
  }
  return supermarket.averageWait();
}

console.log(simulate([new Checkout(), new Checkout(), new Checkout()]));

Starte sie im Ordner supermarkt mit node simulation.js.

Lösch vorher main in supermarket.dart; die Simulation bekommt ihr eigenes. import lädt die Klassen aus supermarket.dart. Die Simulation, in simulation.dart:

import 'dart:io';

import 'supermarket.dart';

double simulate(List<Checkout> checkouts) {
  final supermarket = Supermarket(checkouts);
  for (final line in File('customers.csv').readAsLinesSync().skip(1)) {
    final parts = line.split(',').map(int.parse).toList();
    supermarket.arrive(Customer(parts[1], parts[0]));
  }
  return supermarket.averageWait();
}

void main() {
  print(simulate([Checkout(), Checkout(), Checkout()]));
}

Starte sie im Ordner supermarkt mit dart simulation.dart.

Die Simulation kommt in Simulation.java; Main.java zum Ausprobieren kannst du löschen.

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

public class Simulation {
    static double simulate(List<Checkout> checkouts) throws Exception {
        Supermarket supermarket = new Supermarket(checkouts);
        List<String> lines = Files.readAllLines(Path.of("customers.csv"));
        for (String line : lines.subList(1, lines.size())) {
            String[] parts = line.split(",");
            supermarket.arrive(new Customer(Integer.parseInt(parts[1]), Integer.parseInt(parts[0])));
        }
        return supermarket.averageWait();
    }

    public static void main(String[] args) throws Exception {
        System.out.println(simulate(List.of(new Checkout(), new Checkout(), new Checkout())));
    }
}

Starte sie im Ordner supermarkt mit java Simulation.java.

Aufgabe 11Drei Kassen

Wie viele Sekunden warten die Leute an diesem Samstag mit drei normalen Kassen im Mittel?

s
Vermutung 12Lohnt sich die Expresskasse?

Eine der drei Kassen wird Expresskasse, für höchstens zehn Artikel. Was passiert mit der mittleren Wartezeit aller?

Lohnt sich die Expresskasse?

Ändere die Liste der Kassen in der letzten Zeile: zwei normale Kassen und eine Expresskasse.

Aufgabe 13Zwei Kassen und eine Expresskasse

Wie viele Sekunden warten die Leute mit zwei normalen Kassen und einer Expresskasse im Mittel?

s

Hier geht es weiter, sobald du deine Antwort oben abgegeben hast.

Die Expresskasse lohnt sich nicht: Im Mittel warten die Leute 118 statt 70 Sekunden. Wer höchstens zehn Artikel hat, wartet zwar kürzer, 40 statt 58 Sekunden. Wer mehr hat, wartet aber 197 statt 82 Sekunden, denn für die vollen Wagen bleiben nur zwei Kassen, während die Expresskasse immer wieder leer steht, wenn gerade niemand mit wenigen Artikeln kommt. Drei Kassen für alle bedienen mehr als zwei für alle und eine nur für manche.

Und die Selbstbedienung? Probier die dritte Aufteilung: zwei normale Kassen und zwei Selbstbedienungskassen.

Aufgabe 14Zwei Kassen und zwei Selbstbedienungskassen

Wie viele Sekunden warten die Leute mit zwei normalen Kassen und zwei Selbstbedienungskassen im Mittel?

s

Hier geht es weiter, sobald du deine Antwort oben abgegeben hast.

Mit zwei Selbstbedienungskassen warten die Leute im Mittel nur 12 Sekunden, obwohl jede bei vollen Wagen langsamer ist als eine Kasse mit Kassiererin: Es sind vier Kassen statt drei, und bei wenigen Artikeln ist die Selbstbedienungskasse sogar schneller, weil das Bezahlen kürzer dauert.

Was die Simulation nicht weiß

Die Zahlen sind so gut wie die Annahmen dahinter. In der Simulation sieht jede Kundin, welche Kasse am frühesten frei ist; echte Leute sehen nur die Schlange und schätzen. Niemand wechselt die Schlange, niemand braucht an der Selbstbedienungskasse Hilfe, und die Kundinnen und Kunden sind ausgedacht. Ein Modell lässt immer etwas weg; wichtig ist, dass man weiß, was, und ob es die Antwort ändern könnte.

8. Wie geht es weiter?

  • Gib die mittlere Wartezeit getrennt aus, für Wagen mit höchstens zehn Artikeln und für die anderen. Der Supermarkt braucht dafür zwei Zähler mehr.
  • Was ändert sich, wenn die Expresskasse nur bis fünf Artikel nimmt, oder bis fünfzehn?
  • Die längste Wartezeit sagt oft mehr als die mittlere: Wer fünf Minuten steht, kommt vielleicht nicht wieder.
  • Eine echte Schlange: Jede Kasse hält ihre Kundinnen in einer Queue, und die Kundinnen stellen sich dort an, wo die wenigsten stehen. Kommt dasselbe heraus?
  • Jede neue Idee beginnt wieder im Diagramm: Welche Klasse ändert sich, welche kommt dazu?