Programmierung in Ruby

Der Leitfaden der Pragmatischen Programmierer

Bibliotheken für objekt-orientiertes Design



Eine der interessantesten Sachen bei Ruby ist die Art, wie es den Unterschied zwischen Design und Implementierung verwischt. Ideen, die in anderen Sprachen auf Entwurfs-Ebene ausgedrückt werden müssen, können in Ruby direkt implementiert werden.

Um das zu fördern, unterstützt Ruby einige Stategien der Design-Ebene.

Normalerweise braucht jede dieser vier Strategien jedesmal Code, wenn sie verwendet wird. Bei Ruby können sie in einer Bibliothek verallgemeinert und frei und transparent wieder verwendet werden.

Bevor wir uns die entsprechenden Bibliotheks-Beschreibungen genauer ansehen, handeln wir erstmal die einfachste Strategie ab.

Das Besuchs-Muster

Das ist die Methode each.

Library: delegate

Mit der Delegation von Objekten hat man die Möglichkeit, Objekte zu Laufzeit zusammenzustellen --- ein Objekt mit den Fähigkeiten eines anderen zu erweitern. Damit kann man flexiblen und entkoppelten Code schreiben, da es so etwas wie Abhängigkeiten zwischen übergeordneten und untergeordneten Klassen zur Kompilierzeit nicht gibt.

Die Delegator-Klasse in Ruby realisiert ein einfaches aber wirkungsvolles Schema, mit der Anfragen automatisch von einer Master-Klasse an Deligierte weitergeleitet werden können, wobei die Delegierten zu Laufzeit mit einem einfachen Methoden-Aufruf geändert werden können.

Die delegate.rb-Bibliothek unterstützt zwei Mechanismen, mit denen ein Objekt eine Meldung an einen Delegierten reichen kann.

  1. Bei einfachen Fällen, bei denen die Klasse des Delegierten fest ist, richtet man die Master-Klasse als Unterklasse von DelegateClass ein und übergibt dabei den Namen des Delegierten als Parameter (Beispiel 1). Danach ruft man in der initialize-Methode der Master-Klasse die Oberklasse auf und übergibt ihr das Delegierten-Objekt. Als Beispiel deklarieren wir die Klasse Fred, die auch alle Methoden aus Flintstone unterstützen soll:

    class Fred < DelegateClass(Flintstone)
      def initialize
        # ...
        super(Flintstone.new(...))
      end
      # ...
     end
    
    Der Unterschied zu Unterklassen ist leicht zu übersehen. Bei Unterklassen gibt es nur ein Objekt, das besitzt Methoden, die definierte Klasse und ein Elternteil sowie deren Vorfahren. Bei der Delegation gibt es zwei Objekte, die so verbunden sind, dass Aufrufe für das eine an das andere weitergeleitet werden können.

  2. Bei Fällen, bei denen das Delegieren dynamisch gehalten werden muss, macht man die Master-Klasse zu einer Unterklasse von SimpleDelegator (Beispiel 2). Man kann mit SimpleDelegator die Fähigkeit der Delegation auch einem schon existierenden Objekt hinzufügen (Beispiel 3). In beiden Fällen ruft man die Methode __setobj__ in SimpleDelegator auf, um das Delegierten-Objekt zur Laufzeit zu ändern.

Beispiel 1. Wenn man eine Klasse mit eigenem Verhalten benötigt, die zusätzlich an ein Obejkt einer anderen Klase delegiert, benutzt man die DelegateClass-Methode und erzeugt mit dem Ergebnis eine Unterklasse. In diesem Beispiel nehmen wir an, dass das sizeInInches-Array sehr groß ist, so dass wir nur ein Exemplar davon haben wollen. Dazu definieren wir eine darauf zugreifende Klasse, die die Werte in Feet umrechnet.

require 'delegate'

sizeInInches = [ 10, 15, 22, 120 ]

class Feet < DelegateClass(Array)   def initialize(arr)     super(arr)   end   def [](*n)     val = super(*n)     case val.type     when Numeric; val/12.0     else;         val.collect {|i| i/12.0}     end   end end

sizeInFeet = Feet.new(sizeInInches)
sizeInInches[0..3] » [10, 15, 22, 120]
sizeInFeet[0..3] » [0.8333333333, 1.25, 1.833333333, 10.0]

Beipiel 2. Man erzeugt eine Unterklasse mit SimpleDelegator, wenn man ein Objekt braucht, das sein eigenes Verhalten besitzt und zusätzlich unterschiedliche Objekte delegieren soll. Dies ist auch ein Beispiel für ein Zustandsmuster. Objekte der Klasse TicketOffice verkaufen Tickets wenn ein Verkäufer vorhanden ist, oder sagen, dass man wiederkommen soll, wenn keiner vorhanden ist.

require 'delegate'

class TicketSeller   def sellTicket()     return 'Here is a ticket'   end end

class NoTicketSeller   def sellTicket()     "Sorry-come back tomorrow"    end end

class TicketOffice < SimpleDelegator   def initialize     @seller = TicketSeller.new     @noseller = NoTicketSeller.new     super(@seller)   end   def allowSales(allow = true)     __setobj__(allow ? @seller : @noseller)     allow   end end

to = TicketOffice.new
to.sellTicket » "Here is a ticket"
to.allowSales(false) » false
to.sellTicket » "Sorry-come back tomorrow"
to.allowSales(true) » true
to.sellTicket » "Here is a ticket"

