Private Klassen und Konstanten in Ruby
Marvin Gülker · 22.07.2018
Der Artikel beleuchtet verschiedene Facetten von privaten Konstanten und Klassen in Ruby.
Vor einiger Zeit hatte ich auf der Mailingliste ruby-de angekündigt, daß ich beabsichtige, eine Einführung in die Programmiersprache in deutscher Sprache zu schreiben. Das Vorhaben kommt gut voran und ich setze mich in diesem Zuge mit einigen Aspekten von Ruby auseinander, die man sonst eher selten beleuchtet. Um einen davon soll es in diesem Beitrag gehen: private Konstanten.
Einführung
Langjährige Rubyisten, d.h. solche, die Ruby schon in der seligen Zeit von Ruby 1.8 oder früher benutzt haben, stellen sich oft schnell auf den Standpunkt, daß es private Konstanten in Ruby nicht gäbe. Dafür spricht der Beweis des ersten Anscheins:
class MyClass private MY_CONSTANT = 3 end puts MyClass::MY_CONSTANT #=> 3
Konstanten werden nämlich von private
nicht
erfaßt. Bis einschließlich Ruby 1.9.2 war dies das Ende der
Untersuchung, was erklärt, weshalb das nachfolgend beschriebene
Feature eher selten Gegenstand der Erläuterung von Sichtbarkeiten in
Ruby ist, denn im Nachgang von Ruby 1.9 ging die Änderung vermutlich
in der weiterhin großen Aufregung um die schon mit 1.9.0 eingeführten
erheblichen Umwälzungen unter.
Es galt und gilt auch weiterhin damit der Grundsatz: Konstanten sind öffentlich, wenn keine besonderen Vorkehrungen getroffen werden. Dies gilt unabhängig vom aktiven Sichtbarkeitsmodus für Methoden.
private_constant
Beginnend mit Ruby 1.9.3 unterstützt Ruby die Möglichkeit,
Konstanten als privat zu markieren. Das funktioniert aber nicht über
private
(Stichwort: Rückwärtskompatibilität),
sondern über eine damals neu eingeführte Methode
Module#private_constant. Für das obige Beispiel heißt dies:
class MyClass MY_CONSTANT = 3 private_constant :MY_CONSTANT end puts MyClass::MY_CONSTANT #=> NameError
Damit sind seitdem echte private Konstanten in Ruby
möglich, wenn auch unabhängig von private
, das
weiterhin nur Methoden betrifft. Im Gegensatz dazu
kann private_constant
auch nicht ohne Argumente benutzt werden, um
allgemein die Sichtbarkeit umzuschalten. Dies hat schlicht keinen
Effekt:
class MyClass private_constant MY_CONSTANT = 3 end puts MyClass::MY_CONSTANT #=> 3
Ein Zugriff auf MY_CONSTANT
ist von außen weiterhin möglich. Wird
Ruby im Diagnosemodus (-w
) ausgeführt, wird diesbezüglich auch eine
Warnung ausgegeben.
warning: private_constant with no argument is just ignored
Bei private_constant handelt es sich nicht um ein Schlüsselwort,
sondern bloß um eine Methode (wie es für die Ruby-Entwicklung typisch
ist11
Vgl. nur refine
, using
, __callee__
, u.a.m.
). Von daher mag dieses Verhalten nicht überraschen. Führt man
sich allerdings vor Augen, daß private
selbst auch nur eine Methode
ist, ist das nicht unbedingt schlüssig. Man hätte es auch anders
gestalten können.
Private Klassen und Module
Nun muß man sich in Erinnerung rufen, daß in Ruby alle Bezeichner, die
mit einem Großbuchstaben beginnen, Konstanten sind; mit Ruby 2.6 wird
dies auch endlich für Großbuchstaben außerhalb des ASCII-Zeichensatzes
gelten22
Man munkelt, demnächst würden Emoji als Klassennamen
zugelassen.
. Nicht nur die klassischen, vollständig in Versalien
gehaltenen Bezeichner wie oben MY_CONSTANT
sind daher Konstanten,
sondern ausnahmslos auch alle mithilfe der regulären Schlüsselwörter
class
und module
definierten Klassen und Module, da die Grammatik
von Ruby verlangt, daß auf eines dieser Schlüsselwörter ein mit einem
Großbuchstaben beginnender Bezeichner folgen muß33
module lowercase
führt ebenso wie sein Klassenpendant zu
einem SyntaxError: „class/module name must be CONSTANT“
.
private_constant
kann auch auf diese „besonderen“ Konstanten
angewandt werden, sodaß es möglich ist, private Klassen und sogar
private Module zu definieren, die dem Zugriff von außen entzogen sind.
class MyClass class InnerClass end private_constant :InnerClass end MyClass::InnerClass.new #=> NameError
Es gibt immer wieder Fälle, in denen eine Abkapselung gewisser
„interner“ Klassen sinnvoll erscheint, und diese Fälle kann
man mit private_constant
jetzt gut lösen.
Der Zugriff auf die Konstante wird bei Anwendung des Auflösungsoperators :: überprüft. Ist demzufolge ein einzelnes Modul (Namespace) in einer längeren Kette als private Konstante markiert, sind — jedenfalls von außerhalb des markierten Moduls — auch die darunter liegenden Konstanten nicht erreichbar.
module Foo module Bar class Baz end end private_constant :Bar end Foo::Bar::Baz.new #=> NameError für Foo::Bar
Weil private_constant
aber nicht wie private
als
Sichtbarkeitsmodifikator benützt werden kann44
Dazu oben II.
, muß man den Namen der
Klasse unnötigerweise noch einmal wiederholen. Die Klassendefinition
mit class
und die Moduldefinition mit module
geben leider
nicht den Namen der Klasse zurück, wie das Schlüsselwort def
es für
Methoden tut, sodaß folgendes nicht möglich ist:
class MyClass private_constant class InnerClass end end
Unter Rückgriff auf Module#name kann man dieses Verhalten aber durch
Einfügung einer weiteren Zeile erreichen, denn class
und module
geben den letzten Ausdruck in der Definition zurück.
class MyClass private_constant class InnerClass name.split("::").last end end MyClass::InnerClass.new #=> NameError
Unter dieser Konstruktion leidet indessen die Leserlichkeit des Codes. Wenn überhaupt, dann sollte man sich dafür eine eigene Metamethode definieren. Um diese Metamethode universell einsatzfähig zu halten, empfiehlt sich die Extraktion in ein Mixin, das dann per #extend entsprechend eingebunden werden kann:
module InnerClassName def inner_name name.split("::").last end end class MyClass private_constant class InnerClass extend InnerClassName inner_name end end MyClass::InnerClass.new #=> NameError
Trotz alledem erscheint der Aufwand angesichts der bloß einmaligen
Wiederholung des Klassennamens ungerechtfertigt. Letztlich sehen
Konstruktionen wie private_constant class InnerClass
auch nicht wie
idiomatisches Ruby aus, egal, wie man ihre Wirksamkeit
herbeiführt. Auch steht für normale Konstanten abseits der Klassen- oder
Modulbezeichnungen ein anderer Weg ohnehin nicht zur Verfügung,
weshalb im Sinne der Symmetrie und Gleichbehandlung aller Konstanten
besser von dieser Technik Abstand genommen werden sollte.
Überwindung der Privatheit
Es ist nicht untypisch für Ruby, daß Beschränkungen mithilfe eines
etwas erhöhten Aufwands umgangen werden können55
Vgl. nur #send, das auch auf private Methoden zugreifen kann.
. Mit private_constant
markierte Konstanten machen da keinen Unterschied. Da Konstanten
lexikalisch aufgelöst werden und Rubys Klassen und Module
bekanntlich offen sind, kann man sich durch neue Öffnung der Klasse
bzw. des Moduls einfach Zugriff verschaffen.
class MyClass MY_CONSTANT = 3 private_constant :MY_CONSTANT end class MyClass puts MY_CONSTANT #=> 3 end
Daneben wird die Privatheitsmarkierung auch von der Reflexionsmethode
Module#const_get, die in der Metaprogrammierung Verwendung findet,
ignoriert. Ein public_const_get
wurde zwar vorgeschlagen, bislang
aber noch nicht implementiert.
class MyClass MY_CONSTANT = 3 private_constant :MY_CONSTANT end puts MyClass.const_get(:MY_CONSTANT) #=> 3
Endlich gibt es noch ein Gegenstück zu private_constant
, mit dem die
Auswirkungen dieser Methode beseitigt werden können: Module#public_constant66
Ein protected_constant
gibt es übrigens (derzeit, Ruby 2.5) nicht.
.
class MyClass MY_CONSTANT = 3 private_constant :MY_CONSTANT end class MyClass public_constant :MY_CONSTANT end puts MyClass::MY_CONSTANT #=> 3
Fazit
Das seit 1.9.3 in die Programmiersprache Ruby eingeführte Feature,
Konstanten mit private_constant
als privat zu markieren, kann zur besseren
Abkapselung interner Klassen, Module und sonstiger Konstanten benutzt
werden. Die ggf. notwendige Wiederholung des Klassen- oder Modulnamens
ist dabei in Kauf zu nehmen und kann bei „gewöhnlichen“ Konstanten
ohnehin nicht vermieden werden. Allerdings muß man sich bewußt sein, daß
wie oft in Ruby der durch private_constant
gewährte Schutz nicht
absolut ist und nicht nur mit Methoden der Metaprogrammierung, sondern
schon durch einfaches Neuöffnen der Klasse umgangen werden kann.
Valete.