About Ruby, Rails und 'drum und 'dran

This page contains an archive of all entries posted to /blog/wvk in the Ruby, Rails und 'drum und 'dran category. They are listed from oldest to newest.

Philosophisches und anderer Schmalz is the previous category.

Many more can be found on the main index page or by looking through the archives.

Powered by
Movable Type 3.31

Main

Ruby, Rails und 'drum und 'dran Archives

October 18, 2007

Von befreienden Einschränkungen

Mittlerweile wurde endlich ein klarer(er) Rahmen, in dem ich mich mit der Diplomarbeit befassen soll, festgelegt. Es bleibt sozusagen recht wenig vom ursprünglichen Umfang übrig, was aber nicht bedeutet, dass ich nun nichts mehr zu tun hätte :-). Die Arbeit wird sich nun auf die nachfolgenden Punkte beschränken. Grob gesagt wären das:
- Kernbereich (Framework, Modul-API; Modul: core)
- Benutzerverwaltung (Versch. Benutzertypen, Gruppen, Rollen, Rechte. Module: domain, email, auth)
- Prototypartige Implementierung eines optionalen Moduls (http)
- Installationsroutinen (sehr wahrscheinlich über Rake)

Viele Überlegungen zur Implementierung machen m.E. nur mit Sicht auf das Gesamtsystem Sinn, darum werde ich weiterhin beim Erarbeiten des Datenbankschemas weitestgehend alle beteiligten Entitäten mit einbeziehen, auch wenn diese in der Diplomarbeit nicht konkret behandelt werden.

Als nächstes sollte ich mir nun ein paar Gedanken zur Modulverwaltung machen, sprich wie diese in die Applikation integroert werden sollen. Ein Blogientrag zu dem Thema ist noch immer in Arbeit...

ActiveRecord für Fortgeschrittenere

Neulich habe ich im Buch "Agile Web Development with Rails" eine ganz nette Sache entdeckt, die mir für die Benutzer-Rechteverwaltung sehr gelegen kommt. Ich habe die Entitäten User, Group und AccessRight, wobei sowohl Benutzer als auch Gruppen beliebig viele Zugriffsrechte haben können (habtm). Üblicherweise würde man zur Erstellung der Rechte-Beziehungen einfache Join-Tables mit zwei Fremdschlüsseln verwenden (users_access_rights(user_id, access_right_id)). Da zusätzlich aber gespeichert werden soll, wer wann von wem welche Rechte zugewiesen bekommen hat, macht es Sinn, eine zusätzliche Entität UserAccessRight (dito für Gruppen) zu erstellen. Klassischerweise würde das dann so aussehen:


class User < ActiveRecord::Base
  belongs_to :user_access_right
end
# dito für Group...

class AccessRight < ActiveRecord::Base
  belongs_to :user
end

class UserAccessRight < ActiveRecord::Base
  has_one :user
  has_one :access_right
end

# Liste alle Rechte von Benutzer 1 auf:
user = User.find 1
user.user_access_rights.each do |acc|
  p acc.right.name
end

# darf der Benutzer also Benutzer hinzufügen?
if user.user_access_rights.find {|r| r.access_right_name == 'add_user'}
  p "Benutzer #{user.name} darf Benutzer hinzufügen."
end

Das funktioniert zwar wunderbar, aber bei genauerem hinsehen ist es bestenfalls "etwas unhübsch", um immer über die UserAccessRight-Entität zuzugreifen, wenn man eigentlich mit dem AccessRight selbst arbeiten möchte -- denn eigentlich soll ja weiterhin lediglich eine HABTM-Beziehung zwischen User und AccessRight bestehen, die aber so aufgelöst wird.

Dafür gibt's aber in Rails die folgende Lösung:


class User < ActiveRecord::Base
  has_many :user_access_rights
  has_many :access_right,
          :through => :user_access_right
end

class AccessRight < ActiveRecord::Base
  has_many :user_access_rights
  has_many :access_rights,
          :through => :user_access_rights,
          :unique => true
end

class UserAccessRight < ActiveRecord::Base
  belongs_to :user
  belongs_to :access_right
