QVINTVS · SCRIBET

new ohne initialize

Gelegentlich gibt es ein scheinbar nicht ganz einfach zu lösendes Problem: Man hat eine Klasse, die in initialize bereits verschiedene Initialisierungen durchführt, möchte aber gleichzeitig eine andere Klassenmethode bauen, die anders initialisiert.

Was sich nach einem scheinbar unlösbaren Problem anhört, ist in Ruby kein Problem. Wie wir wissen, bietet Ruby zwar keine Methodenüberladung an, abere andere praktische Möglichkeiten wie die variable Argumentanzahl mithilfe des Splat-Operators *. Wenn man nun aber grundverschiedene Methoden hat, nützt der einem auch nichts, weil die logische Verbindung zwischen Methodenname und Tätigkeit nicht gegeben ist; wenn etwa gar kein neues Objekt erstellt wird, ist new wohl auch der falsche Methodenname.

Betrachten wir etwa folgendes Beispiel:

class Email
  attr_accessor :body
  def initialize(body)
    @header = {}
    @body = body
    @important_internally_used_ivar = 37
  end
  
  def [](name)
    @header[name]
  end
  
  def []=(name, val)
    raise(ArgumentError, "Incorrect header {name}!") unless VALID_EMAIL_HEADERS.include?(name)
    @header[name] = val
  end
  
  def to_s
    # Baue RFC-konforme E-Mail
  end
end

new_mail = Email.new("Hi!")
File.open("mymail.eml", "w"){|f| f.write(new_mail.to_s)}

Wollte man jetzt eine bereits vorhandene, RFC-konforme E-Mail parsen, wäre man mit new als Methodennamen falsch bedient, denn man will ja keine neue E-Mail erstellen. Schnell ist man dann bei folgendem Code:

class Email
  def self.from_file(path)
    hsh = parse(File.read(path))
    email = new("")
    email.body = hsh[:body]
    hsh[:header].each_pair{|k, v| email[k] = v}
    email.instance_variable_set(:@important_internally_used_ivar, 58)
    email
  end
end

Eine meiner Meinung nach elegantere Lösung ist es, mithilfe von Class#allocate ein völlig blankes Objekt zu erstellen und dann alle Initialisierungen »noch einmal« zu machen:

class Email
  def self.from_file(path)
    email = allocate
    hsh = parse(File.read(path))
    email.instance_eval do
      @header = hsh[:header]
      @body = hsh[:body]
      @important_internally_used_ivar = 58
    end
    email
  end
end

Durch die Kombination allocate/instance_eval ist sichergestellt, dass keine unerwünschten Initialisierungen durch #initialize enstehen. Auf der anderen Seite kann man das aber auch als Verletzung von DRY sehen; ich denke, sparsam eingesetzt kann das aber sehr praktisch sein.

Valete.