RubyistたちのDRY症候群との戦い Rubyで自動的にインスタンス変数をセットする
DRY症候群
Rubyistの間では「DRY原則」が浸透しているので、彼らは重複や同じことの繰り返しを極端に嫌います。コードの中に繰り返しがあると、目や肌が乾燥してきて痒くなり落ち着きがなくなります。「DRY!DRY!DRY!」と叫び出す人もいます。アサヒスーパードライを飲み始める人もいます。これらの症状を総称して「DRY症候群」と言います。
Rubyの言語仕様はプログラマーがハッピーにコーディングできるよう考えられているので、RubyistをしてDRY症候群が発症することは稀ですが、日常的にDRY症候群を発症している人たちもいるようです。
以下は、DRY症候群を検査するためのテストコードです。コードを数秒間眺め、痒みが出てきたらあなたは重度のDRY症候群を患っています。
class User
attr_reader :name, :income
def initialize(name, income)
@name = name
@income = income
end
def real_user?
@income > 10_000_000
end
end
痒みが出なかった人たちに向けて説明すると、「initialize
におけるインスタンス変数への引数の引き渡しがDRYでない」、ということです。
それで大抵、彼らは以下を試し、うなだれるのです。
class User
attr_reader :name, :income
def initialize(@name, @income)
end
def real_user?
@income > 10_000_000
end
end
# ~> -:3: formal argument cannot be an instance variable
# ~> def initialize(@name, @income)
# ~> ^
# ~> -:3: formal argument cannot be an instance variable
# ~> def initialize(@name, @income)
# ~> ^
DartとCoffeeScriptの場合
グーグルが開発したプログラミング言語「Dart」では、Automatic field initializationという機能によって、以下のコードを、
class User {
String name;
num income;
User(String name, num income) {
this.name = name;
this.income = income;
}
}
以下のように書けるそうです。
class User {
String name;
num income;
User(this.name, this.income);
}
同様に、「CoffeeScript」でも、以下を、
class User
constructor: (name, income) ->
@name = name
@income = income
以下のように書けるそうです(classes, Inheritance, and Super)。
class User
constructor: (@name, @income) ->
羨ましい。
これをもって、DartまたはCoffeeScriptへの鞍替えを検討しているRubyistも少なくないと聞きます。
さっちゃんによるRuby版AFI
「さっちゃん」こと、@ne_sachirou殿がこの問題を解決すべく、Ruby版AFIの実装を公開してくれてます。
Dart風のautomatic field initializationをRubyで - c4se記:さっちゃんですよ☆
記事によれば、以下のようにinitialize
の定義のあとでauto_attr_init
クラスメソッドを呼べば、initializeの引数が自動で同名のインスタンス変数にセットされるそうです。
require 'auto_attr_init'
class Point
attr_accessor :x, :y
def initialize x, y; end
auto_attr_init
end
p = Point.new 2, 4
assert_equal 2, p.x
assert_equal 4, p.y
自動セットする変数を限定したい場合は、auto_attr_initにシンボルで渡します。
実装は、ソースをRipperで解析して、必要な処理を施して、sorcererというツールでRubyのコードに戻す、ということをやってるそうです。スゴイです。
@merborneによるなんちゃってRuby版AFI
さっちゃんのお陰で、DRY症候群問題は無事解消しました。
したがって僕の出番はないんですが、Ruby芸人を目指す身(!)としては、もっと安直にやれる方法を模索したくなりました。
で、私、@merborneも同じような機能をもった、「AutoAttrSet」というモジュールを作ったので公開します。
対象のクラスでAutoAttrSet
モジュールをextendします。そしてCoffeeScriptのように、initializeで@
を付けて変数を渡します。
require 'auto_attr_set'
class User
extend AutoAttrSet
attr_reader :name, :income
def initialize(@name, @income)
end
def real_user?
@income > 10_000_000
end
end
a_user = User.new('Charlie', 300_000)
a_user.name # => "Charlie"
a_user.income # => 300000
a_user.real_user? # => false
a_user.instance_variables # => [:@name, :@income]
デフォルト値が渡せない、*
や&
を伴う引数の後に渡せないなどの制約はありますが、このコードはシンタックスエラーにはなりません。普通の引数を間に挟むこともできます。
えっ?
…。
…バレちゃいましたか?
initializeの@
。
これは'@'.ord # => 64
ではなく、'@'.ord # => 65312
なんです…。
AutoAttrSetの実装
実装です。
簡単に説明します。Class.newをオーバーライドして、そこでset_instance_variables
というメソッドを呼びます。set_instance_variablesでは、initializeをMethodオブジェクト化してMethod#parameters
でその引数を取り出します。取り出した引数のうち特定のフォームを持ったものを対象として、instance_variable_set
を使ってインスタンス変数をセットします。特定のフォームはname_for_instance_variable?
のcandidatesに登録しておきます。ここでは、'@'
または'at_'
で始まる名前が登録されています。
at_
の例も示しておきます。
class Point
extend AutoAttrSet
attr_accessor :x, :y
def initialize(at_x, z, at_y, *rest)
end
end
po = Point.new(10, 20, 30)
po.x # => 10
po.y # => 30
po.instance_variables # => [:@x, :@y]
こんなもの書いてる暇があるなら、さっちゃんのコード読んでろって話ですが。
=== Ruby関連電子書籍100円で好評発売中! ===
blog comments powered by Disqus