end

# Liste alle Rechte von Benutzer 1 auf:
user = User.find 1
user.rights.each do |acc|
  p acc.name
end

# darf der Benutzer also Benutzer hinzufügen?
if user.rights.find {|r| r.name == 'add_user'}
  p "Benutzer #{user.name} darf Benutzer hinzufügen."
end

So kann man also direkt, wie bei einer klassischen HABTM-Beziehung, auf die Rechte eines Benutzers zugreifen. Zusätzlich stehen aber auch unmittelbar die Meta-Informationen zu den einzelnen Rechten (wann und von wem verliehen) zur Verfügung.

Also ich finde das toll ;-)

Als Nächstes muss ich nur mal sehen, wie man halbwegs elegant an die Rechte herankommt, die einem Benutzer über seine Gruppenzugehörigkeiten verliehen werden. Zur Not tut's natürlich eine einfache Suchschleife über alle Gruppen und deren Rechte, oder die Rechte werden bei der Objektinitialisierung geladen, oder alles wird in einer nifty-tricky-magic Funktion gekapselt mit deren Innenleben sich lieber keiner auseinandersetzen sollte ;-)

November 12, 2007

ActiveRecord::BaseWithoutTable

Gestern brauchte ich (für ein Mailformular) so etwas wie ActiveRecord, aber ohne Tabelle. Wozu das, mag man fragen. Ganz einfach: ich brauchte einfache Validierung der eingebenen Formulardaten und demnächst auch Aggregation. Nach kurzem googlen nach "ActiveRecord without table" fand ich ActiveRecord::BaseWithoutTable. Dieses Plugin liefert einem alles, was ActiveRecord hat, nur eben ohne eine Tabelle für eine Entity aufbauen zu müssen. Toll, was? :)

Ich denke, dass dieses Plugin auch genau das ist, was ich bei meinen Apache-Configfiles einsetzen werde -- man erinnere sich -- auch da wollte ich "etwas wie ActiveRecord" einsetzen, mit Validierung, Aggregierung usw. Nun, wir kommen dem Ziel ein wenig näher...

Rails Helper v0.2

Soo, gerade ist Version 0.2 der Rails-helper-GUI fertig geworden. So schaut das Ding nun aus:

snap20.png

Folgendes hat sich geändert:

  • neues Tab mit RAKE-Buttons. Hier finden sich die meistverwendeten Rake-Aktionen db:migrate (optional mit Schemaversion), db:fixtures:load, db:schema:dump, db:test:prepare, log:clear, tmp:clear sowie test.
  • stdout und stderr werden in der eingebauten Konsole angezeigt
  • statt die Applikation vom Arbeitsverzeichnis aus aufrufen zu müssen, kann (muss) man nun das Arbeitsverzeichnis (Rails Root) explizit eingeben.
  • wenn nicht in das Rails Root-Verzeichnis gewechselt werden kann (z.B.weil Pfad ungültig/nicht gefunden), wird abgebrochen und eine Fehlermeldung erscheint.

Herunterladen kann man das Script hier: Download Rails Helper v0.2 Bitte daran denken, dass man zum Ausführen den KDE Kommander braucht (Aufrufen mit kmdr-executor rails-helper_0.2.kmdr).

Bugreports und Erweiterungswünsche sind stets Willkommen!

Nachtrag für Philipp ;-)

Ja, man kann die Buttons auch anders aussehen lassen, das ist ja das schöne an KDE ;-) schaustu, staunstu (über die Sinnhaftigkeit dieser Themes brauchen wir uns jetzt nicht streiten): snap21.png

snap22.png

Nachtrag:

