Mittwoch, 23. Mai 2012

entwickler.com Magazine Konferenzen Entwickler Akademie Entwickler-Forum Jobbörse Bücher
Software & Support Verlag




April 2006
aus Linux Enterprise Ausgabe: 09.2003
Schillernde Muster
Design Patterns in Ruby
von Michael Neumann

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.


    Hat Ihnen dieser Artikel gefallen? Dann abonnieren Sie das Entwickler Magazin direkt über unser Online-Formular.

zur vorherigen Seite
zurück
an den Anfang der Seite
nach oben
Diesen Artikel drucken
drucken
Diesen Artikel weiterempfehlen
empfehlen

Software & Support Verlag GmbH