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).sortHallo 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}"
end8080 localhost true NoMethodError für unbekannt
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)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["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" }
}
}
}<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".hashnil
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
trueMerke
send,define_method,method_missing,instance_variable_get/setverändern Verhalten zur Laufzeit- Immer
respond_to_missing?mitmethod_missingkombinieren - Refinements sind sicherer als globale Monkey Patches
- Pattern Matching (
case/in) zerlegt Strukturen - Metaprogrammierung sparsam einsetzen
Aufgabe
Baue eine Klasse mit method_missing, die Methodenaufrufe wie find_by_name("Mia") in eine lesbare Beschreibung übersetzt.