EMZETT.
Login

Kurz: Ruby-Code kann sich zur Laufzeit selbst verändern. Das macht DSLs wie Rails möglich.

Teil des Kurses Ruby

Kapitel 9 von 12 im Kurs Ruby (Abschnitt „Fortgeschritten“). Mit Fortschritt, Quiz und Zertifikat auf der Lernseite.

Dynamisch Methoden aufrufen und definieren

Ruby-Code kann sich zur Laufzeit selbst verändern. Das macht DSLs wie Rails möglich.

class Roboter
  def sage_hallo = "Hallo"
end
r = Roboter.new
puts r.send(:sage_hallo)               # Methode per Namen aufrufen
puts r.public_send(:sage_hallo)
puts r.respond_to?(:fliegen)
 
Roboter.class_eval do                   # Klasse nachträglich erweitern
  define_method(:fliegen) { |ziel = "Mond"| "fliege zum #{ziel}" }
end
puts r.fliegen, r.fliegen("Mars")
 
%w[rot gruen blau].each do |farbe|     # Methoden in einer Schleife erzeugen
  Roboter.define_method("#{farbe}?") { @farbe == farbe }
end
r.instance_variable_set(:@farbe, "gruen")
puts r.gruen?, r.rot?
puts r.instance_variable_get(:@farbe)
p Roboter.instance_methods(false).sort

Ausgabe:

Hallo
Hallo
false
fliege zum Mond
fliege zum Mars
true
false
gruen
[:blau?, :fliegen, :gruen?, :rot?, :sage_hallo]

method_missing

Wird eine unbekannte Methode aufgerufen, springt Ruby in method_missing:

class Konfig
  def initialize = @daten = {}
  def method_missing(name, *args)
    n = name.to_s
    if n.end_with?("=")
      @daten[n.chomp("=").to_sym] = args.first
    elsif @daten.key?(name)
      @daten[name]
    else
      super
    end
  end
  def respond_to_missing?(name, include_private = false)
    name.to_s.end_with?("=") || @daten.key?(name) || super
  end
end
 
k = Konfig.new
k.port = 8080
k.host = "localhost"
puts k.port, k.host
puts k.respond_to?(:port)
begin
  k.unbekannt
rescue NoMethodError => e
  puts "NoMethodError für #{e.name}"
end

Ausgabe:

8080
localhost
true
NoMethodError für unbekannt

Tipp

Definiere immer auch respond_to_missing?. Und nutze Metaprogrammierung sparsam: Lesbarkeit geht vor Cleverness.

Offene Klassen und Refinements

Bestehende Klassen lassen sich erweitern (“Monkey Patching”). Sicherer sind Refinements, die nur dort gelten, wo du sie aktivierst:

module Zahlen
  refine Integer do
    def verdoppelt = self * 2
    def istprim? = self > 1 && (2..Integer.sqrt(self)).none? { |t| (self % t).zero? }
  end
end
 
using Zahlen
puts 21.verdoppelt
puts (1..30).select(&:istprim?).inspect
 
class String
  def kuerzen(n) = length > n ? self[0, n - 1] + "…" : self
end
puts "Ein langer Satz hier".kuerzen(10)

Ausgabe:

42
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
Ein lange…

Hooks und DSLs

class Basis
  @unterklassen = []
  class << self
    attr_reader :unterklassen
    def inherited(sub)
      super
      Basis.unterklassen << sub.name
    end
    def feld(name, standard: nil)
      define_method(name) { instance_variable_get("@#{name}") || standard }
      define_method("#{name}=") { |v| instance_variable_set("@#{name}", v) }
    end
  end
end
 
class Produkt < Basis
  feld :name, standard: "ohne Namen"
  feld :preis, standard: 0
end
class Kunde < Basis; end
 
p Basis.unterklassen
pr = Produkt.new
puts pr.name, pr.preis
pr.preis = 9.99
puts pr.preis

Ausgabe:

["Produkt", "Kunde"]
ohne Namen
0
9.99

Ein Blick auf eine kleine DSL mit instance_eval:

class HtmlBauer
  def initialize = @teile = []
  def tag(name, text = nil, &block)
    @teile << "<#{name}>"
    text ? @teile << text : instance_eval(&block)
    @teile << "</#{name}>"
  end
  def self.baue(&block) = new.tap { |b| b.instance_eval(&block) }.to_s
  def to_s = @teile.join
  %w[html body h1 p ul li].each { |t| define_method(t) { |text = nil, &b| tag(t, text, &b) } }
end
 
puts HtmlBauer.baue {
  html {
    body {
      h1 "Titel"
      ul { li "eins"; li "zwei" }
    }
  }
}

Ausgabe:

<html><body><h1>Titel</h1><ul><li>eins</li><li>zwei</li></ul></body></html>

Idiomatisches Ruby

# 1. Safe Navigation und Standardwerte
user = { adresse: nil }
puts user[:adresse]&.fetch(:ort, "?").inspect
name = nil
puts name || "Gast"
 
# 2. Mehrfach-Zuweisung und Splat
kopf, *rumpf = [1, 2, 3]
p kopf, rumpf
def f(a, b = 2, *rest, k: 1, **opts) = [a, b, rest, k, opts]
p f(1), f(1, 9, 8, 7, k: 5, z: 0)
 
# 3. Mustererkennung (Pattern Matching)
konfig = { name: "App", version: [1, 4, 2], db: { host: "localhost", port: 5432 } }
case konfig
in { name: String => n, version: [major, *], db: { port: Integer => port } }
  puts "#{n} v#{major}, DB-Port #{port}"
end
case [1, [2, 3]]
in [Integer => a, [b, *c]]
  p [a, b, c]
end
ergebnis = case 42
           in Integer | Float => zahl if zahl > 40 then "große Zahl #{zahl}"
           in String then "Text"
           end
puts ergebnis
 
# 4. tap, then, itself
puts [3, 1, 2].tap { |a| puts "vor sort: #{a}" }.sort.inspect
puts 5.then { |x| x * 2 }
 
# 5. Freeze und Dup
konst = %w[a b].freeze
puts konst.frozen?, konst.dup.frozen?
begin
  konst << "c"
rescue FrozenError => e
  puts e.class
end
 
# 6. Objekt-Gleichheit
puts 1 == 1.0, 1.eql?(1.0), 1.equal?(1)
puts "a".hash == "a".hash

Ausgabe:

nil
Gast
1
[2, 3]
[1, 2, [], 1, {}]
[1, 9, [8, 7], 5, {:z=>0}]
App v1, DB-Port 5432
[1, 2, [3]]
große Zahl 42
vor sort: [3, 1, 2]
[1, 2, 3]
10
true
false
FrozenError
true
false
true
true

Merke

  • send, define_method, method_missing, instance_variable_get/set verändern Verhalten zur Laufzeit
  • Immer respond_to_missing? mit method_missing kombinieren
  • Refinements sind sicherer als globale Monkey Patches
  • Pattern Matching (case/in) zerlegt Strukturen
  • Metaprogrammierung sparsam einsetzen

Übungsaufgabe

Baue eine Klasse mit method_missing, die Methodenaufrufe wie find_by_name("Mia") in eine lesbare Beschreibung übersetzt.

Quiz zur Selbstkontrolle

Weiter im Kurs

Zurück: Fehlerbehandlung, Dateien und JSON

Weiter: Tests, Gems und Bundler

Alle Kapitel: Ruby im Überblick