next up previous contents index
Search Next: Das Ruby-Kochbuch Up: Objektorientiertes Programmieren in Ruby Previous: Design Patterns inklusive   Contents   Index

Projekt: Databinding Framework


\epsfig{height=24pt,file=images/ruby.eps}Hier beschreiben wir ein kleines OO-Programm, um den Umgang mit Klassen und Vererbung zu erläutern. Außerdem zeigt die sehr kompakte Methode parse sehr schön, wie elegant Ruby-Code sein kann, wenn man die vorhandenen Werkzeuge einsetzt.

Das Programm entstand, um den Datenbestand aus einem (sehr alten) COBOL-Programm in eine moderne Datenbank zu übertragen.

Unter Databinding versteht man, dass man Daten von einem Format wie XML in ein entsprechendes Objektmodell in Ruby umwandeln kann. Diese Objekte können beliebig in Ruby manipuliert werden, bevor sie wieder (automatisch) in dem ursprünglichen Format gespeichert werden.

Die Daten eines alten Geldmanagement-Systems liegen in einer Tabelle mit $\vert$ als Trennzeichen vor. In der Spalte Kategorie werden unterschiedliche Informationen gespeichert.

Flug NUMMER, VON, NACH
Hotel NAME, ORT
Zug VON, NACH

Die Datei ausgaben.txt enthält unter anderem folgende Zeilen:

Betrag|Datum   |Kommentar                  |Kategorie
800   |2.6.2001|miserabler Service.        |Flug, LH801, MUC, GVA
140   |3.6.2001|Nur ISDN Telefon.          |Hotel, Mont-Blanc, Genf
100   |3.6.2001|Hemingway war da auch schon|Zug, Lausanne, Vevey
700   |6.6.2001|Matrix zum 2. Mal gesehen  |Flug, LH201, MUC, JFK

Das Einlesen geschieht mit folgendem Code:

class Eintrag
  attr_accessor :betrag, :datum, :kommentar

  # formale Trennzeichen
  def Eintrag.colsep; "|"; end
  def Eintrag.subsep; ", "; end
  # diese Einträge sind überall vorhanden
  def initialize(betrag, datum, kommentar)
    @betrag, @datum, @kommentar =
      betrag, datum, kommentar
  end

  # describe wird von Unterklassen implementiert!
  def to_s
    [@betrag.to_s.ljust(10),
      @datum.ljust(10), @kommentar.ljust(28),
      describe].join(self.class.colsep)
  end

  # einheitliches Verfahren für die Ausgabe
  def describe(*info)
    info.join(self.class.subsep)
  end
end

# Einträge haben individuelle Eigenschaften
# und wissen, wie sie sich beschreiben.
class Hotel < Eintrag
  attr_accessor :name, :ort
  def initialize(b, d, k, *info)
    @name, @ort = info
    super(b, d, k)
  end
  def describe; super("Hotel", @name, @ort); end
end

class Reisen < Eintrag
  attr_accessor :von, :nach
  def initialize(b, d, k, *info)
     super(b, d, k)
     @von, @nach = info
  end
end

class Flug < Reisen
  attr_accessor :flugnr
  def initialize(b, d, k, *info)
    @flugnr = info.shift
    super(b, d, k, *info)
  end
  def describe; super("Flug", @flugnr, @von, @nach); end
end
class Zug < Reisen
  def describe; super("Zug", @von, @nach); end
end

class Databinder
  include Enumerable
  def initialize(classes)
    @data=Array.new
    @classes = classes
  end

  # Beschreibung im Text unten
  def parse(zeile)
    entries = zeile.strip.split(Eintrag.colsep)
    if (entries.size > 2)
      b, d, k, kat = entries.collect { | item | item.strip }
      typ, *info = kat.split(Eintrag.subsep)
      if @classes.has_key? typ
        @data << @classes[typ].new(b, d, k, *info)
      end
    end
  end

  # Datei einlesen und umsetzen
  def unmarshal(path)
    open(path).readlines.each_with_index { |l, i|
      parse(l) if i > 0
    }
  end

  # Liste in String umwandeln
  def marshal(); each { |e| puts e.to_s }; end

  # wir erzeugen ein each ...
  def each(); @data.each { |d| yield d }; end

  # ... und bekommen von Enumerable ein inject
  def summe
    inject(0) { |sum, e| sum += e.betrag.to_i }
  end

  # Datenstruktur zeigen
  def zeigen; p @data; end

  # neuen Eintrag aufnehmen
  def einfuegen(object); @data << object; end

  # Eintrag nach Datum löschen
  def loeschen(datum)
    @data.reject! { |e| e.datum == datum }
  end
