Aaron Fischer Ingenieur, Vater, Heimwerker, Problemlöser

29 Januar, 2007

Spionieren mit Ruby

TL;DR:

Ich habe gerade etwas darüber nachgedacht, warum Ruby keine Interfaces, wie in anderen Sprachen üblich, enthält. In der UML gibt es eine spezielle Notation dazu (siehe UML Superstructure, Kap 7.3.24) und in Klassendiagrammen werden diese auch öfter verwendet. Nun wollte ich ein solches (Subject/Observer-Pattern) in Ruby umsetzen, doch ohne Interfaces muss man einen anderen Weg einschlagen.

Im IRC (#ruby-lang) wurde mir zu Modulen bzw. Mixins geraten. Eine Technik, die einer Abstrakten Klasse sehr nahe kommt, nur das man damit konkrete Klassen erweitern kann. Um dies bei dem Design-Pattern anzuwenden, habe ich folgendes Klassendiagramm erstellt:

subject_observer_pattern.png

Das Diagramm ist nicht vollständig UML-Konform, da Mixins nirgends vorgesehen sind.

Die Implementierung in Ruby gestaltet sich (wie so oft) recht einfach. Zuerst das Modul Observer: Dieser enthält nur den Konstruktor, der das Subjekt in einem Attribut speichert.

module Observer
  def initialize(s)
    @subject = s
  end

  def update
  end
end

Das Subject-Modul übernimmt hier die meiste Arbeit: Der Konstruktor initialisiert ein neues Array, welches alle Observers sammelt. Die attach() und detach() übernehmen hier die Verwaltung dieses Arrays. Die notify()-Methode ruft von allen eingetragenen Observers die update()-Methode auf.

module Subject
  def initialize
    @observers = Array.new
  end

  def attach(observer)
    @observers << observer
  end

  def detach(observer)
    @observers.delete(observer)
  end

  def notify
    @observers.each { |view| view.update }
  end
end

Eine konkrete Implementierung würde so aussehen:

class Thing
  include Subject

  def shit_happens
    @stuff = "Plop"
    notify
  end
end
# Prints the length of the string
class View1
  include Observer

  def update
    puts @subject.stuff.length
  end
end
# Do some Stuff with the string
class View2
  include Observer

  def update
    puts @subject.stuff.reverse.capitalize
  end
end

Zum Schluss ein kleiner Test:

a = Thing.new
v1 = View1.new(a)
v2 = View2.new(a)

a.attach(v1)
a.attach(v2)
a.shit_happens

a.detach(v1)
a.shit_happens

Dies würde folgende Ausgabe erstellen:

4
Polp
Polp

Ruby ermöglicht es, sauberen Code zu schreiben, auch ohne Interfaces.