Willkommen zur

Ruby-Schulung

im Linuxhotel

Jan Krutisch

Vorstellung

Jan Krutisch

  • Freier Software-Entwickler und Gründer
  • Hamburg
  • Ruby/ Rails seit 2005
  • Projekte in allen Formen und Farben

Jetzt Ihr

  • Name
  • Vorkenntnisse (Auch: Andere Programmiersprachen)
  • Kursziele, ggf. Anwendungsfeld

Was uns erwartet

  • 5 volle Tage
  • Viel Praxis, viel Pausen
  • Viele Fragen, bitte!

Das wichtigste

  • Editor der Wahl
  • Das Pad

Themen

  1. Tag: Einführung, Installation, Grundlagen
  2. Tag: Fortsetzung, der Kern von Ruby
  3. Tag: Objektorientierung
  4. Tag: Die Standard-Library, Rubygems, das Web
  5. Tag: This page intentionally left blank

Try Ruby & Registrierung

...

Ruby

Entwickelt von Yukihiro "Matz" Matsumoto ab 1995

Design-Goals:

  • Developer Happiness
  • Von Beginn an Objektorientiert
  • Ideen aus Perl, Lisp und Smalltalk

Offizielle Implementierung:

MRI ("Matz' Ruby-Interpreter")

Diverse andere Implementierungen:

JRuby (JVM)

TruffleRuby

DragonRuby

mruby

...

Ruby ist...

...objektorientiert

...elegant

...lesbar

...schön?

3.times do
  print "Welcome"
end
20.days.from_now
# (Nicht wirklich ruby)

Installation

Viele Wege führen nach Rom

Windows

https://rubyinstaller.org/

Mac

homebrew

Linux

Paketmanager

Aber

vs.

Generic Version Managers

asdf, mise, proto, aqua

Ruby Version Managers

rbenv, rvm

New kid on the block

rv

Für heute:

mise install ruby@3.4.10
mise use ruby@3.4.10

hello_world.rb

#!/usr/bin/env ruby
puts "Hello World"

Anatomie eines Ruby-Skripts

  • "shebang" #!/usr/bin/env ruby
  • Einrückung: 2 Leerzeichen
  • Nicht vergessen: chmod +x
  • Kommentare starten mit # (wie bash, python etc.)

Über Ruby aufrufen

$ ruby script.rb

Zeichenkodierung

  • UTF-8 hat gewonnen (Code ist UTF-8 per default, seit Ruby 2.0.0)
  • Strings haben eigenes encoding (UTF-8 in meisten Fällen default)
  • Umfangreiche Funktionen für Transcoding

Syntax

Eine Anweisung pro Zeile, ohne zwingendes Semikolon

puts "Eins"
puts "Zwei"

Semikolons können verwendet werden, sehr unüblich

puts "Eins"; puts "Zwei"

IRB

$ irb

Praxis

Erste Schritte

  • Schreibe ein Programm, das auf drei getrennten Zeilen auf der Konsole eine Ausgabe macht.
  • Führe das Programm testweise in der Konsole aus.
  • Lade Dein Programm in der IRB mit Hilfe von require hinzu. Was passiert?

Variablen

Lokale Variable

a = 1

Variablen

  • Muss mit kleinem Buchstaben anfangen (Ausnahmen!)
  • Üblich: snake_case
  • Möglich: Fast alles (🎂 = "lie")
  • Keine Deklaration
  • Keine Typdeklaration

Konstanten

EPOCH_YEAR = 1970

Konstanten

  • Sind nicht konstant
  • Müssen mit großem Buchstaben anfangen
  • Üblich: SNAKE_CASE
  • Auch hier: Emojis sind möglich

Globale Variablen

$foo = "bar"

Globale Variablen

  • Nein
  • Nein
  • Nein

Datentypen

Literale

nil                         # Nichts
1, 300_000, 3.14, 2.4e-20   # Zahlen
'hallo', "hallo", %q(hallo) # Zeichenketten
:status, :through           # Symbole
true, false                 # Wahrheitswerte
10..20, 10...20, 'a'..'z'   # Bereiche
/pattern/, /^\d+/           # Reguläre Ausdrücke (Regexp)

Zeichenketten

a = 'aha'
b = "soso"
b = "jaja\nnundenn"
c = "->#{b}<-"
d = %Q("#{a}")

Symbole

  • Global einmalig
  • unveränderbar
  • Parameter, Konfiguration, etc. etc.

Alles ist ein Objekt

true.class # => TrueClass
2.class    # => Integer
2.2.class  # => Float
"hello".class # => String