end


# Anwendung
db = Databinder.new({ "Flug" => Flug, "Zug" => Zug,
  "Hotel" => Hotel })
db.unmarshal("ausgaben.txt")

puts "Daten zeigen:"
db.zeigen

db.marshal
puts "Summe der Ausgaben:\n#{db.summe}"

h = Hotel.new("80","7.6.2001",":-)","Seeblick","Lindau")
h.betrag = 90
db.einfuegen(h)
db.loeschen("2.6.2001")
db.marshal
puts "Summe der Ausgaben:\n#{db.summe}"

Die eigentliche Arbeit wird in der Methode parse erledigt. Dort werden die Zeilen zuerst in die einzelnen Spalten Betrag, Datum, Kommentar und Kategorie zerlegt und die Kategorie-Spalte wird weiter in ihre Einzelteile aufgespalten.

Die Zeilen

if @classes.has_key? typ
  @data << @classes[typ].new(b, d, k, *info)
end
enthalten ein bisschen Magie: Wenn der typ in der Liste der erlaubten Klassen enthalten ist, wird in der nächsten Zeile ein entsprechendes Objekt erzeugt und an die Liste angehängt. Nicht erwünschte Einträge werden stillschweigend weggelassen. Man könnte also durch Einschränkung der Klassenliste beim Erzeugen des DataBinders die vorhandenen Daten schon beim Einlesen filtern.

Daten zeigen:
[#<Flug: @kommentar="...", @nach="GVA", @datum="2.6.2001",
   @von="MUC", @betrag="800", @flugnr="LH801">,
 #<Hotel: @kommentar="...", @datum="3.6.2001", @ort="Genf",
   @betrag="140", @name="Mont-Blanc">,
 #<Zug: @kommentar="...", @nach="Vevey", @datum="3.6.2001",
   @von="Lausanne", @betrag="100">,
 #<Flug: @kommentar="...", @nach="JFK", @datum="6.6.2001",
   @von="MUC", @betrag="700", @flugnr="LH201">]
800 |2.6.2001|miserabler Service.         |Flug, LH801, MUC, GVA
140 |3.6.2001|Nur ISDN Telefon.           |Hotel, Mont-Blanc, Genf
100 |3.6.2001|Hemingway war da auch schon |Zug, Lausanne, Vevey
700 |6.6.2001|Matrix zum 2. Mal gesehen   |Flug, LH201, MUC, JFK
Summe der Ausgaben:
1740
140 |3.6.2001|Nur ISDN Telefon.           |Hotel, Mont-Blanc, Genf
100 |3.6.2001|Hemingway war da auch schon |Zug, Lausanne, Vevey
700 |6.6.2001|Matrix zum 2. Mal gesehen   |Flug, LH201, MUC, JFK
90  |7.6.2001|:-)                         |Hotel, Seeblick, Lindau
Summe der Ausgaben:
1030

Dieser Ansatz zeigt, dass das Einlesen eines "`freien"' Datenformats ziemlich aufwändig sein kann. Besser funktionieren sollte es, wenn die zu Grunde liegenden Daten in einem einheitlichen Format wie XML vorliegen.


next up previous contents index
Search Next: Das Ruby-Kochbuch Up: Objektorientiertes Programmieren in Ruby Previous: Design Patterns inklusive   Contents   Index
(C) 2002 by dpunkt.de, Armin Roehrl, Stefan Schmiedl, Clemens Wyss 2002-01-20