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
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) endenthalten 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.