Der vorliegenden Artikel übersetzt die in Python verfassten Entwurfsmuster des in Ausgabe 7/8.03 erschienenen Artikels Musterländle in die Programmiersprache Ruby. Auf eine Erklärung der bereits in o. g. Artikel eingeführten Entwurfsmuster Factory, Proxy und Template Method wird hierbei verzichtet. Zusätzlich werden drei neue Patterns vorgestellt und implementiert: Singleton, Visitor und Observer. Viele weitere Patterns und deren Implementierung in Ruby werden unter www.rubygarden.org/ruby?ExampleDesign/PatternsInRuby gezeigt und erklärt, allerdings auf Englisch.
Factory Pattern
In Ruby wird das Factory Pattern bereits durch die Klassenmethode
new implementiert (z. B.
Array.new), wobei
new zuerst ein neues Objekt der Klasse erzeugt und danach dessen
initialize-Methode aufruft, um das eben entstandene Objekt erstmals zu initialisieren. Im folgenden Beispiel verwenden wir jedoch, dem Python-Beispiel folgend, die
factory-Methode. Da es in Ruby im Gegensatz zu Python nicht möglich ist, Instanzvariablen von außen direkt abzufragen oder zuzuweisen, müssen wir uns ein Getter/Setter-Methodenpaar für die Instanzvariable
@msg definieren. Diese, gerade bei vielen Instanzvariablen, oft leidige Arbeit, nimmt uns die
attr_accessor-Methode ab. Somit werden on the fly zwei Methoden
msg (Getter) sowie
msg= (Setter) erzeugt (siehe Listing 1).
Listing 1 class A
def A.factory() new end
end
class B < A
attr_accessor :msg
def B.factory(h="Hallo")
obj = new
obj.msg = h
obj
end
def method() puts "#{ @msg } in B" end
end
class B1 < B
def initialize(h, w)
@msg, @val = h, w
end
def B1.factory(h="Hallo", w="Welt")
new(h, w)
end
def method() puts "#{ @msg } #{ @val } in B1" end
end
class C < A
def method() puts "in C" end
end
# Hauptprogramm
if __FILE__ == $0
a = A.factory; puts a.type # => A
b = B.factory; puts b.type # => B
b1 = B1.factory; puts b1.type # => B1
c = C.factory; puts c.type # => C
b.method # => Hallo in B
b1.method # => Hallo Welt in B1
c.method # => in C
end
Nebenbei sei angemerkt, dass selbst Klassen in Ruby Objekte sind, deren Methoden sich wie bei jedem anderen Objekt aufrufen lassen. Konstruktoren sind somit überflüssig und werden durch Methoden ersetzt. Zudem stellen Klassen-Methoden in Ruby nichts weiter als Singleton-Methoden des Klassenobjektes dar. Singleton-Methoden sind Methoden die nur einem ganz speziellen Objekt zugehören. Wie Singleton-Methoden definiert werden und wie diese funktionieren wird unter Listing 2 kurz dargestellt.
Listing 2 # zwei unterschiedliche Objekte (jedoch mit gleichem Wert)
a = [1, 2, 3]
b = [1, 2, 3]
# definiere Methode laenge für Objekt a
def a.laenge
size
end
p a.id == b.id # => false
p a.laenge # => 3
p b.laenge # => undefined method
Proxy Pattern (Delegator)
Ruft man in Ruby eine nicht vorhandene Methode eines Objektes auf, so wird dessen
method_missing-Methode aufgerufen, mit dem Namen der nicht vorhandenen Methode sowie deren Argumente als Parameter. Überschreibt man nun
method_missing und leitet die Methodenaufrufe mit Hilfe von
send an ein anderes Objekt weiter, so lässt sich sehr einfach wie im Beispiel (siehe Listing 3) gezeigt, das Proxy Pattern implementieren.
Listing 3 class Implementierung
def a() puts "in a" end
def b() puts "in b" end
def c() puts "in c" end
end
class Proxy
def initialize(impl)
@impl = impl
end
def method_missing(meth, *args, &block)
@impl.send(meth, *args, &block)
end
end
if __FILE__ == $0
p = Proxy.new(Implementierung.new)
p.a
p.b
p.c
end
Im unter Listing 3 gezeigten Beispiel gilt jedoch zu beachten, dass Methoden, die für Objekte der Klasse
Proxy definiert sind, und dazu gehören alle Methoden der Superklasse
Object, nicht an das zu delegierende Objekt (im Beispiel
@impl) weitergeleitet werden, da
method_missing hier nicht aktiv wird. Zum Beispiel sind dies die Methoden
to_s,
type,
class oder
id, um nur einige zu nennen. Die Lösung des Problems ist dabei recht einfach. Man muss nur diese Methoden aus der Klasse heraus löschen. Hierfür gibt es die
remove_method-Methode. Man sollte sich jedoch im Klaren sein, dass gewisse Methoden zum normalen Umgang mit Objekten benötigt werden bzw. recht nützlich sein können, so liefert beispielsweise
type oder
class das dem Objekt zugehörige Klassenobjekt oder
inspect wird von der
p-Methode aufgerufen um ein Objekt anzuzeigen. Dies ist natürlich nicht mehr möglich, sollte man diese Methoden gelöscht haben. Dazu folgendes Beispiel, in dem wir die Methoden
id und
to_s aus
Proxy entfernen:
class Proxy
# lösche Methoden id und to_s
remove_method :id, :to_s
end
Da das Proxy (bzw. Delegator) Pattern jedoch bereits in Ruby durch das Modul
delegate implementiert ist, ließe sich das Proxy-Beispiel sogar noch kürzer schreiben:
require 'delegate'
class Implementierung
# ... wie oben ...
end
if __FILE__ == $0
p = SimpleDelegator.new(Implementierung.new)
p.a
p.b
p.c
end
Template Method Pattern
Dieses Pattern lässt sich genauso wie in Python realisieren, indem man einfach eine
NotImplementedError-Exception auslöst. Da in Ruby jedoch die Klassen-Rümpfe (d. h. alles zwischen
class und
end) ausführbaren Code enthalten können, der dann im Kontext des Klassenobjektes
Class bzw. dessen Superklasse
Module ausgeführt wird, lässt sich eine generische Lösung finden:
class Module
def abstract(*methods)
methods.each do |meth|
class_eval %{
def #{meth}
raise NotImplementedError, "Muss ueberschrieben werden."
end
}
end
end
end
Wird nun die Methode
abstract innerhalb eines Klassenrumpfes aufgerufen, so wird dynamisch pro Argument eine Methode definiert, deren Rumpf schlicht und einfach die
NotImplementedError-Exception auslöst. Mit Hilfe der oben gezeigten Erweiterung reduziert sich die Ruby-Implementierung der
Template- und
Application-Klassen aus dem äquivalenten Python-Beispiel auf die unter Listing 4 gezeigte:
Listing 4 class Template
def initialize
templateMethod
end
def templateMethod
method1
method2
end
abstract :method1, :method2
end
class Application < Template
def method1
puts "in method1"
end
def method2
puts "in method2"
end
end
Template.new
Application.new
An dieser Stelle gilt zu bemerken, dass z. B.
public,
protected oder
private ebenso realisiert sind wie obiges
abstract, d. h. keine Schlüsselworte in Ruby darstellen, sondern ganz normale Methoden sind. Taucht man etwas tiefer in Ruby ein, so wird man feststellen, dass vieles, was einem auf den ersten Blick wie ein Schlüsselwort erscheint, wie z. B.
raise,
require,
throw oder
catch, tatsächlich als Methode implementiert ist. Ein Vorteil hierbei ist der höhere Grad an Flexibilität, denn Methoden lassen sich erweitern oder ersetzen, Schlüsselworte dagegen nicht.
Singleton Pattern
Auch das Singleton Pattern findet in Ruby bereits seine Anwendung. Prominenteste Beispiele hierfür sind die drei speziellen Werte
nil,
true und
false, die als Instanzen der Singleton-Klassen
NilClass,
TrueClass bzw.
FalseClass implementiert sind. Das Singleton-Pattern kommt also immer dann zum Einsatz, wenn von einer Klasse nur jeweils eine Instanz zur selben Zeit existieren darf. Listing 5 zeigt wie einfach sich dies in Ruby implementieren lässt.
Listing 5 class World
@@instance = nil
def World.instance
if @@instance.nil?
# erzeuge neue Instanz
@@instance = new
else
# gebe vorhandene Instanz zurück
@@instance
end
end
end
a = World.instance
b = World.instance
p a.id == b.id # => true
Den Nicht-Ruby-Programmierern oder Anfängern sei an dieser Stelle mit auf den Weg gegeben, dass es sich bei Variablen mit zwei
at-Zeichen (
@) um Klassenvariablen handelt, in etwa vergleichbar mit Attributen in C++ die als Static deklariert sind.
Um nicht jedes Mal das Rad neu erfinden zu müssen, ist das Singleton Pattern bereits im
singleton-Modul der Standardbibliothek implementiert, sodass man sich einige Zeilen Code ersparen kann, wie folgendes Beispiel zeigt:
require 'singleton'
class World
include Singleton
end
a = World.instance
b = World.instance
p a.id == b.id # => true
Eine anderes Beispiel für die Verwendung von Singleton-Klassen gibt die Programmiersprache Eiffel. Da Eiffel keine globalen Variablen besitzt, verwendet man hier Singleton-Klassen, um globale Daten zwischen verschiedenen Objekten auszutauschen. In Ruby ist das durchaus auch möglich, jedoch, ob dieses Verfahren Vorteile mit sich bringt, hängt immer von der jeweiligen Anwendung ab.
Visitor Pattern
Objekte, die als Behälter für andere Objekte auftreten, wie z. B. Array, Hash
oder aber auch ein XML-Tag-Objekt, das weitere Tags enthält, sollten in der Regel Methoden bereitstellen, die ein einfaches Iterieren über dessen Elemente erlauben. In Ruby ist es daher guter Stil, hierfür die Methode
each zu definieren, so wie wir dies im folgenden Beispiel für die Klasse
Sequence getan haben:
class Sequence
include Enumerable
def initialize(from, to)
@from, @to = from, to
end
def each
i = @from
while i <= @to
yield i
i = i.succ
end
end
end
Sequence.new(1, 10).each {|i| p i}
Dieses Beispiel gibt alle Zahlen zwischen eins und zehn aus. Eine Besonderheit stellt die zweite Zeile dar, in der das Modul
Enumerable eingebunden wird (so genanntes Mix-in). Dies hat zur Folge, dass alle Methoden, die in diesem Modul definiert sind, in die Klasse eingefügt werden und fortan dort verfügbar sind. Zu den im Modul
Enumerable definierten Methoden gehören unter anderem
collect,
detect,
find,
grep,
include?,
reject,
inject sowie einige weitere. Alle diese Methoden erfordern lediglich das die Methode
each definiert ist. Hat man also
each definiert und bindet
Enumerable ein, so stehen einem all diese Methoden kostenlos zur Verfügung. Hiermit lassen sich nette Dinge, wie im folgenden Beispiel gezeigt, implementieren:
p Sequence.new(1, 10).
select {|i| i % 2 == 0}. # filtere alle ungeraden Zahlen heraus
map {|i| i ** 2} # und bilde die Quadrate der verbleibenden Zahlen
# => [4, 16, 36, 64, 100]
Observer Pattern
Überall dort, wo Objekte über eine Zustandsänderung eines anderen Objektes informiert werden wollen, ist der Einsatz des Observer Patterns denkbar. Als Beispiel stelle man sich eine Dialog-Box vor, die einige grafische Elemente wie Checkboxen und Texteingabefelder enthält. Wird nun eine Checkbox angewählt, so soll ein bestimmtes Textfeld deaktiviert werden. Dies ließe sich durch das Observer Pattern realisieren, indem das Textfeld dieser Checkbox sein Interesse an dessen Zustandsänderungen mitteilt (
subscribe) und die Checkbox bei einer Änderung ihres Zustandes allen interessierten Objekten eine entsprechende Mitteilung sendet (
publish).
Im Beispiel in Listing 6 wollen wir das oben genannte Szenario mit Hilfe von
observer implementieren, das sich ebenfalls in der Standardbibliothek von Ruby befindet.
Listing 6 require 'observer'
class Checkbox
include Observable
def check
@state = true
changed
notify_observers(@state)
end
def uncheck
@state = false
changed
notify_observers(@state)
end
end
class Textfield
def update(state)
puts "Zustandsänderung: #{ state }"
end
end
cbox = Checkbox.new
text = Textfield.new
# bekunde Interesse an Zustandsänderung
cbox.add_observer(text)
# ändere Zustand der Checkbox
cbox.check # => Zustandänderung: true
cbox.uncheck # => Zustandänderung: false
Im Modul
Observable sind Methoden wie
add_observer,
changed,
notify_observers oder
delete_observer definiert, die wir durch
include in die Klasse
Checkbox einbinden und somit um dessen Observer-Funktionalität erweitern. Tritt eine Zustandsänderung ein, so kann dies den interessierten Objekten durch einen Aufruf von
changed, gefolgt von
notify_observers, angezeigt werden. Dies hat zur Folge, dass die
update-Methode aller interessierten Objekte aufgerufen wird. Um Interesse zu bekunden, muss man lediglich die
add_observer-Methode aufrufen.