Beispiel 3. Man erzeugt SimpleDelegator-Objekte, wenn man möchte, dass ein einzelnes Objekt alle seine Methoden an zwei oder mehr andere Objekte delegiert.

# Example 3 - delegate from existing object
seller   = TicketSeller.new
noseller = NoTicketSeller.new
to = SimpleDelegator.new(seller)
to.sellTicket » "Here's a ticket"
to.sellTicket » "Here's a ticket"
to.__setobj__(noseller)
to.sellTicket » "Sorry-come back tomorrow"
to.__setobj__(seller)
to.sellTicket » "Here's a ticket"

(Der Übersetzer: irgendwie kommt mir das alles komisch vor. Ich glaube, da ist irgendwie durcheinandergekommen, wer Methoden abgibt und wer sie empfängt.)

Library: observer

Das Beobachter-Muster, auch bekannt unter Publish/Subscribe (Veröffentlichen/Abonnieren), liefert einen einfachen Mechanismus, mit dem ein Objket eigene Zustandsänderungen an andere Objekte melden kann.

Bei der Implementation von Ruby mischt die meldende Klasse das Observable-Modul ein, das die Methoden zum Verwalten der zugehörenden Beobachter-Klassen verwaltet.

add_observer(obj) Füge obj als ein Beobachter dieses Objektes hinzu. obj bekommt ab jetzt Mitteilungen.
delete_observer(obj) Lösche obj als Beobachter dieses Objektes. Es bekommt ab jetzt keine Mitteilungen mehr.
delete_observers Lösche alle mit diesem Objekt verbundenen Beobachter.
count_observers Liefert die Anzahl der mit diesem Objekt verbundenen Beobachter.
changed(newState=true) Setzt den Changed-Status dieses Objektes. Mitteilungen werden nur gesendet, wenn der Change-Status true ist.
changed? Fragt den Changed-Status dieses Objektes ab.
notify_observers(*args) Falls der Changed-Status dieses Objektes true ist, so rufe die update-Methode von jedem zur Zeit angeschlossenen Beobachter auf und übergib ihr die angegebenen Argumente. Der Changed-Status wird danach auf false gesetzt.

Die Beobachter müssen die update-Methode implementieren, um Mitteilungen empfangen zu können.

require "observer"

  class Ticker # Periodically fetch a stock price     include Observable

    def initialize(symbol)       @symbol = symbol     end

    def run       lastPrice = nil       loop do         price = Price.fetch(@symbol)         print "Current price: #{price}\n"         if price != lastPrice           changed                 # notify observers           lastPrice = price           notify_observers(Time.now, price)         end       end     end   end

  class Warner     def initialize(ticker, limit)       @limit = limit       ticker.add_observer(self)   # all warners are observers     end   end

  class WarnLow < Warner     def update(time, price)       # callback for observer       if price < @limit         print "--- #{time.to_s}: Price below [email protected]: #{price}\n"       end     end   end

  class WarnHigh < Warner     def update(time, price)       # callback for observer       if price > @limit         print "+++ #{time.to_s}: Price above [email protected]: #{price}\n"       end     end   end

ticker = Ticker.new("MSFT") WarnLow.new(ticker, 80) WarnHigh.new(ticker, 120) ticker.run
erzeugt:
Current price: 83
Current price: 75
--- Sun Mar 04 23:26:31 CST 2001: Price below 80: 75
Current price: 90
Current price: 134
+++ Sun Mar 04 23:26:31 CST 2001: Price above 120: 134
Current price: 134
Current price: 112
Current price: 79
--- Sun Mar 04 23:26:31 CST 2001: Price below 80: 79

Library: singleton

Das Design-Muster Singleton stellt sicher, dass nur eine Instanz einer speziellen Klasse erzeugt werden kann.

Die singleton-Bibliothek macht die Implemantation einfach. Man mischt einfach das Modul Singleton zu jeder Klasse bei, die ein Singleton werden soll, und die new-Methode dieser Klasse wird privat. An seiner Stelle tritt die Methode instance, die eine Singleton-Instanz der Klasse zurück liefert.

Im folgenden Beispiel sind die beiden Instanzen von MyClass ein und dasselbe Objekt.

require 'singleton'
class MyClass
  include Singleton
end
a = MyClass.instance » #<MyClass:0x4018c924>
b = MyClass.instance » #<MyClass:0x4018c924>


Extracted from the book "Programming Ruby - The Pragmatic Programmer's Guide"
Übersetzung: Jürgen Katins
Für das englische Original:
© 2000 Addison Wesley Longman, Inc. Released under the terms of the Open Publication License V1.0. That reference is available for download.
Diese Lizenz sowie das Original vom Herbst 2001 bilden die Grundlage der Übersetzung
Es wird darauf hingewiesen, dass sich die Lizenz des englischen Originals inzwischen geändert hat.
Für die deutsche Übersetzung:
© 2002 Jürgen Katins
Der Copyright-Eigner stellt folgende Lizenzen zur Verfügung:
Nicht-freie Lizenz:
This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 or later (the latest version is presently available at http://www.opencontent.org/openpub/). Distribution of substantively modified versions of this document is prohibited without the explicit permission of the copyright holder. Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior permission is obtained from the copyright holder.
Freie Lizenz:
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1 or any later version published by the Free Software Foundation; with no Invariant Sections, with no Front-Cover Texts, and with no Back-Cover Texts. A copy of the license is included in the section entitled "GNU Free Documentation License".