Ein Entwurfsmuster
beschreibt ein bestimmtes, in einem gegebenen Kontext immer wiederkehrendes
Problem sowie ein vorgegebenes Schema zu seiner Lösung. Ein wesentliches Ziel
beim Einsatz von Entwurfsmustern ist die Minimierung von Abhängigkeiten
zwischen den beteiligten Objekten, da Software, die auf einer Architektur mit
wenigen Abhängigkeiten basiert, leichter zu pflegen und zu erweitern ist.
Entwurfsmuster sind vor allem durch das Buch "`Design Patterns"'
[GOF94] bekannt geworden.
Da Ruby dynamisch und objektorientiert ist, wird die Implementation von Desgin Patterns (Entwurfsmustern) und anderen objektorientierten Ideen zum Kinderspiel. Wir greifen hier nur einige einfache Muster auf, um eine Vorstellung zu vermitteln, worum es beim Einsatz von Entwurfsmustern geht.
Ruby bietet eine direkte Unterstützung für dieses Muster zum
Beispiel mit der Methode each an, mit der
ein Codeblock alle Elemente einer Aufzählung besuchen
kann, ohne die Interna der Aufzählung zu kennen.
Das folgende Beispiel implementiert each für die
Stundenziffern einer Uhr.
class Stunden
def each
i = 1
while i <= 12
yield i
i += 1
end
end
end
s = Stunden.new
s.each { | stunde | p stunde }
Eine Klasse hat höchstens eine Instanz, auf die von einer
zentralen Stelle aus zugegriffen werden kann.
require 'singleton' class Single include Singleton end a = Single.instance #-> #<Single:0xa0c70c8> b = Single.instance #-> #<Single:0xa0c70c8> a = Single.new #-> NameError
Wenn ein Objekt geändert wird, werden alle bei ihm registrierten
Beobachter automatisch aktualisiert.
Das Observermodul bietet folgende Methoden:
add_observer(obs): Fügt obs als Beobachter
zum aktuellen Objekt hinzu.
delete_observer(obs): Entfernt einen Beobachter.
delete_observers: Entfernt alle Beobachter.
count_observers: Zählt die Beobachter.
changed(newState=true): Wenn sich der Zustand
ändert, sollte mit changed dokumentiert werden,
dass es eine mitteilenswerte Änderung gegeben hat.
changed?: Müssen die Beobachter informiert werden?
notify_observers(*args): Wenn eine Zustandsänderung
mit changed(true) vermerkt worden ist, wird bei jedem
Beobachter die Methode update(*args) aufgerufen und
automatisch changed(false) gesetzt.
Das folgende Beispiel simuliert einen Temperatursensor in einem Ferienhaus. Wird es zu kalt, schaltet sich die Heizung automatisch ein, bei extremer Kälte wird der Besitzer informiert.
require "observer"
class Sensor
include Observable
def simulation
temperatur=11
Thread.new {
loop {
notify_observers(temperatur)
sleep 1
}
}
loop do
temperatur += rand(2) - 0.5
print "#{temperatur} "
changed
sleep 0.3
end
end
end
class Geraet
def initialize(sensor, limit)
@heizen = false
@limit = limit
sensor.add_observer(self)
end
end
class Heizung < Geraet
def update(temperatur)
if (temperatur < @limit) and (!@heizen)
puts "Heizung ein"
@heizen = true
elsif (temperatur >= @limit) and (@heizen)
puts "Heizung aus"
@heizen = false
end
end
end
class Alarmgeber < Geraet
def update(temperatur)
puts "\nALARM!!" if temperatur<@limit
end
end
sensor = Sensor.new
Heizung.new(sensor, 10)
Alarmgeber.new(sensor, 5)
sim = Thread.new { sensor.simulation }
sleep 3
liefert
10.5 10.0 9.5 Heizung ein 9.0 8.5 9.0 ... 9.5 10.0 Heizung aus 9.5 Heizung ein 9.0 8.5 8.0 7.5 8.0 7.5 ... 5.0 5.5 5.0 4.5 ALARM!! 4.0 ALARM!! 3.5 ....
Da alle Klassen in Ruby die new-Methode unterstützen,
ist das AbstractFactoryPattern schon implementiert.
Fabrik liefert ein neues Objekt, unabhängig von der Klasse:
class Plastik
def karat; puts "0"; end
end
class Diamant
def karat; puts "14"; end
end
def Fabrik(obj)
new_object = obj.new
end
t1,t2 = Fabrik(Diamant), Fabrik(Plastik)
t1.karat #-> 14
t2.karat #-> 0
Bouncer ermöglichen eine Zentralisierung der Fehlerbehandlung und können bei Bedarf auch komplexe Tests enthalten.
class ArithmeticError < RuntimeError
end
class Bouncer
@err = RuntimeError
@msg = "Fehler"
def Bouncer.check
raise(@err, @msg) unless yield
end
end
class ArithmeticBouncer < Bouncer
@err = ArithmeticError
@msg = "Rechenfehler"
def ArithmeticBouncer.gerade(n)
check { n % 2 == 0 }
end
end
Bouncer.check { 1 + 1 == 3 }
#-> RuntimeError: Fehler
ArithmeticBouncer.gerade(3)
#-> ArithmeticError: Rechenfehler
Nat Pryce hat eine einfache Möglichkeit vorgeschlagen,
mit Hilfe der Methode missing_method
einen Decorator zu implementieren, der Methodenaufrufe protokolliert.
class LogDecorator
def initialize(target)
@target = target
end
def method_missing(name, *args)
call = "#{@target}.#{name}(" +
(args.map {|a|a.inspect}).join(",") + ")"
puts "calling #{call}"
result = @target.__send__(name, *args)
puts "#{call} returned #{result.inspect}"
result
end
end
demo = "Ruby rules!"
loggedDemo = LogDecorator.new(demo)
loggedDemo.size #-> 11
Die Ausgaben des LogDecorators sehen so aus:
calling Ruby rules!.size() Ruby rules!.size() returned 11
Sehr viele weitere gute Design Patterns, inklusive Ruby-Code, findet man in [DPRuby].