OOP vertieft - Theorie Lektion 7

Erstellt von Frithjof Mon, 24 Dec 2007 12:40:00 GMT

In Lektion 16 hast du einen ersten Eindruck von objektorientierter Programmierung (abgekürzt OOP) bekommen. Die prozedurale Programmierung im Spiel Tic-Tac-Toe führte dazu, dass wir eine Methode nach der anderen implementierten, die aber alle irgendwie zusammengehören. In Lektion 17, der Abschlußlektion des Spiels, wollen wir gemeinsam den bisherigen Rubycode für Tic-Tac-Toe in einen objektorientierten Code umschreiben.

Dazwischen füge ich diese Theorielektion ein, damit wir etwas Zeit haben uns mit dem Neuen vertraut zu machen.

Du programmierst ein Wohnhaus auf objektorientierte Art und Weise. Formulieren wir zunächst etwas vereinfacht, was ein Wohnhaus überhaupt ist.

Ein Wohnhaus ist ein Gebäude mit einer Haustür und mehreren Fenstern, einem Dach mit Schornstein. Innen hat das Wohnhaus mehrere Zimmer und Möbel, einen Kachelofen. Die Tür und die Fenster kann man öffnen und schließen, der Schornstein raucht, solange der Kachelofen angeschürt ist.

Diesen beschreibenden Text eines Wohnhauses gilt es nun in ein objektorientiertes Programm umzuwandeln. Du musst irgendwie herausfinden, welche Objekte hier im Spiel sind, damit du die entsprechenden Klassen implementieren kannst und welche Methoden diese Objekte haben sollen. Dafür gibt es eine klassische Vorgehensweise:

  1. Markiere alle Substantive im Text, das sind die Klassen!
    Ein Wohnhaus ist ein Gebäude mit einer Haustür und mehreren Fenstern, einem Dach mit Schornstein. Innen hat das Wohnhaus mehrere Zimmer und Möbel, einen Kachelofen.

    Die Tür und die Fenster kann man öffnen und schließen, der Schornstein raucht, solange der Kachelofen angeschürt ist.

  2. Markiere alle Verben im Text, das sind die Methoden!
    Ein Wohnhaus ist ein Gebäude mit einer Haustür und mehreren Fenstern, einem Dach mit Schornstein. Innen hat das Wohnhaus mehrere Zimmer und Möbel, einen Kachelofen.

    Die Tür und die Fenster kann man öffnen und schließen, der Schornstein raucht, solange der Kachelofen angeschürt ist.


  3. Das Programm funktioniert dann so als würden die Substantive die Verben anderer Substantive aufrufen.

Wir werden nicht alle Klassen implementieren, sondern nur folgende:

  • Tür
  • Fenster
  • Kachelofen
  • Wohnhaus

Fangen wir mit der Klasse Tür an.


class Tuer
  def initialize(farbe, material)
    @farbe = farbe
    @material = material
    @zustand = :geschlossen
  end
end

Im Konstruktor initialize werden drei Attribute der Klasse (das sind die Variablen mit dem @-Zeichen) angelegt: farbe, material und zustand. Wozu das Attribut farbe dient ist ziemlich klar. Im Attribut material wird gespeichert, aus welchem Material die Tür besteht (Holz, Metall, ...) und im Attribut zustand, ob sie offen oder geschlossen ist.

Der Konstruktor bekommt von außen die gewünschte Farbe und das Material übergeben. Der Zustand wird nicht übergeben, sondern immer auf :geschlossen gesetzt. Statt :geschlossen könnten wir auch "geschlossen" verwenden. Wir müssen uns nur für eine Schreibweise entscheiden und diese dann später beim Ändern des Zustandes beibehalten.

Die Klasse Tuer funktioniert schon, wie das folgende Beispiel zeigt. Es werden zwei Objekte der Klasse Tuer angelegt.

  tuer1 = Tuer.new("schwarz", :holz)
  tuer2 = Tuer.new("blau", :metall)

  puts tuer1
  puts tuer2
Lassen wir das Programm laufen, liefert es folgendes:

C:\entwicklung>ruby theorie_07.rb
#<Tuer:0x2c7f9b4>
#<Tuer:0x2c7f98c>

Naja, sieht ja nicht gerade informativ aus. Schöner wäre doch, wenn der puts Befehl auch die Inhalte der Attribute ausgeben würde. Das geht so einfach nicht, weil der puts Befehl nichts von den Attributen einer Tür weiß. Es gibt aber eine Möglichkeit: der puts Befehl versucht immer eine Methode to_s (englisch abgekürzt für to string) am Objekt aufzurufen. Wird sie gefunden, dann wird der Rückgabewert dieser Methode ausgegeben, andernfalls macht puts was es will. Eine schöne to_s Methode wäre vielleicht die folgende:


class Tuer
  def initialize(farbe, material)
    @farbe = farbe
    @material = material
    @zustand = :geschlossen
  end

  def to_s
    "Tuer[zustand=#{@zustand}, farbe=#{@farbe}, material=#{@material}]" 
  end
end
Damit sieht die Ausgabe gleich viel besser aus:

C:\entwicklung>ruby theorie_07.rb
Tuer[zustand=geschlossen, farbe=schwarz, material=holz]
Tuer[zustand=geschlossen, farbe=blau, material=metall]

Ganz zufrieden sind wir aber immer noch nicht. Oft werden wir auf ein einzelnes Attribut zugreifen wollen. Innerhalb der Klasse können wir das ja immer über den Variablennamen mit dem @-Zeichen machen. Aber von außen geht das nicht (Stichwort Kapselung, du erinnerst dich?).


  tuer1 = Tuer.new("schwarz", :holz)
  puts tuer1.farbe

Versuchen wir es, erhalten wir einen NoMethodError Fehler.