Mittlerweile gibt Version 0.2.1 mit folgenden Fixes:

  • Tab-Reihenfolge korrigiert
  • Einblenden der Kommando-Felder bei "Scaffold" und "Controller" wiederhergestellt (muss irgendwie verloren gegangen sein bei v0.2

Der Download-Link (s.o.) ist immer noch der selbe.

December 9, 2007

Böser Bug in Rails 1.2.6

Das Serialisieren von Sitzungs-/Transaktionsobjekten zur Speicherung in der Datenbank mittels YAML ist in Rails eigentlich super-einfach:

class ConsolvixTransaction < ActiveRecord::Base
  serialize :data, Hash
  # ...

  def [](key)
    self.data[key]
  end

  def []=(key, value)
    self.data[key] = value
  end
end

Obiger Code veranlasst Rails, die data-Spalte vor dem Speichern zu serialisieren und nach dem laden wieder zu deserialisieren. Dann kann ich während eine Transaktion aktiv ist einfach über

@transaction[:address] = Address.new(params[:address])

z.B. ein neues Adressenobjekt in der Transaktion ablegen und theoretisch später wieder darauf zugreifen. Aber weit gefehlt, denn Rails deserialisiert das Objekt nachher nicht als Address-Objekt, sondern als YAMLObject.

Eine kurze Internetrecherche ergab, dass das tatsächlich ein Bug in Rails ist: http://dev.rubyonrails.org/ticket/7537 bzw. http://dev.rubyonrails.org/ticket/8933

Die bei Ticket Nr. 7537 vorgeschlagene Lösung, den folgenden Code in die environment.rb einzufügen, funktioniert zwar so weit, aber dass ich nun für jede theoretisch mögliche Entität, die einmal in einer Transaktion vorkommen könnte, ein require hinzufügen muss, ist mehr als nur hässlich.

require 'address'
require 'client'
require 'domain'
require 'email_address'
require 'system_user'
require 'user'
require 'email_account'
require 'ftp_account'
require 'hosting_account'

YAML.add_domain_type("ActiveRecord,2007", "") do |type, val|
  klass = type.split(':').last.constantize
  YAML.object_maker(klass, val)
end

class ActiveRecord::Base
  def to_yaml_type
    "!ActiveRecord,2007/#{self.class}"
  end
end

Kennt jemand anderes von euch dieses Problem auch? Existiert das noch in Rails 2.0 oder einer späteren Version als 1.2.6?

December 13, 2007

Rails Helper v0.3

Server Tab
Server starten/stoppen
Migration Fix
rake db:migrate VERSION=... geht nun
Stats Button
Button für Code-Statistik

Es hat sich wieder etwas getan um das numehr zum dritten Male hochgelobte Rails-Helper-Applikatiönchen, welches soeben in seiner 0.3. Version erschienen ist.

Erweiterungen

  • ein neues Tab zur Kontrolle des Webrick-Servers ist hinzugekommen. Zur Zeit kann man damit den Server nun nur starten oder stoppen. Eigetnlich soltle man das auch von Kdevelop aus bewerkstelligen können, aber das stoppend es Servers klappt dir nicht so wie vorgesehen -- daher diese eigene Implementierung.
  • im Tab "Rake" ist zum Leidwesen derer, die nichts von LOCs halten ( ;-)), ein weiterer Button zur Anzeige von Code-Statistiken (rake stats) hinzugekommen.
  • Die Terminalausgaben assen sich nun auf Knopfdruck löschen (noch nicht in den Screenshots abgebildet)

Bugfixes:

  • das Auswählen einer Migrations-Version != 0 funktioniert jetzt auch tatsächlich (hatte vorher einen Syntaxfehler wegen "+" vor der Versionsnummer gegeben) (Beweis siehe Screenie ;-))
  • die Schriftart in den Termninals wurde auf Monospace gesetzt, um die Ausgaben etwas Leserlicher zu gestalten.

Herunterladen kann man sich die neue Verson hier: Download Rails Helper v0.3. Zur Erinnerung, ausführen kann man das Script nur mit dem Kommander-Executor.

January 4, 2008

Auf zwei Schienen fährt man besser...

...so dachte ich mir, und stand vor der Entscheidung, ob ich nun den gleichen kapitalen Fehler begehen sollte, den ich schon während der Bachelorarbeit (nur damals mit PHP) begangen hatte: Sollte ich mitten während der Entwicklung einer Software das Framework aktualisieren? Bislang arbeitete ich ja mit Rails 1.2.6 und war gut zufrieden, doch dann wurden ein paar neue Features (wie eingebautes HTTP Basic Auth... Blog-Artikel dazu ist in Arbeit!) doch zu verlockend.... und ich tat es.