Alles ist ein Objekt

  • Datentypen sind Klassen
  • Werte sind Objekte dieser Klassen
  • Weitgehende Introspektion (2.class.superclass # => Numeric)

Objekte haben Methoden

5.next              # 6
'hallo'.length      # 5
'hallo'.chop        # "hall"
:name.is_a? Symbol  # true

Klammern um Argumente?

'hallo'.length()
:name.is_a?(Symbol)

Typ-Umwandlung

23.to_s    # "23"
"42".to_i  # 42
"42".to_f  # 42.0
"3.3".to_f # 3.3
"abc".to_i # 0

Aha!

23.to_s(16)    # "17"
"abc".to_i(16) # 2748

"Globale" Methoden

(Dokumentation: Kernel)

puts "Hallo" # Zeile ausgeben

a = gets     # Zeile einlesen

Praxis

Nachfolger

Schreibt ein Programm, das den Benutzer auffordert, eine Ganzzahl einzugeben.
Anschließend soll das Programm den Nachfolger dieser Ganzzahl ausgeben.

Operatoren

5 + 1               # 6
5 / 2               # 2 (Achtung!)
5.0 / 2.0           # 2.5
"Hello" + " World"  # "Hello World"
true && true        # true
true and false      # false
true || false       # true
false or true       # true
!true               # false
not false           # true

Kurzformen

Aus Operator und Zuweisung lassen sich Kurzformen
bilden

a = a + 1
a += 1
a ||= 5
a = a || 5

Eigene Methoden Definieren

def add(a, b)
  return a + b
end

Methoden

  • Benennung wie lokale Variablen
  • Zusätzlich gewisse Sonderzeichen erlaubt
  • Beliebige Anzahl Parameter möglich

Sonderzeichen in Methodennamen

  full? # Wahrheitswert erwartet
  reset! # Seiteneffekte, In-Place, Achtung!

Zuweisungen/ "Attribute"

  car.plate=("XUF 203")
  car.plate = "XUF 203"
  puts car.plate()
  puts car.plate

Operatoren

def +(b)
  # ...
end

2.+(3)
2 + 3

Default-Parameter

def add(a, b = 1)
  return a + b
end
add(3)    # 4
add(3, 5) # 8

Benannte parameter

("keyword args")

def format_currency(amount:, currency: 'EUR')
  formatted_amount = sprintf('%.2f', amount)
  "#{formatted_amount} #{currency}"
end
format_currency(amount: 23) # 23.00 EUR
format_currency(currency: '$', amount: 6) # 6.00 $

Rückgabewerte

def add(a, b) # "Ruby Style"
  a + b
end

def add(a, b) # explizit
  return a + b
end

Praxis

Hello World & Quadratzahlen

Schreibt eine hello-world-Methode, die einen
personalisierten Gruß ausgibt. Dazu soll ein Parameter
“name” übergeben werden. Anschließend soll der Gruß
“Hallo <name>” ausgegeben werden, wobei <name>
durch den übergebenen Namen ersetzt wird.

Schreibt eine Methode, um Quadratzahlen zu berechnen.
Als Parameter soll eine Zahl übergeben werden.
Rückgabewert soll das Quadrat der Zahl sein.
Schreibt jeweils einen Aufruf der Methode, der die
Funktionsweise testet und das Ergebnis auf der Konsole ausgibt.

Kontrollstrukturen

Fallunterscheidungen

if a > 1000
  puts "zu groß"
elsif a == 23
  puts "nicht erlaubter Wert"
else
  puts "korrekte Eingabe"
end

Kompakte version

  puts "OK" if a == 23

Ternär

(a == 2) ? "zwei" : a.to_s

Unless

return 20 unless status == 200

Unless

  • Eignet sich gut für "Guards"
  • Bitte nicht:
    • Mit komplexen boolschen Ausdrücken (Knoten im Kopf garantiert)
    • Mit else kombinieren

Bedingungen

Beliebige Ruby-Ausdrücke können als Bedingungen
benutzt werden

  • true ist wahr
  • false ist unwahr
  • nil ist unwahr
  • Alles andere (auch 0, "" etc.) ist wahr
  • Kombination mit && und || und !
  • (Theoretisch auch mit and und or und not)
  • Klammern nur für Präzendenz.

Alles ist ein Ausdruck

a = if b == 1
  'Eins'
elsif b == 2
  'Zwei'
else
  b.to_s
end

Und wenn dann?!?

a = if false
  'Falsch'
end

# => a = nil

Vergleiche

2 == 2          # true
3 == 3.0        # true
3.equal? 3.0    # false

2 != 3          # true
3 > 2           # true

Fallunterscheidungen

case a
when 1
  'Eins'
when 2
  'Zwei'
else
  a.to_s
end

Fallunterscheidungen

case var
when Fixnum, Float
  'Zahlwert'
when String
  'Zeichenkette'
else
  'Unbekannter Datentyp'
end

Drei =

4 === 4         # true
Integer === 4   # true
2 === 2.0       # true
4 === Integer   # false

Praxis

Taschenrechner

Schreibt ein Taschenrechnerprogramm, das den Nutzer
nacheinander auffordert eine Zahl, einen Operator und
dann noch eine Zahl einzugeben.
Das Programm soll anhand des Operators die passende
Rechnung mit den beiden Zahlen durchführen und das
Ergebnis ausgeben.

Schleifen

loop do
  puts "I WON'T STOP!"
end

Mehr Schleifen

while true
  puts "Do something"
end

until false
  puts "Do something"
end

Kontrolle

loop do
  next if something?  # nächste iteration
  break if something? # exit
  redo if something?  # iteration wiederholen
end

Praxis

Ratespiel

Schreibt ein Zahlratespiel, das eine zufällige Zahl von 1 bis
10 (oder 100, oder …) auswählt und den Benutzer so lange
die Zahl raten lässt, bis er korrekt geraten hat.
Bei jedem Durchlauf soll angezeigt werden, um den
wievielten Versuch es sich handelt. Anschließend soll der
Nutzer einen Tipp abgeben können. Das Programm gibt aus,
ob der Tipp korrekt war, oder ob die gesuchte Zahl kleiner
oder größer ist als der abgegebene Tipp.
Ist der Tipp korrekt, beendet sich das Programm.
Andernfalls wird wieder der Versuch angezeigt und um
einen neuen Tipp gebeten. Usw.

Arrays

Sortierte Liste von Werten

a = [1, 2, 5, 6]
a[0]                            # => 1
a = ["hallo", 1, 3.8, :name]
a << 3
a.push(3)
a.pop                           # => 3
a.first                         # "hallo"

Hashes

"Assoziative Arrays"

"HashMaps"

Schlüssel/Wert-Paare

Hashes

Allgemeine Syntax

h = {:name => "Otto Mustermann", :strasse => "Musterstraße 22", :ort => "Musterstadt"}

Wenn Schlüssel Symbols sind

h = {name: "Otto Mustermann", strasse: "Musterstraße 22", ort: "Musterstadt" }
h[:name] \# "Otto Mustermann"

Code blocks

loop { puts "Endlos!" }

5.times do |i|
  puts i
end

Iteratoren

a = [1, 2, 3]
a.each { |element| puts element }
a.each_with_index do |element, i|
  puts "#{i}. #{element}"
end

Iteratoren

h = {eins: 1, zwei: 2, drei: 3}
h.each_key { |key| puts key }
h.each { |key, value| puts "#{key}: #{value}" }

Select & Map

a = [1, 2, 3, 4, 5, 6]
a.select {|e| e > 3}   # [4, 5, 6]
a.map {|e| e * 2}      # [2, 4, 6, 8, 10, 12]

API-Dokumentationen Lesen lernen

ruby-doc.org

Praxis

API-Doku lesen

  • Lest die API-Dokumentation von String, Array und/oder
    Hash.
  • Sucht Euch eine Methode aus, die Euch interessant
    erscheint und lernt ihre Funktionsweise.
  • Präsentiert Eure Methode.

Code blocks als objekte

  • Objekte vom Typ Proc
  • Erzeugen mit Proc.new, proc, lambda oder ->
  • lambda / ->
    • parameter-checks
    • Verhalten von return
  • call zum ausführen

Blöcke benutzen

def benchmark
  a = Time.now
  yield
  puts Time.now - a
end

benchmark do
  1000000.times { "23000".to_i }
end

Block?

def frame(content = "")
  content = yield if block_given?
  border = "-" * (content.size + 4)
  puts border
  puts "| #{content} |"
  puts border
end

frame("hallo")
frame { "zufall#{rand(1009)}" }

Block "durchreichen"

def twice(&block)
  2.times &block
end

twice { puts "hallo" }

Praxis

Adressdatenbank

Schreibt eine Adressdatenbank. Das Programm soll
Adressen, die mindestens Name, Straße und Ort enthalten
im Hauptspeicher ablegen und wieder verfügbar machen.
Dazu präsentiert das Programm dem Benutzer ein
Hauptmenü mit folgenden Möglichkeiten:
Bei der Eingabe von “N” (oder “n”) wird ein neuer Datensatz
angelegt. Der Benutzer wird der Reihe nach gebeten, alle
erforderlichen Daten einzugeben.
Bei der Eingabe von “L” (oder “l”) werden alle bisher
eingegebenen Datensätze in verkürzter Form ausgegeben.
Die Darstellung soll nur den Index und den Namen
enthalten.

Bei Eingabe eines gültigen Index wird der passende
Datensatz vollständig angezeigt.
Zusätzlich soll es möglich sein, das Programm zu beenden.

Dienstag

Weitere Klassen aus Core

Reguläre Ausdrücke

  • Formaler Weg um Muster in Zeichenketten zu
    beschreiben
  • Mit einem Muster kann man suchen, validieren, parsen
    und und und…
  • Starker theoretischer Background
  • Viel Verwendung in Perl und auf der Shell
  • In Ruby Objekte

Einfache Ausdrücke

/a/
/hallo/
regexp = /test/
a = "hal"
/#{a}lo/ # /hallo/

Pattern matching

"hallo" =~ /a/    # => 1
"hallo" =~ /l/    # => 2
"hallo" =~ /test/ # => nil
"hallo" !~ /p/    # => true

if "hallo" =~ /allo/
  puts "Selber Hallo"
end

Objektorientiert

regexp = /l/
match_data = regexp.match("hallo")
match_data.size       # 1
match_data.pre_match  # "ha"
match_data.post_match # "lo"
/x/.match("hallo")    # nil

Zeichengruppen

  • Vokale: [aeiou]
  • Großbuchstaben: [A-Z] (Falle: Umlaute!)
  • Keine Kleinbuchstaben: [^a-z]
  • Ziffern: [0-9]
  • l,m,n,o: [l-o]
  • Beliebiges Zeichen: .
  • /h.llo/ matcht auf "hallo", "hello" usw.

Quantifizierer

  • Ein oder keinmal: ?
  • Beliebig oft (auch keinmal): *
  • Beliebig oft, aber mindestens einmal: `+``
  • Genau 5 mal: {5}
  • 5 mal oder öfter: {5,}
  • 5-10 mal: {5,10}

Beispiel

Variablennamen in Ruby, Versuch 1:

/[_a-z][a-zA-Z0-9_]*/

Alternativen

eine Auswahl zwischen Alternativen wird mit | markiert

/[_a-z][a-zA-Z0-9_]*(\?|!)?/

Bereichsmarkierungen

  • Zeilenanfang: ^
  • Zeilenende: $
  • Stringanfang: \A
  • Stringende: \z

Beispiel

Variablennamen in Ruby, Versuch 2:

/^[_a-z][a-zA-Z0-9\_]*$/

Variablennamen in Ruby, Versuch 3:

/\A[_a-z][a-zA-Z0-9_]*\z/

Zeichenklassen

  • \d entspricht [0-9], \D [\^0-9]
  • \s Whitespace
  • \w gültige Zeichen in Bezeichnern ([a-zA-Z0-9\_])
  • \b Wortgrenze

Posix-Expressions

Mit Unicode-Unterstützung (Auswahl):

  • [[:alpha:]] Buchstabe
  • [[:digit:]] Ziffer
  • [[:alnum:]] alphanumerisches Zeichen

Beispiel

Variablennamen in Ruby, Versuch 4:

/\A[_a-z]\w*\z/

Modifikatoren

Am Ende eines Ausdrucks
Mehrere Kombinierbar

  • i steht für case-insensitive
  • m steht für multiline
  • x für das entfernen von Whitespace
  • o dafür, Ersetzungen nur einmal vorzunehmen

Darüberhinaus weitere für Encoding

Gier / Greed

* wird häufig benötigt, macht aber oft nicht, was man erwartet.

Beispiel HTML-Parsen: /\<.*>/ findet nicht einzelne Tags, sondern das gesamte Dokument
* ist greedy (gierig) und sucht den größtmöglichen Match

*? sucht den kleinsten Match

Arbeiten mit Matches

Für geklammerte Ausdrücke werden gematchte Strings
gespeichert ("Capture Groups")

"hallo" =~ /h(a)ll(o)/ # => 0
$1 # => "a"
$2 # => "o"

Objektorientiert

date_exp = /(\d{1,2})\. (\w+) (\d{2,4})/
date_string = "23. Januar 2014"
match_data = date_exp.match(date_string)
match_data.size     # => 4
match_data[0]       # => "23. Januar 2014"
match_data[1]       # => "23"
match_data.captures # ["23", "Januar", "2014"]

Benannte Gruppen

exp = /(?<day>\d{1,2}). (?<month>\w+) (?<year>\d{2,4})/
string = "23. Januar 2014"
match_data = exp.match(string)
match_data[:day] # "23"
match_data[:month] # "Januar"

Alle Fundstellen finden

date = "23.01.2014"
date.scan(/\d+/)              # => ["23", "01", "2014"]
date.scan(/(\d{2})\.(\d{2})/) # => [["23", "01"]]
date.scan(/\d+/) { |match| puts match }

Suchen & Ersetzen

"hallo".sub(/l/, "test")  # => "hatestlo"
"hallo".gsub(/l/, "test") # => "hatesttesto"
"Otto Mustermann".sub(/(\w+)\s+(\w+)/, '\2, \1') # "Mustermann, Otto"

Strings teilen

"1,2,3".split(",") # => ["1", "2", "3"]
"1 2\t3\n4".split(/\s+/) # ["1", "2", "3", "4"]

Dokumentation und Spielwiese

Praxis

Taschenrechner, die Zweite

Verbessert das Taschenrechnerprogramm, indem Ihr die
Eingabe des Benutzers in einem einzigen Schritt verlangt
und mit Hilfe regulärer Ausdrücke auf ihre Gültigkeit prüft.

Hat der Benutzer nicht, wie gefordert, eine Zahl, bzw. einen
der Operatoren +, -, * und / eingegeben, soll keine
Berechnung erfolgen, sondern eine Fehlermeldung
ausgegeben werden.

Kommandozeilenargumente

  • Alle Kommandozeilenargumente landen in ARGV
  • ARGV ist ein Array
  • ARGV.size nennt Anzahl der Parameter
  • (Anders als in C ist ARGV[0] nicht der Programmname)
  • OptionParser für komplexere Fälle (--help, -v, --verbose)
puts ARGV[0]

Umgebungsvariablen

Umgebungsvariablen landen in der Konstanten ENV
ENV verhält sich wie ein Hash:

ENV["HOME"] # "/home/dave"

Shellout

result = `uname`    # => "Linux\ņ"
system("uname -a")  # => true

I/O

  • STDIN, STDOUT, STDERR
  • puts ist eigentlich STDOUT.puts
  • gets ist STDIN.gets

I/O-Operationen

  • Zeilenweise: gets, puts
  • Zeichenweise: getc, putc
  • Mehr: read, write
  • Gilt für Streams, Dateien, Netzwerk

Dateien

File.open("test.txt", "w") do |file|
  file.puts "Hallo"
end

“r“ead, “w“rite, “a“ppend (, “b“inary)

Datei zeilenweise verarbeiten

File.open("data.txt", "r") do |file|
  file.each_line do |line|
    puts line
  end
end

Helper

File.exists?("/tmp/test.txt")   # false
File.basename("/tmp/test.txt")  # "test.txt"
File.dirname("/tmp/test.txt")   # "/tmp"
File.join("tmp", "test.txt")
# "tmp/test.txt"
Dir.mkdir("/tmp/testdir")
Dir.entries("/tmp")             # [...]
Dir.glob("/tmp/*.txt")          # [...]

Praxis

Addressdatenbank die zweite

Erweitert Eure Adressdatenbank, so dass Ihr die Daten
dauerhaft abspeichern und auch wieder einlesen könnt.

Datum und Uhrzeit

t = Time.now    # 2026-10-27 18:21:04 +0200
t.year          # 2026
t + 60          # 2013-05-27 18:22:04 +0200
t.to_i          # Sekunden seit 01.01.1970

strftime

# %d Tag, %m Monat, %Y Jahr (vierstellig)
Time.now.strftime("%d.%m.%Y") # "25.09.2018"

Mittwoch

Objektorientierte Programmierung

OO

  • Stammt u.a. von Smalltalk, entwickelt im Xerox PARC
  • Wird verwendet in C++, Java, Ruby, Python(, Perl, PHP …)
  • Technische Vorteile, aber auch besser an den Menschen angepasst

Was bedeutet Objektorientierung

  • Objekte als Funktionseinheiten
  • Objekte Bündeln Daten (Attribute) und Funktionen (Methoden)
  • Objekte kommunizieren über Nachrichten (Methodenaufrufe)
  • Vererbung als Möglichkeit der Abstraktion

Ziele aus Entwicklersicht

Modellierung in Objekten entspricht eher dem
menschlichen Denken (citation needed)

Ziele aus technischer Sicht

  • Bessere Struktur
  • Stärkere Kapselung
  • Weniger Redundanz
  • Wiederverwendbarkeit
  • Oder zusammengefasst: Modularität

Wichtige Begriffe

  • Klasse (Bauplan)
  • Instanz (konkretes Objekt, das nach Bauplan der Klasse
    erstellt wurde)
  • Instanzenvariable (Eigenschaft des Objekts)
  • Instanzenmethode
  • Klassenvariablen und Klassenmethoden

Klassen entwerfen

  • Welche Objekte soll mein System enthalten?
  • Was sind die Daten, wer sind die Akteure?
  • Wie sind Zuständigkeiten verteilt?
  • Was kann ich möglicherweise wiederverwenden?

Beispiel Musik-Player

  • Klassen: Song, Artist, Album
  • Song-Eigenschaften: Titel, Länge usw.
  • Song-Verhaltensweisen: Abspielen, Pausieren usw.

Beispiel Büromöbel

  • Klassen: Möbelstück, Stuhl, Tisch
  • Möbelstück-Eigenschaften: Beschreibung, Hersteller usw.
  • Stuhl-Eigenschaften: Sitzhöhe, Armlehnen usw.
  • Tisch-Eigenschaften: Länge, Breite, Höhe usw.
  • Stuhl und Tisch sind beides Möbelstücke und teilen sich
    die gemeinsamen Eigenschaften

Klassen in Ruby

class Song
  # [...]
end

s = Song.new

Klassen in Ruby

  • In einer .rb-Datei darf man beliebig viele Klassen definieren
  • Best practice: Bei größeren Programmen eine Klasse pro Datei
  • Code zwischen class und end wird einmalig ausgeführt (!)

Methoden

class Song
  def play
    puts "playing!"
  end
end

s = Song.new
s.play

this is self

  • self ist im Instanz-Kontext die Instanz
  • self ist im Klassen-Kontext die Klasse

Klassenmethoden

class Player
  def self.play
    puts "Playing..."
  end
end

Player.play

Konstruktor

class Song
  def initialize(title)
  end
end
s = Song.new("Piano Man")

Instanzvariablen

  • @instanzvariable
  • Repräsentiert internen Zustand, Zugriff von außen möglich, aber mit Absicht doof

Instanzvariablen

class Song
  def initialize(title)
    @title = title
  end

  def play
    puts "Now Playing: #{@title}"
  end
end
s = Song.new("Piano Man")
s.play

"Attribute"

class Song
  attr_accessor :title

  def initialize(title)
    @title = title
  end
end

s = Song.new("Piano Man")
s.title     # => "Piano Man"
s.title = "Piano Man 2"

"Attribute"

attr_accessor, attr_reader, attr_writer

class Song
  def title=(title)
    @title = title
  end

  def title
    @title
  end
end

Klassenvariablen

  • @@songs_played ist eine Klassenvariable
  • Existiert genau einmal pro Klasse (inkl. Unterklassen)
  • Übergeordnetes Wissen: Ein einzelner Song kann und muss nicht wissen, welche Songs bisher schon gespielt wurden
  • Selten wirklich notwendig

Praxis

Bücher

Schreibt eine Klasse, die Bücher repräsentiert.
Ein konkretes Buch soll folgende Eigenschaften haben:
Titel, Autor, Seitenzahl, Preis
Alle Eigenschaften sollen von außen les- und schreibbar
sein.
Außerdem sollen sie bereits beim instanziieren von BuchObjekten übergeben werden.
Die Klasse soll nachhalten, wieviele Bücher instanziiert
wurden und eine Methode bieten, mit der die Anzahl bisher
instanziierter Bücher abgefragt werden kann.

DRY: Vererbung

  • Unterschiedliche Objekte können Gemeinsamkeiten aufweisen, sowohl bei Eigenschaften als auch bei Verhaltensweisen
  • Auch hier bitte kein Copy & Paste
  • Sind es gleichartige Objekte, kann man Vererbung nutzen
  • Eine Oberklasse enthält alle Gemeinsamkeiten
  • Unterklassen erben von dieser und implementieren nur noch die Unterschiede
  • Vorsicht vor zu enger Kopplung
  • In Ruby gibt es Alternativen

Vererbung in Ruby

class Square < Rectangle
end

Vererbung in Ruby

  • Square (Quadrat) erbt alle Eigenschaften von Rectangle (Rechteck) (Ein Quadrat ist ein Spezialfall eines Rechtecks)
  • Das Überschreiben und Ergänzen von Methoden ist möglich
  • Zugriff auf überschriebene Methoden mit super

Vererbung

class Rectangle
  def initialize(width, height)
    @width = width
    @height = height
  end
  def area ; @width * @height ; end
end

class Square < Rectangle
  def initialize(width)
    super(width, width)
  end
end

Sichtbarkeit

  • public (implizit): immer aufrufbar
  • protected : Nur aufrufbar, wenn Empfänger die selbe Klasse hat wie “self”
  • private (Schlüsselwort) : Nur auf implizitem self aufrufbar (also nur innerhalb der selben Klasse)

Praxis

Kaffeemaschine

Wenn's mal schief geht

Wie behandeln wir Fehlerfälle?

  • In C-APIs (Und z.B. in Go): Rückgabewerte
  • In OO-Sprachen (meistens): Exceptions ("Ausnahmen")

Exceptions in Ruby

raise "Uff, alles kaputt"

Exceptions abfangen

begin
  things_that_can_go_wrong
rescue ThingsWentWrongError => e
  puts "Things went wrong: #{e}"
ensure
  puts "we'll always get here"
end

Eigene Exceptions

class ThingsWentWrongError < StandardError
  attr_reader :thing
  def initialize(thing)
    @thing = thing
  end
end

raise ThingsWentWrongError.new("A Thing that went wrong")

Sonderfall Methoden-Ebene

def cannot_go_wrong
  things_that_can_go_wrong
rescue ThingsWentWrongError => e
  puts "Dammit"
end

Detaillierte Fehlerbehandlung

begin
  things_that_can_go_wrong
rescue ThingsWentWrongError => e
  puts "Thing went wrong"
rescue IOError => e
  puts "Something different went wrong"
end

Retry / Re-Raise

begin
  goes_sometimes_wrong
rescue ThingsWentWrongError
  retry
rescue FatalError
  puts "Okay, Mist"
  raise
end

Praxis

Kaffeemaschine mit Exceptions

  • Entfernt alle interaktiven Elemente (puts, gets) aus der Klasse Kaffeemaschine.
  • Ersetzt Ausgaben durch Rückgabewerte der Methoden.
  • Erweitert die Klasse Kaffeemaschine um eine Fehlerausgabe mit Hilfe von Exceptions.
  • Definiert hierfür eigene Exceptions, die den drei möglichen Fehlerfällen beim Kaffeekochen entsprechen.
  • Sorgt dafür, dass beim Auftreten eines Fehlerfalls die passende Exception geworfen wird.

Module

  • Code-Organisation (Namespace)
  • Kapselung
  • Wiederverwendung (Mixins)

Module

module MyModule
  # [...]
end

Namespaces

module MusicPlayer
  VERSION = "0.0.1"
  class Song
  end
end

puts MusicPlayer::VERSION
s = MusicPlayer::Song.new

Mixins

  • Besser für "cross cutting concerns" als Vererbung (Stuhl, Tisch, Benutzer)
  • Verhindert unendliche Vererbungsbäume

Mixins

module Logger
  def log(msg) ; puts msg ; end
end

class User
  include Logger
end

u = User.new
u.log("hello")

Klassenmethoden

class System
  extend Logger
end
System.log("test")

Benutzung erkennen

module MyModule
  def self.included(klass)
    puts "MyModule wurde in #{klass} inkludiert"
  end
  def self.extended(klass)
    puts "#{klass} wurde durch MyModule erweitert"
  end
end

Automatisiertes testen

  • Ruby ist sehr dynamisch, keine echte Kompiler-Phase die z.B. Typfehler findet
  • Automatisierte tests als Fangnetz
  • Test first: Tests als Spezifikation, red-green-cycle

Die Zwei Welten

  • In Standard-Library: minitest
  • Spec driven development: rspec

Ein Test

require 'minitest/autorun'
class SongTest < Minitest::Test
  def test_duration_in_minutes
    song = Song.new('test', 300)
    assert_equal 5, song.duration_in_minutes
  end
end

Assertions

  • assert / refute
  • assert_equal
  • assert_instance_of
  • assert_kind_of
  • assert_nil
  • refute_nil
  • assert_raises
  • assert_match

Setup

class SongTest < MiniTest::Test
  def setup
    @song = Song.new('test', 300)
  end
  def test_duration_in_minutes
    assert_equal 5, @song.duration_in_minutes
  end
end

Mocks/ Stubs

  • Unit-Tests sollen immer nur eine Einheit testen
  • Manche Komponenten sollten beim Testen nicht real ausgeführt werden (Bezahlsystem, Emails)
  • Andere verlangsamen Tests vielleicht unnötig
  • Wieder andere liefern nicht die zum Testen notwendigen reproduzierbaren Ergebnisse (Time.now !)
  • Bei “Test first”-Ansätzen existieren Komponenten möglicherweise noch gar nicht

Mocks/ Stubs in Minitest

mock = MiniTest::Mock.new
mock.expect(:to_s, 'test')
mock.to_s
mock.verify
Time.stub(:now, Time.at(1234567890)) do
  Time.now # 2009-02-14 00:31:30 +0100
end

Test driven development - TDD

  • Aus Test-first entwickelte Vorgehensweise
  • Red-Green-Refactor
  • Red: Kleinen, fehlschlagenden Test schreiben
  • Green: Code schreiben, bis Test erfolgreich läuft
  • Refactor: Ergebnis kritisch betrachen, ggf. umstrukturieren und dabei mit Test absichern

Tipps & Lebenslektionen

  • Test first / TDD ausprobieren
  • Testen lohnt bei großen / langlebigen Projekten fast immer
  • Wenn Testen kompliziert ist, ist das ein Hinweis, dass die Klasse zu komplex ist
  • Wenn ein Bug entdeckt wird, immer erst Test schreiben, dann fixen (Regressionstest)
  • Auch Tests refactorn (Aber nicht zu sehr!)
  • Üben, üben, üben

Praxis

Kaffeemaschine

Testen

Schreibt Tests für Eure Kaffeemaschine.

Code coverage

  • Was ist ein guter Test?
  • Metriken, z.B. wieviel Code wurde tatsächlich durchlaufen?
  • Für Ruby z.B. SimpleCov (gem install simplecov)

Debugging

  • Skript debuggen: require "debug"
  • Ausdrücke evaluieren mit p
  • Kommandobsp.: list, where, step, next, break, cont
  • Alternativ: Gems: byebug, pry

Donnerstag

Core vs. standard library vs. default gems

  • Klassen aus Core stehen immer zur Verfügung (ohne require)
  • Klassen der Stdlib werden mit Ruby zusamme ausgeliefert und können als gegeben betrachtet werden
  • Trotzdem explizites require
  • Stdlib besteht aus Klassen, die mit Ruby entwickelt werden, und vorinstallierten Gems (default gems)

Standard library

  • Datentypen: BigDecimal, Date, IPAddr, Set
  • Parser/ Generatoren: csv, json, yaml, rexml
  • Kryptographie: digest, securereandom, openssl
  • Netzwerk: socket, net/http etc.
  • Web: cgi, erb, webrick
  • Tools: irb, rake, rdoc, debug

CSV-Dateien verarbeiten

  • Bibliothek ‘csv’ zum Lesen und Schreiben
  • Gängige Eigenschaften können konfiguriert werden, wie z.B.
    • Trennzeichen
    • Anführungszeichen
    • Zeilenumbruch
    • Kopfzeile
    • Encoding

Lesen

require "csv"
CSV.foreach("/tmp/test.csv") do |row|
  pp row
end

Schreiben

CSV.open("/tmp/test.csv", "w") do |csv|
  csv << [23,"test",11]
  csv << [42,"hallo", 12]
end

Logger

  • Simple Bibliothek zum Schreiben von Logfiles
  • Ausgabe in Datei oder Ausgabestrom
  • Log-Level: debug, info, warn, error, fatal, unknown
  • Sinnvolles Standardformat der Nachrichten
  • Rotation von Logdateien

Logger

require 'logger'
logger = Logger.new("/tmp/logfile.log")
logger.level = Logger::WARN
logger.debug("setup complete")
logger.error("nothing much is happening...")

Fileutils

  • Methoden, die benannt sind und funktionieren wie auf der Shell
  • Bsp: cp, rm, mv usw.
  • Besonders nützlich: mkdir_p

Rake

  • Ruby-Variante des bekannten make-Tools
  • sucht im aktuellen Verzeichnis automatisch nach Datei “Rakefile“
  • einfache Syntax, aber volle Ruby-Power
  • rake -T zum Auflisten aller Tasks
  • Eingebaute Tasks z.B. für Tests

Beispiel rakefile

task :hello do
  puts "hello"
end
task :world do
  puts "world"
end

task :hello_world => [:hello, :world]
rake hello_world

Rake testtask

require "rake/testtask"
Rake::TestTask.new do |t|
  t.pattern = 'test/**/*_test.rb'
end
rake test

Praxis

Stdlib

Probiert eine oder mehrere der Bibliotheken aus der
Standard Library aus.

Rubygems

  • Paketverwaltung für Ruby-Programme und -Bibliotheken
  • CLI ähnlich apt-get
  • gem install <paketname>
  • unzählige Pakete verfügbar
  • rubygems.org
  • ruby-toolbox.com

Bundler

  • Deklaration der Abhängigkeiten in Gemfile
  • Lockfile: Gemfile.lock sorgt für reproduzierbare Installationen
  • bundle install

Rubygems selber bauen

  • <gemname>.gemspec enthält deklaration
  • Standardisiertes Verzeichnislayout
    • lib für code
    • bin oder exe für ausführbare Dateien
    • test für tests
  • Tools, z.B. bundle gem zum generieren

Praxis

Kaffeemaschinen-Gem bauen

Erstellt ein ausführbares Programm, dass Eure
Kaffeemachine interaktiv benutzbar macht.
Paketiert diese Datei, die Kaffeemaschine und Eure Tests als
Gem.

Webanwendungen mit Sinatra

  • Simpler Ansatz, Webanwendungen mit Ruby zu entwickeln
  • Gegenentwurf zum “schwergewichtigen” Ruby on Rails
  • Sehr einfache API, flexibel erweiterbar
  • Ideal für einfache Anwendungen und Web-APIs
  • gem install sinatra

Sinatra DSL

require "sinatra"
get '/' do
  "Willkommen"
end

post '/login' do
  "Willkommen #{params[:user]}"
end

Templates

get '/now' do
  @now = Time.now
  erb :now
end
<p>Current time: <%= @now %></p>

Layouts

  • Layouts sind Templates, die ein yield enthalten
  • Ideal für üblichen HTML-Rahmen, fixe Header und Footer
  • Default: layout.erb für ERb-Templates
  • Für jedes Template konfigurierbar

Parameter

post '/login' do
  @username = params[:username]
  erb :welcome
end

Parameter in der URL

get '/posts/:id' do |id|
  @id = id
  erb :post
end
<h3>Post: <%= @id %></h3>

Praxis

Webbasierte Kaffeemaschine

Entwickelt eine Webseite, über die sich Eure
Kaffeemaschine fernsteuern lässt.

Fazit

Jeder fängt mal klein an

puts "Hello World"

Erste strukturen

def greet(name)
  puts "Hello #{name}"
end

OO

class Greeter
  def initialize(greeting)
    @greeting = greeting
  end

  def greet(name)
    puts "#{greeting} #{name}"
  end
end

Ruby-Projekte

Web: rails, hanami, sinatra
Webseiten: jekyll, bridgetown
Projektmanagement: redmine, gitlab
Social Media: Mastodon
Dokumente erzeugen: prawn, hexapdf, axlsx
E-Mails: mail
HTML/XML: nokogiri, builder

Und jetzt?

DAY 2

DAY 3

DAY 4