Monkeypatching in Ruby
Ruby ist eine gute Programmiersprache und mein täglich Brot auf der Arbeit. Ich bin sehr zufrieden mit Rubys Features, dem Ökosystem und den Bibliotheken für Web-Entwicklung, jedoch vermisse ich aufgrund meiner früheren Experimente mit Python ein bisher noch nicht implementiertes Feature.
Die Problematik
Die folgenden Code-Schnipsel demonstrieren ein tiefergreifendes Problem:
[][0]
{}[:foobar]
[][0]
{}['foobar']
Ein weniger erfahrener Entwickler würde erwarten dass beide Beispiele
eine Exception für den fehlerhaften Array- und
Dictionary-/Hash-Zugriff werfen. Dies ist bei der
Python-Variante der Fall, jedoch gibt die Ruby-Variante einfach nil
in beiden
Fällen zurück.
Während man argumentieren kann dass dieses Verhalten nützlich sein
kann, halte ich es für eine mögliche Fehlerquelle des berüchtigten
undefined method for nil:NilClass
-Fehlers. Die offizielle
Empfehlung ist die #fetch
-Methode für Exceptions bei
fehlerhaftem Zugriff auf Arrays und Hashes zu erhalten. Ich könnte
natürlich dieser Empfehlung nachgehen und bei jeder Stelle im Code
prüfen welche Verhaltensweise besser ist, aber was wenn ich mir
stattdessen ein Feature à la JavaScripts use strict
wünsche?
Die Lösung
Es stellt sich heraus, dass es erstaunlich einfach ist Ruby dank seiner ausgezeichneten Metaprogramming-Fähigkeiten dieses Verhalten beizubringen. Die übliche Herangehensweise ist das "öffnen" von Klassen und selektives Verändern ihrer Methoden, jedoch ist dieser Ansatz problematisch da er global angewendet wird und mit Code der sich auf das alte Verhalten lässt (oder anderem Code welcher die gleichen Klassen manipuliert) kollidieren kann.
Aus diesem Grund werde ich stattdessen das
"Refinements"-Feature ausprobieren welches in Ruby 2.1.0 eingeführt
wurde. Es erlaubt einem das Verhalten bestehender Klassen zu
verändern und die Manipulation in jedem beliebigen Scope zu
aktivieren. Der andere fehlende Baustein für diese technische Demo
ist alias_method
, ein Weg Klassenmethoden Aliase zu vergeben. Der
gewünschte Effekt kann einfach erzielt werden indem man einen Alias
von :fetch
auf :[]
erzeugt:
autoload :ItsATrap, 'its_a_trap'
module ItsATrap
refine Array do
alias_method :[], :fetch
end
refine Hash do
alias_method :[], :fetch
end
end
An dieser Stelle fehlt nur noch das Modul als Ruby Gem zu paketieren. Die offizielle Anleitung empfiehlt folgende Dateistruktur:
.
├── its_a_trap.gemspec
└── lib
└── its_a_trap.rb
Hier noch die fehlende Gemspec-Datei:
Gem::Specification.new do |s|
s.name = 'its_a_trap'
s.version = '0.0.0'
s.date = '2018-02-15'
s.summary = 'It\'s a trap!'
s.description = 'Strict mode for your Ruby files'
s.author = 'Vasilij Schneidermann'
s.email = 'v.schneidermann@gmail.com'
s.files = ['lib/its_a_trap.rb']
s.homepage = 'https://github.com/wasamasa/its_a_trap'
s.license = 'MIT'
end
gem build its_a_trap.gemspec; gem install its_a_trap-0.0.0.gem
installiert das so erzeugte Gem lokal, auf diese Weise kann es in
eigenem Code wie folgt verwendet werden:
require 'its_a_trap'
using ItsATrap
[][0]
{}[:foobar]
Monkeypatching in Ruby - Geschafft!
Und das war es auch schon! Durch das Befolgen dieser Schritte erhält man eine Bibliothek die für Debugging-Zwecke selektiv aktiviert und in Release-Code wieder deaktiviert werden kann.