Ja, ich fahre nun auch auf Rails 2.0.

Und der Umstieg gelang erstaunlich reibungslos! Meine Schritte waren:

  • Lesen des Artikels mit den Änderungen in Rails 2.0 aus dem InnoQ-Wiki
  • Deinstallieren von Rails 1.2.6 (über APT, denn ich hatte Rails nicht über Gem installiert)
  • Installieren von Rails 2.0 (über Gem, denn Rails 2.0. gab's noch nicht in meinen Apt-Repos)
  • Deployen einer plain vanilla Rails App irgendwohin
  • Kopieren von /config/{boot, environment, routes, initializers}, /script/* und /public/{dispatch.*, *.html} von vanilla nach Consolvix
  • Korrigieren der Pfadangaben zu den includes in dispatch.fcgi, denn sonst spackte Apache
  • Umbenennen aller Views mit u.g. Script, das vielleicht auch jemand anderem irgendwann eine Hilfe sein könnte.

#!/usr/bin/env ruby
# rename all *.rhtml files within the /app/views directory ro *.html.erb
# Author: WvK <wvk@consolving.de>
require 'fileutils'
Dir.new('.').each do |entry|
  if File.directory? entry and not entry[0..0] == '.'
    Dir.new("./#{entry}").each do |file|
      if /.*.rhtml/.match file
        old_path = "./#{entry}/#{file}"
        new_path = old_path.gsub /.rhtml/, '.html.erb'
        if FileUtils.mv old_path, new_path
          p "mv #{old_path} -> #{new_path}"
        else
          p "ERROR RENAMING #{old_path} to #{new_path}!"
        end
      end
    end
  end
end

Tja, und was soll ich sagen: läuft! Bis auf die Sache mit der pagination, derer ich mich noch nicht angenommen habe, läuft alles weitestgehend tadellos. Ich bin froh, dass ich ab und an mal die Deprecation-Warnungen in /log/development.log zu Herzen genommen hatte!

Nun wo der Schritt also vollbracht ist, ist Consolvix bestimmt besser als je zuvor den Änderungen, welche die Zukunft bescheren wird, gewachsen ;-)

January 15, 2008

Neues System, neue Probleme

Mal wieder ging ein potentiell produktiver Tag ohne ein produktives Ergebnis zu Ende, weil mein Apache nicht mit den Rails-Anwendungen spielen wollte. mod_cgi meldete das übliche "premature end of script headers", was im Wesentlichen nichts weiter aussagt als "irgentwas stimmt da nicht". mod_fcgid hatte anschienend noch viel weniger Lust, denn eine Instanz nach dem Anderen hängte sich einfach nach einem "unexpected Signal 11" auf, später gab's dann auch noch lustige Segfaults dazu. Neuinstallieren von Apache und Modulen und trallala brachte alles nichts, auch die Schreib-, Lese- und Ausführrechte waren alle korrekt gesetzt FRUST.

Schließlich und endlich rang ich mich dann dazu durch, den Apachen mitsamt des Fcgid-Moduls selber zu kompilieren, denn vielleicht gab es ja tatsächlich Probleme, weil der eine oder andere Teil noch im 32 Bit-Modus lief. Glücklicherweise ist selber bauen und dann installieren unter Debian sehr sauber und einfach:

$ cd /usr/src
$ apt-get source apache2 libapache2-mod-fcgid
  ... [ratter] ...

das holt erstmal die ganzen Sourcen. Anschließend wird kompiliert:

$ cd apache2-2.2.6
$ fakeroot debian/rules binary
  ... [ratter ... ratter ... ratter] ...

dito für mod_fcgid. Im Verzeichnis /usr/src liegen anschließend einige .debs:

apache2_2.2.6-3_all.deb
apache2.2-common_2.2.6-3_amd64.deb
apache2-dbg_2.2.6-3_amd64.deb
apache2-doc_2.2.6-3_all.deb
apache2-mpm-event_2.2.6-3_amd64.deb
apache2-mpm-perchild_2.2.6-3_all.deb
apache2-mpm-prefork_2.2.6-3_amd64.deb
apache2-mpm-worker_2.2.6-3_amd64.deb
apache2-prefork-dev_2.2.6-3_amd64.deb
apache2-src_2.2.6-3_all.deb
apache2-threaded-dev_2.2.6-3_amd64.deb
apache2-utils_2.2.6-3_amd64.deb

Installiert werden sollten jedoch nicht alle (genauer gesagt geht das gar nicht). Es reicht, common, mpm-worker, utils und fcgid zu installieren.

# dpkg -i apache2.2-common_2.2.6-3_amd64.deb \
   apache2-mpm-prefork_2.2.6-3_amd64.deb \
   apache2-utils_2.2.6-3_amd64.deb

wenige Minuten später ... siehe da ... der Apache startet (OK, das hätte ich auch erwartet) und Rails rennt wieder. Geschafft!

Vielleicht noch ein paar Worte zu den verschiedenen MPM-Paketen oben: Seit der Version 2.X besteht der Apache aus zwei Teilen, einmal einem common-Paket und einem mpm-*-Paket. Die eigentliche Arbeit, nämlich das Bedienen der Clients, macht je ein "Arbeiter". Da der Apache bekanntlich durchaus 1000+ Anfragen pro Sekunde verarbeiten können muss, versteht sich von selbst, dass da irgendwie parallele Prozesse am Werk sein müssen. Und tatsächlich, "MPM" steht für Multi-Processing Module.

MPM-PREFORK ist das klassische Apache-Worker-Modell, wo sich einfach beim Start des Servers schon mehrere Apache-Instanzen im Speicher einnisten. Jede Instanz für sich genommen arbeitet singlethreaded. Dieses "Thread"-Modell ist sehr ausgereift und meines Wissens neben MPM-ITK das einzige, was mit Rails (und auch PHP5) zusammenarbeitet.

MPM-ITK erlaubt es, eine eigene UID und GID pro VirtualHost anzulegen, also ähnlich wie mod_suexec es für PHP tut. Das Klingt auf jeden Fall für meinen Server schonmal sehr interessant...

Die anderen MPM-Modelle sind entweder nicht sonderlich ausgereift oder sonstwie ungeeignet, deswegen werde ich nicht weiter drauf eingehen und mich einfach freuen, dass der alte Indianer wieder läuft :-)

INI-Files

Heute Nacht saß irgendwann wie betäubt vor meinem Quelltext und machte ein apathisches Auditing, bis ich auf die Stelle stieß, wo ich im TracEnviironment-Model die Trac.ini verarbeite. Wie bereits anderswo beschrieben, werden die Werte aus der ini-Datei als Attribute der jeweiligen TracEnvironmant-Instanz abgelegt, damit ich darauf zugreifan kann wie auf ein ActiveRecord-Objekt. Das übernimmt diese Methode:

def parse_config_file!(file=nil)
  config  = {}
  section = ''
  begin
    file = File.new(file||self.config_file)
    file.each_line do |line|
      if /^[([a-z0-9_]*)]$/.match line
        section = $1
      elsif /^\s*([a-zA-Z0-9_]*)\s*=\s*(.*)$/.match line
        self.send "#{section}__#{$1}=", $2
      end
    end
  rescue Errno::ENOENT
  end
  config
end

Zurückschreiben geht ähnlich, halt einfach den gleichen Weg rückwärts. Das funktioniert alles Prima so weit udn ich denke auch nicht, dass ich das so bald ändern werde. Dennoch packte mich die Neugier, ob es nicht vielleicht ein Rails-Plugin oder ein Gem zurm INI-Dateihandling gibt. Und siehe da: das inifile-Gem tut genau das: https://rubyforge.org/projects/inifile. Intern werden die "Sections" als Hash gespeichert und auf die Sections wird ebenfalls wie ein Hash zugegriffen.

Alles sehr nett. Viel Spaß damit, sollte jemand das mal brauchen...

January 20, 2008

Von etwas komplizierteren Beziehungen

Wenn A viele B hat, und C viele B hat, kann C auch viele A haben, wenn C :through benutzt. Wenn A aber viele B und B viele C hat, außerdem C zu vielen B gehört, dann kann C nicht auch viele A haben, selbst wenn B :through benutzt.

Ich wollt's nur mal erwähnt haben, sollte jemand von euch das auch mal probieren wollen.

Wie Bahnhof? Gut, dann nochmal kurz:

:through geht nicht bei hasandbelongstomany-Assoziationen.

Mein Problem:

class AccessRight
  # ...
end

class AccessRightGrant
  belongs_to :acces_right
  belongs_to :subject,
             :polymorphic => true
end

class SystemGroup
  has_many :access_right_grants,
           :as => :subject
  has_many :access_rights,
           :through => :access_right_grants
  has_and_belongs_to_many :users
end

class User
  has_and_belongs_to_many :system_groups
  has_many :access_right_grants,
           :as => :subject
  has_many :user_access_rights,
           :class_name => 'AccessRight'
           :through => :access_right_grants
  has_many :access_rights,
           :class_name => 'AccessRight',
           :through => system_groups
end

... geht also NICHT.

Was ich erreichen möchte, ist dies: User hat AccessRights, SystemGroup hat AccessRights, User hat alle AccessRights, die SystemGroup auch hat. User.access_rights soll also alle , nicht nur des User's AccessRights zurückliefern (in obigem Code sollten erstmal nur die AccessRights der Gruppen geladen werden, nicht alle). AccessRightGrant ist die Linking Table zwischen AccessRight und User/SystemGroup, wobei letztere über die polymorphische subject-Spalte gelinkt werden.

Dem mit o.g. Code generierten SQL nach zu urteilen, liegt das Problem bei der HABTM-Beziehung. Bestätigt hat das ein Tauchgang in den Rails-Source, der übrigens mit seinen n Metaprogrammier-Ebenen mehr als faszinierend und beeindruckend ist, wenn man sich mal etwas Zeit für ihn nimmt. Bei einer normalen has_many-Beziehung zwischen User und SystemGroupkönnte es funktionieren, ausprobiert habe ich das jedoch nicht.

Aber es lässt sich für alles eine Lösung finden und bis ich hierfür eine elegante Lösung gefunden habe, werde ich einfach ganz skrupellos brute-force-Methoden wie "lade alle Rechte und durchsuche das Array" benutzen. Tja, Rails, das haste nun davon :-)

January 21, 2008

Ein weiterer Rails-Bug

Vielleicht erinnert sich der eine oder andere aus der Schnittmenge der treuen Leser dieses Blogs und der regelmäßig bei unserer Diplomandenrunde in Dortmund Anwesenden noch daran, dass wir "damals" erwähnt hatten, dass es manchmal, aber nicht immer, in Rails zu einer Exception kommt, wenn man z.B. sowas hier probiert:

@client.user.system_groups.access_rights

obwohl alles ganz sicher korrekt mit has_many usw. definiert wurde. Ich werde hier nichts breittreten, was mich grob geschätzt die letzten 4 Stunden und viel mehr Nerven als diese Stunden Sekunden haben, gekostet hat, sondern es in einem einfachen Satz zusammenfassen:

NIEMALS in Join-Tables einen Primärschlüssel namens 'id' verwenden!!

das Problem ist, dass bei einem Join wie diesem:

User.find(17).system_groups

dieses SQL generiert wird:

SELECT * FROM `system_groups` 
  INNER JOIN groups_users 
    ON system_groups.id = groups_users.group_id 
WHERE (groups_users.user_id = 17 )

Na, wem fällt's auf? genau, da steht SELECT * FROM ..., und nicht, wie es sich gehört, SELECT system_groups.* .... Damit tritt der Spaltenname id nämlich zweimal im Resultat auf (einmal von der Gruppentabelle und einmal von der Join-Table), wobei letzten Endes die ID von der Join-Table von Rails verarbeitet wird und dann als FALSCHE system_group_id eingesetzt wird.

Nach dem Entfernen der id Spalte aus groups_users lief alles wie erwartet.

Man mag mich bei Gelegenheit dafür steinigen, dass ich in Join-Tables eine künstliche ID-Spalte verwende, aber Rails' Verhalten bezeichne ich in diesem Fall als schlichtweg falsch. I hereby declare it a bug.

January 25, 2008

Rubys Objektmodell

OK, was ich jetzt hier schreibe ist nichts neues und auch schon auf viel bessere Art und Weise von Why the Lucky Stiff beschrieben worden. Jedenfalls trieb mich ein Bug in Consolvix dazu, mich nochmals intensiver mit dem Thema Metaprogrammierung und dem Ruby-Objektmodell auseinanderzusetzen. Es folgen ein Paar Notizen.

  • Objekte in Ruby können nur Variablen aufnehmen -- Ein Ruby-Objekt hat keine Methoden
  • Objekte sind in erster Linie dies: Objekte. Erst in zweiter Linie sind sie Instanzen einer Klasse. Auch Klassen sind Objekte (ABER auf C-Quellcode-Ebene sind Object und Class zwei verschiedene Structs -- ich halte diese Information für wichtig, wenn man wirklich blicken will, warum diese Class-Objekt-Abhängigkeit keine unendliche Rekursion mit sich bringt)
  • Objekt-/Instanz-Methoden befinden sich in der Klasse, von der sich das Objekt ableitet. Eine Klasse ist nicht ein statisches, abstraktes Dingda, sondern ein Singleton-Objekt. Ein Objekt, das (Klassen-)Variablen UND Methoden aufnehmen kann. Eine Klasse eben. (Und hier würde dann die Rekursion einsetzen ;-))
  • Klassenmethoden befinden sich -- nein, nicht in der Klasse, die ja ein Objekt ist, sondern in der Klasse, die meistens Metaklasse genannt wird.
  • Klassen vererben ihre Methoden ihren Unterklassen. Somit steht, wenn man eine Klasse um eine neue Methode bereichert, sofort sämtlichen Unterklassen und sämtlichen Instanzen der Klasse diese Methode zur Verfügung. Wenn man aber einem einzelnen Objekt an dieser Stelle eine andere Implementierung dieser Methode geben möchte, so kann man dem Objekt diese Methode über die Metaklasse dieses Objektes zur Verfügung stellen. Methoden werden zuerst in der Metaklasse dann erst in der Klassenhierachie gesucht.
  • Das Konzept der Vererbung erweitert eine Klassenhierachie vertikal, während Module die Klassenhierachie horizontal erweitern -- man könne also sagen, dass das Konzept der Metaklassen die Hierachie in der tiefe erweitern -- aber das klingt gleich wieder so esotherisch...
  • Metaklassen können, da diese wiederum Objekte sind, selbst auch wieder Metaklassen enthalten. Eine Veränderung der Metaklasse einer Metaklasse einer Klasse zieht jedoch keinerlei Konsequenzen für die Klasse nach sich. Außerdem ist es sehr unwahrscheinlich, dass von diesem Konzept jemals jemand Gebrauch machen wird (OK, ich werde meinen kranken Geist darauf ansetzen, in einer freien Minute etwas passendes zu finden ;-))

Folgendes ER-Diagramm hab ich nach meinem Verständnis des Sachverhaltes zusammengestellt. Wenn jemand darin einen grundlegenden Fehler entdeckt, bitte sofort melden!

Dieses Klassendiagramm habe ich nach dem ASCII-Diagram aus einem (vermutlich bekannten) Post in der Ruby-Talk-Mailingliste gebastelt. Es stellt die Abhängigkeit der Klassen und Objekte untereinander dar. Wie man sieht, erbt letztendlich alles von Object. Die Klassen mit eingeklammerten Namen sind die jeweiligen Meta-Klassen (bzw. Singleton-Klassen-Instanzen...).

RubyObjectModelInheritances.png

Ich finde, dass das Bild etwas klarer wird, wenn man die Vererbung durch eine gerichtete Beziehung der Objekte untereinander ersetzt. super zeigt somit immer auf das Objekt der Elternklasse während self auf das Objekt der Singleton-Klasse zeigt.

RubyObjectModelReferences.png

So hat man im Wesentlichen die Abbildung des Ruby-Objektmodells auf C-Ebene vor sich: Vererbung und "magische Meta-Dingsda" sind nichts weiter als Structs, von denen einige lediglich Variablen, andere auch Methoden referenzieren können... der ganze Zauber gelüftet, aber eine Menge mehr Klarheit geschaffen :)

Zu dem ganzen ist anzumerken, dass nicht zu jedem Objekt a priori eine Singletonklasse existiert -- das würde unendlich viel Speicher erfordern. Eine Singletonklasse wird nur dann erstellt, wenn sie explizit angefordert wird, und zwar über

class Foo
  # ...
end

obj = Foo.new

class << obj
  # hier sind wir im Kontext der Metaklasse von obj!
end

class << Foo
  # hier sind wir im Kontext der Metaklasse von Foo!
end

class Foo
  class << self
    class << self
      # im Kontext der Metaklasse der Metaklasse von Foo ;-)
    end
  end
end

Alles klar? Nicht? Dann hilft nur lesen des o.g. Artikels von Why the Lucky Stiff oder sich einfach mit anderen Dingen beschäftigen...

January 27, 2008

Ein paar Worte zu Modulen in Ruby

Als Ergänzung zu meinem letzten Eintrag zum Thema "Rubys Objektmodell" folgen ein paar Notizen dazu, wie sich Module in das Gefüge einfügen (müssen sie ja irgendwie, sonst wäre es kein Gefüge ;-))

  • Wird ein Modul mittels include eingebunden, werden alle Modulmethoden zu Instanzmethoden der einbindenden Klasse.
  • Wird ein Modul mittels extend eingebunden, werden alle Modulmethoden zu Klassenmethoden der einbindenden Klasse.
  • es ist (in Rails) durchaus üblich, ein Modul Foo zu definieren, welches dann mit include Foo eingebunden wird, sowie ein Untermodul Foo::ClassMethods, welches mittels extend Foo::ClassMethods eingebunden wird. Diese Praxis habe ich einfach mal so übernommen.

So viel zu den Methoden. Doch was ist mit den Variablen? Wie wir alle wissen, werden Klassenvariablen weitervererbt, Instanzvariablen jedoch nicht (irgendwie logisch...). Aber bei Modulen...?

  • Da ein Modul nicht instanziiert werden kann, kann es auch keine Instanzvariablen (@foo) haben -- ganz einfach. Wird ein Modul mittels include eingebunden, müssen alle @foo's also Instanzvariablen der Objekte werden; wird es mit extend eingebunden, sind es eben Instanzvariablen der Klasse (und NICHT etwa Klassenvariablen oder so...)
  • Klassenvariablen (@@bar) werden bei include einfach übernommen -- sprich, was im Modul eine "Klassen"variable war, wird auch in (den Instanzen) der einbindenden Klasse als Klassenvariable erhalten bleiben. Bei extend jedoch sind alle im Modul definierten Klassenvariablen nur für die im Modul definierten Methoden erreichbar! Alle Versuche, aus dem Kontekt der Klasse (oder einer ihrer Instanzen) an sie heranzukommen, sind bei mir bislang gescheitert.

An einem Beispiel zeigt sich so was immer schön:

module Foo
  @@truth = 42

  def say_the_truth
    p @@truth
  end
end

class Bar
  include Foo
end
Bar.new.say_the_truth # 42 *SMILE*

class FooBar < Bar
  def say_something
    p @@truth
  end
end
FooBar.new.say_something # 42 *SMILE*

class Baz
  extend Foo

  def say_something
    p @@truth
  end
end
Baz.say_the_truth # 42 *SMILE*
Baz.new,say_something # PENG! WUMMS! "NameError: uninitialized class variable @@truth in Baz" ... *Autsch*

Alles klar?