C:\entwicklung>ruby theorie_07.rb
theorie_07.rb: undefined method `farbe' for #<Tuer:0x2c7f838> (NoMethodError)

Es braucht also noch eine Methode, die uns den Inhalt des Attributes nach außen liefert:


class Tuer
  def initialize(farbe, material)
    @farbe = farbe
    @material = material
    @zustand = :geschlossen
  end

  def farbe
    @farbe
  end

  def to_s
    "Tuer[zustand=#{@zustand}, farbe=#{@farbe}, material=#{@material}]" 
  end
end

Dann funktioniert der Aufruf von puts tuer1.farbe, aber irgendwie unschön ist das doch, für jedes Attribut so eine Methode anbieten zu müssen. Ruby hält dafür eine Abkürzung bereit:


class Tuer
  attr_reader :farbe, :material, :zustand
  attr_writer                    :zustand

  def initialize(farbe, material)
    @farbe = farbe
    @material = material
    @zustand = :geschlossen
  end

  def to_s
    "Tuer[zustand=#{@zustand}, farbe=#{@farbe}, material=#{@material}]" 
  end
end

Mit attr_reader legen wir fest, welche Attribute von außen gelesen werden dürfen. Mit attr_writer legen wir fest, welche Attribute von außen beschrieben werden dürfen. Oben haben wir festgelegt, dass nur der Zustand gelesen und beschrieben, also geändert werden darf. Die Attribute farbe und material sind unveränderbar. Sie behalten den Wert, den sie beim Konstruktoraufruf (Methode initialize) erhalten haben. Das sind readonly (engl. nur lesen) Attribute.

Wir geben uns mit der Klasse zunächst einmal zufrieden und machen mit der Klasse Fenster weiter. Die ist ziemlich ähnlich aufgebaut:


class Fenster
  attr_reader :farbe, :material, :zustand
  attr_writer                    :zustand

  def initialize(farbe, material)
    @farbe = farbe
    @material = material
    @zustand = :geschlossen
  end

  def to_s
    "Fenster[zustand=#{@zustand}, farbe=#{@farbe}, material=#{@material}]" 
  end
end

Womit wir auch schon bei der Klasse Kachelofen angelangt wären, die sich auch nur wenig von den bisherigen unterscheidet:


class Kachelofen
  attr_reader :farbe, :zustand
  attr_writer         :zustand

  def initialize(farbe = "gelb")
    @farbe = farbe
    @zustand = :aus
  end

  def to_s
    "Kachelofen[zustand=#{@zustand}, farbe=#{@farbe}]" 
  end
end

Beim Kachelofen ist aber etwas neu. Der Konstruktor bekommt genau einen Parameter übergeben: farbe. Der Parameterwert wird aber dabei auf den Wert “gelb” gesetzt. Das ist ein sogenannter Default, also ein Standardwert. Falls beim Anlegen des Objektes keine Farbe für den Kachelofen festgelegt wird, dann ist er immer gelb.


  k1 = Kachelofen.new
  puts k1

  k2 = Kachelofen.new("braun")
  puts k2

C:\entwicklung>ruby theorie_07.rb
Kachelofen[zustand=aus, farbe=gelb]
Kachelofen[zustand=aus, farbe=braun]

Kommen wir zur letzten Klasse Wohnhaus. Jetzt wird es erst wirklich interessant. Ein Objekt der Klasse Wohnhaus verwaltet all die anderen Objekte Tuer, Fenster, Kachelofen.


class Wohnhaus
  attr_accessor :farbe, :farbe_dach, :kachelofen, :tuer, :fenster

  def initialize(farbe, farbe_dach)
    @farbe      = farbe
    @farbe_dach = farbe_dach
    @kachelofen = Kachelofen.new
    @tuer       = Tuer.new("schwarz", :holz)
    f = Fenster.new("braun", :holz)
    @fenster    = [f, f, f]
  end

  def to_s
    res = "Wohnhaus[\n" 
    res << "  farbe       = " + @farbe + "\n" 
    res << "  dach        = " + @farbe_dach + "\n" 
    res << "  kachelofen  = " + @kachelofen.to_s + "\n" 
    res << "  tuer        = " + @tuer.to_s + "\n" 
    res << "  schornstein = " + @schornstein.to_s + "\n" 
    res << "  fenster     = " + "\n" 
    for f in @fenster
      res << "    " + f.to_s + "\n" 
    end
    res << "\n" 
    res << "]" 

    res
  end
end

Ein Objekt der Klasse Wohnhaus hat zu Beginn (d.h. so wie es vom Konstruktor initialize ausgeliefert wird) eine bestimmte @farbe, das Dach hat eine vielleicht andere @farbe_dach. Es besitzt einen @kachelofen, genau eine @tuer und drei @fenster.

Die to_s Methode ist etwas umfangreicher, weil wir hier viele Details des Hauses ausgeben möchten. Wir nutzen dabei die to_s Methoden der jeweiligen Objekte.

Auch hier ist wieder eine Kleinigkeit neu. Den Zugriff auf die Attribute der Klasse Wohnhaus legen wir diesmal in der Zeile


  ...
  attr_accessor :farbe, :farbe_dach, :kachelofen, :tuer, :fenster
  ...

fest. attr_accessor bedeutet, dass wir alle so festgelegten Attribute sowohl lesen als auch beschreiben können. Alternativ hätten wir auch ausführlich es so festlegen können:


  ...
  attr_reader :farbe, :farbe_dach, :kachelofen, :tuer, :fenster
  attr_writer :farbe, :farbe_dach, :kachelofen, :tuer, :fenster
  ...

Das Haus verändern

In der Beschreibung eines Wohnhauses haben wir gesagt, dass man die Tür und die Fenster öffnen und schließen kann und der Schornstein raucht, solange der Kachelofen angeschürt ist.

Fenster und Türen öffnen und schließen

Fenster und Türen haben einen Zustand, den wir im Konstruktor immer mit :geschlossen festgelegt haben. Wollen wir eine Tür oder eine Fenster öffnen, muss dieser Zustand den Wert :offen erhalten. Wir brauchen dafür eine Methode, die wir von außen aufrufen, und die dann den Zustand der Tür oder der Fenster ändert.

Das Öffnen und Schließen der Tür übernehmen die Methoden tuer_auf und tuer_zu.


class Wohnhaus
  attr_accessor :farbe, :farbe_dach, :kachelofen, :tuer, :fenster
  ...
  def tuer_auf
    @tuer.zustand = :offen
  end

  def tuer_zu
    @tuer.zustand = :geschlossen
  end
  ...
end

Bei den Fenster ist etwas mehr zu tun, weil es nicht nur ein Fenster gibt.


class Wohnhaus
  attr_accessor :farbe, :farbe_dach, :kachelofen, :tuer, :fenster
  ...
  def fenster_auf
    for f in @fenster
      f.zustand = :offen
    end
  end

  def fenster_zu
    for f in @fenster
      f.zustand = :geschlossen
    end
  end
  ...
end

Den Kachelofen anschüren

Auch der Kachelofen hat eine Zustand, der anzeigt, ob er aus oder angeschürt ist. Das Ändern dieses Zustandes übernehmen die beiden Methoden ofen_an und ofen_aus.


class Wohnhaus
  attr_accessor :farbe, :farbe_dach, :kachelofen, :tuer, :fenster
  ...
  def ofen_an
    @kachelofen.zustand = :an
    @schornstein        = :raucht
  end

  def ofen_aus
    @kachelofen.zustand = :aus
    @schornstein        = :aus
  end
  ...
end

Die Methoden ofen_an bzw. ofen_aus überwachen also sowohl den Zustand des Kachelofens als auch des Schornsteins. So können wir sicher stellen, dass beide Zustände immer gleichzeitig in übereinstimmender Weise geändert werden. Es kann so nicht passieren, dass der Ofen aus geht, aber der Schornstein noch weiter raucht. Oder doch?

Hier nochmal alle Klassen zusammen. Ganz unten ein paar Zeilen Code, die ein Wohnhausobjekt anlegen und es verändern.


class Tuer
  attr_reader :farbe, :material, :zustand
  attr_writer                    :zustand

  def initialize(farbe, material)
    @farbe = farbe
    @material = material
    @zustand = :geschlossen
  end

  def to_s
    "Tuer[zustand=#{@zustand}, farbe=#{@farbe}, material=#{@material}]" 
  end
end

class Fenster
  attr_reader :farbe, :material, :zustand
  attr_writer                    :zustand

  def initialize(farbe, material)
    @farbe = farbe
    @material = material
    @zustand = :geschlossen
  end

  def to_s
    "Fenster[zustand=#{@zustand}, farbe=#{@farbe}, material=#{@material}]" 
  end
end

class Kachelofen
  attr_reader :farbe, :zustand
  attr_writer         :zustand

  def initialize(farbe = "gelb")
    @farbe = farbe
    @zustand = :aus
  end

  def to_s
    "Kachelofen[zustand=#{@zustand}, farbe=#{@farbe}]" 
  end
end

class Wohnhaus
  attr_accessor :farbe, :farbe_dach, :kachelofen, :tuer, :fenster

  def initialize(farbe, farbe_dach)
    @farbe      = farbe
    @farbe_dach = farbe_dach
    @kachelofen = Kachelofen.new
    @schornstein = :aus
    @tuer       = Tuer.new("schwarz", :holz)
    f = Fenster.new("braun", :holz)
    @fenster    = [f, f, f]
  end

  def tuer_auf
    @tuer.zustand = :offen
  end

  def tuer_zu
    @tuer.zustand = :geschlossen
  end

  def fenster_auf
    for f in @fenster
      f.zustand = :offen
    end
  end

  def fenster_zu
    for f in @fenster
      f.zustand = :geschlossen
    end
  end

  def ofen_an
    @kachelofen.zustand = :an
    @schornstein        = :raucht
  end

  def ofen_aus
    @kachelofen.zustand = :aus
    @schornstein        = :aus
  end

  def to_s
    res = "Wohnhaus[\n" 
    res << "  farbe       = " + @farbe + "\n" 
    res << "  dach        = " + @farbe_dach + "\n" 
    res << "  kachelofen  = " + @kachelofen.to_s + "\n" 
    res << "  tuer        = " + @tuer.to_s + "\n" 
    res << "  schornstein = " + @schornstein.to_s + "\n" 
    res << "  fenster     = " + "\n" 
    for f in @fenster
      res << "    " + f.to_s + "\n" 
    end
    res << "\n" 
    res << "]" 

    res
  end
end

w = Wohnhaus.new("gelb", "rot")
w.tuer = Tuer.new("blau", :holz)
w.kachelofen = Kachelofen.new("braun")
# Ein weiteres Fenster einbauen
w.fenster << Fenster.new("weiss", :kunststoff)
puts w

w.tuer_auf
w.fenster_auf
puts w

w.ofen_an
w.tuer_zu
w.fenster_zu
puts w

Verletzliche Kapselung

Zurück zu obiger Frage: Kann es passieren, dass der Ofen aus geht, der Schornstein aber weiter raucht?


w = Wohnhaus.new("gelb", "rot")
w.ofen_an
w.kachelofen.zustand = :aus

puts w

Liefert folgende Ausgabe


C:\entwicklung>ruby theorie_07.rb
Wohnhaus[
  farbe       = gelb
  dach        = rot
  kachelofen  = Kachelofen[zustand=aus, farbe=gelb]
  tuer        = Tuer[zustand=geschlossen, farbe=schwarz, material=holz]
  schornstein = raucht
  fenster     =
    Fenster[zustand=geschlossen, farbe=braun, material=holz]
    Fenster[zustand=geschlossen, farbe=braun, material=holz]
    Fenster[zustand=geschlossen, farbe=braun, material=holz]

]

Der Ofen ist also aus, der Schornstein raucht aber noch. Wie konnte das passieren? Die Klasse Wohnhaus ist nicht sicher genug gekapselt. Sie lässt es zu, dass man direkt mit w.kachelofen auf den Kachelofen zugreifen kann. Dann kann man dort den Zustand ändern und das Wohnhaus bekommt von dieser Änderung nichts mit.

Wie können wir das Problem beheben und die Kapselung hier wieder sicher machen? Mindestens zwei Möglichkeiten bieten sich an.

  1. Wir lassen den direkten Zugriff auf das Attribut kachelofen nicht mehr zu:
    
    class Wohnhaus
      attr_accessor :farbe, :farbe_dach, :tuer, :fenster
      ...
    end
    
    Dann müssen wir dem Erschaffer des Hauses aber einen weiteren Parameter im Konstruktor anbieten, der es erlaubt, die Farbe des Ofens festzulegen.
  2. Wir belassen den Zugriff auf das Attribut kachelofen wie bisher, überschreiben jedoch die Lesemethode mit einer ausführlichen Methode. In dieser Methode übergeben wir nicht das Attribute @kachelofen nach außen zurück, sondern stets eine Kopie davon.
    
    class Wohnhaus
      attr_accessor :farbe, :farbe_dach, :kachelofen, :tuer, :fenster
      ...
      # Attribut kachelofen auslesen, dabei gegen Änderung schützen, indem
      # nur eine Kopie nach außen gegeben wird.
      def kachelofen
        Kachelofen.new(@kachelofen.farbe)
      end
      ...
    end
    
    Mit der Kopie kann der Aufrufen dann machen, was er will, das Attribut @kachelofen innerhalb der Klasse wird sich dadurch nicht ändern.

Üben mit Peter und Livia

Livia: Es ist aber trotzdem immer noch möglich, den Kachelofen aus zu machen und den Schornstein weiter rauchen zu lassen. Wie muss die Klasse Wohnhaus weiter angepasst werden, damit auch dieser Fehler behoben ist?

Peter: Füge für Schornstein noch eine eigene Klasse mit den notwendigen Attributen hinzu.

Hinein und wieder heraus - Theorie Lektion 4

Erstellt von Frithjof Sun, 22 Jul 2007 04:44:00 GMT

Die letzte Lektion lehrte dich, was in Ruby eine Methode ist—eine Menge von Codezeilen, denen du einen Namen verpasst. Du rufst die Methode auf, indem du den Namen der Methode verwendest.

Eine Methode ist aber eigentlich noch mehr. Sie ist wie eine Maschine. Das Aufrufen der Methode bei ihrem Namen ist, als wenn du eine Maschine einschaltest. Wenn die Maschine fertig ist, hört sie auf zu arbeiten—die Methode hat alle Codezeilen ausgeführt.

Und wo ist das mehr? Bevor du eine Maschine arbeiten lässt, kannst du normalerweise etwas in die Maschine hineingeben. Ist sie fertig mit ihrer Arbeit, dann liefert sie ein fertiges Produkt. Schau dir die Goldmaschine in der Abbildung an. Sie erhält als Eingabe Goldsteine (Felsbrocken, in denen Gold eingeschlossen ist) und liefert als Ausgabe Goldbarren. Wie sie das genau macht, wissen wir nicht. Irgendwie wird sie die Steine erhitzen, das Gold schmilzt und läuft heraus, wird aufgefangen in Formen gegossen… Egal, Hauptsache sie macht, wozu sie konstruiert wurde.

Oder stell dir eine Kaffeemaschine vor! Es ist Sonntagmorgen und du möchtest Mama, die noch friedlich schläft, mit einem Tässchen Kaffee überraschen. Du gibst folgendes in die Kaffeemaschine hinein:
  1. Ein wenig Wasser
  2. Einen Kaffeefilter
  3. Etwas Kaffeepulver

Du schaltest die Maschine ein. Sofern sie korrekt funktioniert, wird sie dir Kaffee zubereiten, der in eine Glaskanne herausläuft. Nun nur noch in eine schöne Sonntagstasse füllen, etwas Sahne dazu, vielleicht Zucker, Untertasse nicht vergessen (Mamas mögen sowas!), auf das Tablett neben das leckere Marmeladebrötchen und das frisch gekochte Ei (das hast du mit einer anderen Maschine erledigt) und ab zu Mama! So kriegst du sie immmer wach, egal wie früh es ist…

Eine Methode ist wie eine Maschine, hatte ich gesagt. Wie gibst du nun etwas in die Methode hinein? Und wie bekommst du etwas aus der Methode heraus? Schauen wir uns an, wie das mit einer Rubymethode aussieht:


# theorie_04.rb

def kaffee_maschine(wasser, filter, pulver)
  # Die Maschine beginnt mit dem Heizen des Wassers
  puts "=> #{wasser} Wasser werden heisser und heisser..." 
  puts "=> Pumpe das Wasser durch ein Roehrensystem..." 
  puts "=> Und lasse es auf #{pulver} Pulver in den #{filter} fallen.\n" 
  # Das heisse Wasser laeuft durch das Pulver im Filter in die Glaskanne
  # darunter, das geht von alleine, da braucht die Maschine nichts 
  # weiter zu machen 
  # ...
  # Maschine ist fertig:
  puts "=> Fauchen und Dampf ablassen als Zeichen, dass ich fertig bin..." 

  # Fertigen Kaffee zurueckliefern
  return "Bitte schön: Glaskanne mit #{wasser} leckerem Kaffee" 
end

wasser = "2 Tassen (okay Papa kriegt auch eine)" 
filter = "Kaffeefilter Groesse 4 (der aus dem Aldi)" 
pulver = "Mhhmm, riecht das lecker. 2 Löffel!" 

kaffee_fuer_mama = kaffee_maschine(wasser, filter, pulver)

puts "Kaffee ist fertig!" 
puts kaffee_fuer_mama

Schauen wir uns den Code genauer an!

Zunächst definierst du eine Methode mit dem Namen kaffee_maschine. Die Methode umfasst alle Codezeilen bis zum zugehörigen end. Die Methode selbst kocht uns natürlich keinen echten Kaffee, es ist ja nur ein Rubyprogramm. Daher gibt es nur aus, was es tun würde, wenn es eine echte Kaffeemaschine wäre.

Hier ist aber etwas neu. Hinter dem Namen der Methode steht ein Paar runde Klammern, und innerhalb der Klammern stehen drei Variablennamen wasser, filter und pulver. Merke also, wenn du möchtest, dass jemand etwas in deine Methode (Maschine) hineintun können soll, dann schreibst du hinter den Methodennamen in runde Klammern für jedes Ding einen Variablennamen.

Innerhalb der Methode kannst du dann die Variablen wie ganz gewöhnliche Variablen verwenden und irgendetwas damit machen.

Ist die Methode (Maschine) fertig, kann sie das Ergebnis (das fertige Produkt) zurückgeben. Das passiert in der letzten Zeile vor dem Ende der Methode mit dem return Befehlt. Nach dem return steht das, was du deine Methode zurückgeben lassen möchtest. Im Beispiel oben ist es einfach die Mitteilung, dass in der Glaskanne 2 Tassen Kaffee sind.

Noch einige Besonderheiten

Die Variablen, die du hinter dem Methodennamen aufzählst, sind wirklich nur innerhalb der Methode verwendbar. Außerhalb der Methode sind die Variablen nicht bekannt. Wir haben zwar unterhalb der Methode diese 3 Variablen noch einmal stehen, das sind aber komplett andere Variablen, die heißen bloß genauso! Wir könnten sie oder die Variablen hinter dem Methodennamen ganz anders nennen, und das Programm funktionierte noch immer. Bedenke nur, dass du die Variablen dann auch dort ändern musst, wo du sie verwendest.



def kaffee_maschine(wasser, filter, pulver)
  # Die Maschine beginnt mit dem Heizen des Wassers
  ...
  return "Bitte schön: Glaskanne mit #{wasser} leckerem Kaffee" 
end

wieviel_wasser = "2 Tassen (okay Papa kriegt auch eine)" 
welcher_filter = "Kaffeefilter Groesse 4 (der aus dem Aldi)" 
wieviel_pulver = "Mhhmm, riecht das lecker. 2 Löffel!" 

kaffee_fuer_mama = kaffee_maschine(wieviel_wasser, welcher_filter, wieviel_pulver)

puts "Kaffee ist fertig!" 
puts kaffee_fuer_mama

Bei der Rückgabe des Ergebnisses kannst du das Wörtchen return auch weglassen, wenn die Rückgabe in der letzten Zeile der Methode erfolgt. Das muss nicht zwingend immer die letzte Zeile vor dem end sein. Sondern es muss die letzte Zeile sein, die Ruby während der Abarbeitung deiner Methode ausführt, bevor Ruby die Methode verlässt. Hast du beispielsweise eine IF-Abfrage in der Methode, könnte es mit und ohne return so aussehen.


# Methode mit return

def mehr_kaffee(soviel_hatte_sie_schon)
  if soviel_hatte_sie_schon > 2
    return "gibt nix mehr!" 
  else
    return "Okay, noch eine Tasse!" 
  end
end


# Methode ohne return

def mehr_kaffee(soviel_hatte_sie_schon)
  if soviel_hatte_sie_schon > 2
    "gibt nix mehr!" 
  else
    "Okay, noch eine Tasse!" 
  end
end

Die Zeile, die im letzten Codebeispiel als letztes ausgeführt wird, hängt ab vom Wert, der in der Variablen soviel_hatte_sie_schon übergeben wurde. Ist der Wert größer 2, gibt die Methode “gibt nix mehr!” zurück, auch wenn diese Codezeile nicht die letzte in der Methode ist. Sie ist aber die letzte, die unter diesen Bedingungen von Ruby ausgeführt wird. Denn in den else-Zweig gelangt Ruby nur, wenn der Wert der übergebenen Variablen kleiner oder gleich 2 ist.

Üben mit Peter und Livia

Livia: Schreibe eine Methode mit dem Namen maximum, die zwei Zahlen entgegen nimmt, dann entscheidet, welche Zahl die größere ist und diese dann als Ergebnis zurückliefert.

Peter: Schreibe eine Methode mit dem Namen verketten, die zwei Zeichenketten entgegen nimmt, sie beide aneinander hängt und die so neu entstandene Zeichenkette zurück gibt.

Programme aufteilen - Theorie Lektion 3

Erstellt von Frithjof Sat, 14 Jul 2007 21:28:00 GMT

Du bist noch immer unterwegs zu Lektion 11, wo du dann beginnen wirst, ein Spiel zu programmieren.

Aus Lektion 7 weißt du bereits, dass lange Programme zunehmend schwieriger zu lesen sind. Du reihst eine Zeile an die nächste. Was machst du, wenn du im Verlauf des Programms an eine Stelle kommst, wo du genau denselben Code brauchen würdest, den du weiter oben schon einmal verwendet hast? Nochmal hinschreiben? Naja, das stinkt ja bereits schon beim nur dran denken. Du erinnerst dich, was stinkender Code ist?

Du lernst in dieser Theorielektion eine Möglichkeit kennen, die du sehr häufig brauchen wirst wenn es darum geht, gleichen Code wiederzuverwenden, ohne ihn erneut hinschreiben zu müssen.

Die Idee ist ganz einfach. Du schreibst deine Zeilen Code, die du häufiger verwenden möchtest, in eine Datei und gibst ihnen dabei einen Namen. Ja richtig gehört, du denkst dir einen Namen für die Programmzeilen aus. Immer dann wenn du anschließend die Zeilen Code verwenden möchtest, verwendest du stattdessen nur den Namen. Du rufst den Code bei seinem Namen!

Sagen wir, du willst in deinem Programm an verschiedenen Stellen die Aufforderung zur Eingabe einer Zahl verwenden


puts "Bitte gib eine Zahl größer 0 ein:" 

Du denkst dir als Namen frag_sie aus. Das Benennen des Codes sieht dann so aus:


def frag_sie
  puts "Bitte gib eine Zahl größer 0 ein:" 
end

Das Schlüsselwort def leitet die Benennung des Codes ein, gefolgt von dem Namen den du dir überlegt hast. Am Ende der Codezeilen wird die Benennung mit end abgeschlossen.

Nun kannst du den benannten Codeabschnitt verwenden, indem du ihn bei seinem Namen aufrufst. Dabei musst du nur darauf achten, dass der Aufruf natürlich nach der Benennung steht.


# theorie_03.rb

# Zuerst den Code mit einem Namen versehen
def frag_sie
  puts "Bitte gib eine Zahl größer 0 ein:" 
end

# Dann den benannten Code verwenden
frag_sie
a = gets

# Noch eine Zahl verlangen
frag_sie
b = gets

# und noch eine, weils so einfach ist
frag_sie
c = gets

Das Schlüsselwort def leitet sich von Definieren ab. Das Gebilde vom def über den Namen, den Code bis zum end hin nennt man auch ein Unterprogramm, weil es innerhalb eines anderen Programmes existiert. Andere Bezeichnungen dafür sind noch Subroutine, Funktion oder Methode.

Lass uns von nun an den Begriff Methode (d.h. eine Handlungsanweisung oder Art und Weise) dafür verwenden. Denn der benannte Code stellt eine Folge von Anweisungen dar, die bei Aufruf so ausgeführt werden.

Du hast also soeben deine erste Methode definiert, die Methode heißt frag_sie.

-
Update 15.07.2007

Üben mit Peter und Livia

Gestern Abend war es schon spät, Peter und Livia bereits in tiefen Schlaf gefallen. Heute morgen waren sie dann ganz enttäuscht, dass sie keine Übung zum Theorieartikel beisteuern konnten. Daher hier noch eine kleine Übung zu Methoden. Wie üblich erst selbst versuchen, wenn du nicht weiterkommst, Mama fragen, als letztes kannst du dann in der Lösung nachsehen.

Peter und Livia: Schreibe die Methode frag_sie so um, dass sie nach dem Namen der Anwenderin fragt. Schreibe zusätzlich noch eine Methode mit Namen sie_sagte, die ausgibt, was die Anwenderin eingegeben hatte. Verwende danach beide Methoden.

Lösungen

def frag_sie
  puts "Wie heisst du? " 
end

def sie_sagte
  a = gets
  puts "Aha, du heisst also #{a}" 
end

frag_sie
sie_sagte

Lass uns reden! 1

Erstellt von Frithjof Fri, 29 Jun 2007 22:19:00 GMT

Na schön, das mit dem Ausgeben klappt ja nun schon recht gut. Irgendwie kann es aber nicht alles sein, wenn das Rubyprogramm nur gestartet wird, ein wenig was arbeitet und dann irgendetwas ausgibt. Das ist, als wenn dir jemand gegenüber steht, der nur plaudert und dich nicht zu Wort kommen lässt. Dabei hast du auch etwas zu sagen, oder?

Ganz so schlimm ist es aber doch nicht. Ein wenig hattest du schon zu sagen – das Fahrrad konntest du mit der L- und R-Taste steuern und mit der X-Taste vom Rad absteigen.

In diesem Artikel zeige ich dir, wie du dein Rubyprogramm dazu bekommst, sich mit dem, der es ausführt zu unterhalten.

Eingaben von Anwenderinnen abfragen

Schauen wir uns gleich folgendes Programm an:


# theorie_02.rb

print "Gib eine Zahl a zwischen 1 und 100 ein! a=" 
a = gets
a = a.chomp
a = a.to_i

zahl_teilbar_durch_3 = false
if a % 3 == 0 
  zahl_teilbar_durch_3 = true
end

puts "Du hast a=#{a} eingegeben." 
if zahl_teilbar_durch_3
  puts "#{a} ist durch 3 teilbar." 
else
  puts "#{a} ist nicht durch 3 teilbar." 
end

Wir fordern die Anwenderin zunächst mit der Ausgabe Gib eine Zahl a zwischen 1 und 100 ein! dazu auf, na was wohl, natürlich eine Zahl einzugeben, die größer oder gleich 1 aber kleiner oder gleich 100 ist. Damit sie weiß, wo beim Tippen die Zahl erwartet wird, schreiben wir noch das a= hin. Direkt nach dem = wird dann an der Kommandozeile das erscheinen, was sie eintippt. Ruby wartet nun darauf, solange, bis sie die Enter-Taste drückt.

Dann geht unser Programm zur nächsten Zeile und liest das was eingegeben wurde von der Kommandozeile in die Variable a ein. In a steht nun eine Zeichenkette mit dem Sonderzeichen für die Entertaste \n am Schluß.

Dieses Sonderzeichen schneiden wir mit dem Befehl chomp (englisch: mampfen) ab. Den Befehl oder besser die Nachricht chomp schicken wir direkt an die Variable a. Das kennst du auch schon aus den früheren Lektionen. Wir trennen dabei die Nachricht chomp vom Variablennamen mit dem Punkt . ab. Das was nach dem Abschneiden von \n noch übrig bleibt, weisen wir wieder der Variablen a zu.

In der nächsten Zeile schicken wir wieder eine Nachricht an a und zwar lautet diese to_i. Das ist die Abkürzung für englisch to integer, d.h. mache eine ganze Zahl daraus!. Den dadurch entstehenden Zahlenwert weisen wir wieder der Variablen a zu. Wir könnten auch jedesmal eine andere Variable verwenden. Da wir aber eigentlich nur am Zahlenwert interessiert sind, können wir den Wert von a ohne Sorge überschreiben.

So, jetzt steht in a keine Zeichenkette (in Ruby String genannt) mehr, sondern eine Zahl (in Ruby Fixnum oder Integer genannt).

Mit der Zahl a führen wir nun den Test durch, ob sie durch 3 teilbar ist. Dazu berechnen wir den Rest bei der Division durch 3. Den Rest erhalten wir mit dem Operator %. Nur wenn der Rest beim Teilen durch 3 Null ist, ist die Zahl (restlos) durch 3 teilbar. Ob sie das ist, merken wir uns in der Variablen zahl_teilbar_durch_3.

Okay, nun können wir der Anwenderin Bescheid geben, was wir herausgefunden haben. Wir zeigen ihr zuerst nochmal das, was sie eingegeben hat. Halt, das stimmt eigentlich nicht. Sie hat ja genau genommen noch die Entertaste eingegeben. Aber das will sie sicher nicht wissen.

Schließlich verraten wir ihr noch, ob die Zahl durch 3 teilbar ist oder nicht.

Und wenn sie nicht auf mich hört?

Versuche das Programm zu starten und gib mal eine Zahl ein, die größer als 100 ist. Es geht! Warum auch nicht. Wir prüfen nirgendwo, ob die eingegebene Zahl auch tatsächlich einen Wert hat, den wir gefordert haben. Das überlasse ich dir als Übung.

Die Büchse der Pandora

Ein Programm, das man startet, es dann irgendetwas tut und sich am Ende mit irgendeiner Ausgabe verabschiedet ist einfach. Wobei einfach hier nicht im Sinne zu verstehen ist, dass solche Programme nicht auch kompliziert oder auch schwierig zu entwickeln sein könnten. Nein, mit einfach meine ich, dass der Ablauf des Programmes recht übersichtlich ist. Man gibt etwas ein, wartet und schaut sich dann das Ergebnis an. Fertig.

Sobald du aber ein Programm während seiner Laufzeit (englisch: runtime), also nachdem du es gestartet hast und es noch nicht fertig ist, für die Anwenderinnen öffnest, verliert es seine einfache Ablaufstruktur.

Kommunizierst du mit einem realen Menschen direkt von Angesicht zu Angesicht, kannst du auf ihr Reden sofort reagieren. Du siehst ihre Mimik und Gestik und hörst auch auf den Ton, wie sie etwas sagt. Daraus kannst du neben den Worten, die von ihr an dein Ohr dringen, sogar noch eine zusätzliche Bedeutung entnehmen.

Die Kommunikation mit einem realen Menschen, allerdings nicht direkt, sondern nun über den Umweg eines Computerprogramms ist da anders. Der Austausch von Information geschieht hier zeitlich versetzt. Du musst dir während du das Programm entwickelst, Gedanken darüber machen, wie du die Fragen stellst, welche Antworten du erwartest und wie du wiederum auf diese Antworten reagieren möchtest.

Angenommen, du schreibst ein Programm, dass zwei Zahlen a und b einliest und als Ausgabe den Quotienten aus a/b ausgeben soll. Du forderst die spätere Anwenderin deines Programms etwa so auf:


"Gib die erste Zahl größer als 1 ein! a=" 
...
"Gib die zweite Zahl größer als 1 ein! b=" 

Nun hast du in deinem Programm zwei Variablen a und b. Du weißt aber während du das Programm entwickelst nicht, welchen Wert genau a und b haben werden. Du wirst sicher nicht dabei sein, wenn sie irgendwo auf der Welt vor ihrem Computer sitzt und dein Programm ausführt, du siehst nicht, was sie eingibt. Es wäre leichtsinnig sich darauf zu verlassen, dass sie wirklich das macht, wozu du sie aufgefordert hast, nämlich zwei Zahlen einzugeben, die beide je größer als 1 sind. Was, wenn sie für b eine Null eintippt? Die Division durch Null ist in der Mathematik nicht erlaubt. Ruby wird sich mit einer Fehlermeldung beklagen. Je mehr Fehlermeldungen dein Programm produziert, ums so mehr wird das Vertrauen in deine Software sinken. Sie wird nie sagen Oh, ich habe eine Null eingegeben, das war mein Fehler. Stattdessen wird sie sagen So ein dummes Programm, es lässt einfach zu, dass ich eine Null eingeben kann. Dabei weiß doch jeder, dass man durch Null nicht dividieren kann. Und sie wird sich ein anderes Divisionsprogramm suchen.

Sobald dein Programm interaktiv genutzt werden soll—also nicht nur Ausgaben produziert, sondern auch Eingaben entgegen nimmt—bist du gezwungen, jede Eingabe zu prüfen. Liegt sie nicht in dem Bereich, mit dem du weiterarbeiten kannst, dann gibt dein Programm eine freundliche Hinweismeldung zurück.

Es gibt verschiedene Formen von fehlerhaften Eingaben:

  1. Unabsichtliche Fehleingaben Vielleicht waren deine Anweisungen nicht verständlich genug. Oder sie hat sie nicht richtig gelesen, oder hat einfach die falsche Taste erwischt.
  2. Bewusste Fehleingaben Sie legt es darauf an, dein Programm in einen Fehlerzustand zu versetzen. Entweder, um dir zu zeigen, dass du dein Programm nachlässig entwickelt hast, oder sogar, um dir absichtlich zu schaden. Insbesondere ist Software, die über das Internet genutzt wird böswilligen Angriffen ausgesetzt. Sie will über dein Programm hinaus eigentlich an die dahinterliegende Hardware herankommen, bpsw. an den Server und seine Festplatten, um dort an weitere Daten zu gelangen, die du in deinem Programm niemals freiwillig über das Internet zu ihr geschickt hättest.

Du wirst bald merken, dass du manchmal viel mehr Code dafür schreiben musst, um diese Fehleingaben sicher abfangen zu können, als für das, was du eigentlich mit deinem Programm anbieten möchtest.

Üben mit Peter und Livia

  1. Livia: Ich will aber keine Zahl zwischen 1 und 100 eingeben. Vielleicht will ich überhaupt keine Zahl eingeben. Ändere obiges Programm so ab, dass ich dann eine höfliche Fehlermeldung von dir bekomme!
  2. Peter: Entwickle ein Programm, das folgendes macht:
    • Es verlangt von der Anwenderin eine Zahl zwischen 1 und 5. Nennen wir sie n.
    • Es fragt dann die Anwenderin nacheinander n mal nach je einer weiteren Zahl im Bereich von 1 bis 1000.
    • Am Schluß teilt es der Anwenderin mit, ob die eingegebenen n Zahlen jede für sich eine Primzahl ist. Eine Primzahl ist eine Zahl, die nur durch 1 und sich selbst teilbar ist.

schreib und schreibe oder puts und print 2

Erstellt von Frithjof Sun, 24 Jun 2007 20:30:00 GMT

Das ist der erste reine Theorieartikel. Ich schiebe damit den Start des neuen Blocks an Lektionen noch etwas auf. Wir wollen ab Lektion 11 ein Spiel programmieren. Darauf möchte ich dich mit den folgenden Theorielektionen etwas besser vorbereiten.

In Lektion 7 hatten wir schon ein recht umfangreiches Programm geschrieben. Wir konnten ein Fahrrad auf der Straße vor einer Häuserreihe mit der Tastatur hin und her fahren lassen. Wenn du dir das Programm zur Wiederholung nochmals anschaust, stellst du sicher fest, dass es sich trotz der paar Wochen Abstand immer noch gut lesen lässt.

Das liegt zu einem großen Teil daran, dass wir viele deutsche Befehle und Variablennamen verwenden. Wenn dort der Befehl schreib dach steht, ist klar, was passiert – es wird etwas ausgegeben, was das Dach eines Hauses darstellen soll.

Die deutschen Namen für die Variablen sind sehr okay. Die deutschen Befehle wie schreibe, schreib, lies_ein_zeichen und dergleichen wollen wir aber schrittweise durch die richtigen Rubybefehle ersetzen. Keine Angst, dadurch werden unsere Programme nicht unleserlicher. Im Gegenteil, oft sind die deutschen Begriffe etwas länger und machen den Programmcode etwas zu dicht.

Wie du dir sicher denken kannst, sind die echten Befehle in Ruby hauptsächlich in englischer Sprache, entweder als komplettes Wort oder als Abkürzung. Schauen wir uns also als erstes heute einmal die Befehle für die Ausgabe genauer an.

schreibe oder puts

Mit dem schreibe Befehl können wir Text (Zeichenketten) oder Zahlen ausgeben. Am Ende der Ausgabe wird dabei immer die Zeile abgeschlossen, sodass jede weitere Ausgabe in der Zeile darunter beginnt. Etwa so:


schreibe "Dies ist der Text der Ausgabe" 

Den Befehl schreibe habe ich in der Datei rubykids.rb festgelegt. Der echte Rubybefehl, der dasselbe macht wie schreibe lautet puts. puts steht für die englischen Wörter put string oder put as string oder output as string, was soviel heißt wie gib es als Zeichenkette aus. Du kannst somit überall, wo wir den Befehl schreibe verwendet haben, ihn durch puts ersetzen.

Nach dem Befehl selbst folgt der Text oder die Zahl die ausgegeben werden soll. Folgt stattdessen der Name einer Variablen, dann wird nicht der Name, sondern der Inhalt der Variablen ausgegeben. Mehrere Ausgabeargumente lassen sich durch je ein Komma trennen. puts beginnt dann allerdings hinter jedem Argument einen neue Zeile (schreibe macht das ebenfalls).


puts "Erste Zeile", "Zweite Zeile", 5*3

Die Ausgabe sieht dann so aus:


C:\entwicklung>ruby test.rb
Erste Zeile
Zweite Zeile
15

C:\entwicklung>

schreib oder print

Der Befehl schreib arbeitet fast genauso wie schreibe, allerdings springt er nach der Ausgabe nicht in eine neue Zeile. So wird jede weitere Ausgabe direkt danach angefügt.

Der echte Rubybefehl, der dasselbe macht lautet print (englisch drucken). Auch ihm kannst du mehrere Argumente durch Komma getrennt mitgeben.

Nehmen wir bspw. den obigen Code und ersetzen nur puts durch print dann siehst du den Unterschied:


print "Erste Zeile", "Zweite Zeile", 5*3

Die Ausgabe ist eine einzige Zeile, alle Argumente sind aneinander geklatscht:


C:\entwicklung>ruby test.rb
Erste ZeileZweite Zeile15
C:\entwicklung>

Du kannst aber mit print natürlich auch eine neue Zeile erzeugen. Weißt du noch, wie das komische Sonderzeichen für den Zeilenumbruch war? Wir hatten es in den letzten Lektionen mal verwendet – \n. Wenn wir die beiden ersten Argumente mit dem Newline Zeichen abschließen, dann wird jedes Argument in einer eigenen Zeile ausgegeben, obwohl wir den print Befehl verwenden:


print "Erste Zeile\n", "Zweite Zeile\n", 5*3

Ausgabe:


C:\entwicklung>ruby test.rb
Erste Zeile
Zweite Zeile
15
C:\entwicklung>

Üben mit Peter und Livia

In den Theorieartikeln werden dir Peter und Livia ein paar Aufgaben stellen, die du selbstständig zur weiteren Übung lösen kannst. Wenn du dir eine Lösung erarbeitet hast, kannst du sie gerne als Kommentar hier an den Artikel anfügen - würde mich freuen. Nutze die Möglichkeit zum Kommentar auch, wenn du Fragen hast, oder mit einer Aufgabe nicht weiterkommst.

  1. Livia: Mit dem Body Mass Index kannst du feststellen, ob du mehr Sport treiben solltest oder ob das mit dem Nachschlag beim Essen okay ist. Der Index berechnet sich aus deinem Körpergewicht (in kg) und deiner Körpergröße (in m) nach der Formel: Gewicht / (Größe*Größe). Schreibe ein Rubyprogramm, das für Gewicht und Größe je eine Variable definiert und dann den BMI ausgibt.
  2. Peter: Schreibe ein Rubyprogramm, dass in einer Schleife alle Zahlen von 1 bis 20 ausgibt. Versuche das Programm so zu schreiben, dass die Ausgabe stets in derselben Zeile erfolgt. Die Ausgabe von 2 überschreibt also die vorherige Ausgabe von 1, die Ausgabe von 3 die von 2 usw. Versuche das Programm so zu verändern, dass es nur aus einer einzigen Zeile Rubycode besteht (die Zeile für require ‘rubykids’ zählt natürlich nicht mit).
Lösungen

require 'rubykids'

# 1. Livia
gewicht = 80
groesse = 1.85
bmi = gewicht / (groesse*groesse)
print "Der BMI bei Gewicht ", gewicht, " und Groesse ", groesse, " ist ", bmi

# 2. Peter
1.biszu(20) { |zahl| print "\r", zahl; schlafe_kurz }

Wie geht's weiter?

Erstellt von Frithjof Thu, 21 Jun 2007 20:44:00 GMT

Ich würde gerne schon längst mit Lektion 11 anfangen. Irgendwie muss ich aber die Strategie ändern. Wir haben noch recht wenig über Ruby gelernt im Vergleich zu dem, was noch vor uns liegt. Aber in einer Lektion gleichzeitig neues Wissen und praktische Anwendung in einem echten Projekt zu vermitteln, macht die Lektion etwas zu kompakt.

Zwischen den normalen Lektionen werde ich weitere Artikel (nennen wir sie einfach mal Theorieartikel) verfassen, in denen ich Neues über Ruby erkläre, das wir dann in den Lektionen verwenden können.

Die Theorieartikel müssen auch nicht so umfangreich sein. So hoffe ich häufiger zum Schreiben zu kommen, wenigstens einen Artikel oder Lektion in der Woche.