QVINTVS · SCRIBET

Private Klassen und Konstanten in Ruby

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.

I. 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.

II. 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 ist1). 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.

III. 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 gelten2. 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ß3.

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 kann4, 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.

IV. Überwindung der Privatheit

Es ist nicht untypisch für Ruby, daß Beschränkungen mithilfe eines etwas erhöhten Aufwands umgangen werden können5. 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_constant6.

class MyClass
  MY_CONSTANT = 3
  private_constant :MY_CONSTANT
end
class MyClass
  public_constant :MY_CONSTANT
end
puts MyClass::MY_CONSTANT #=> 3

V. 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.

  1. Vgl. nur refine, using, __callee__, u.a.m. 

  2. Man munkelt, demnächst würden Emoji als Klassennamen zugelassen. 

  3. module lowercase führt ebenso wie sein Klassenpendant zu einem SyntaxError: „class/module name must be CONSTANT“ 

  4. Dazu oben II

  5. Vgl. nur #send, das auch auf private Methoden zugreifen kann. 

  6. Ein protected_constant gibt es übrigens (derzeit, Ruby 2.5) nicht.