Posted at

これはMUST!ActiveSupport の Class#class_attribute を使おう!

More than 3 years have passed since last update.

この記事は Ruby on Rails の Advent Calendar 2015 の11日目です。

この記事では ActiveSupport の Class#class_attribute を紹介します。

Class#class_attribute を使うと、親クラスの設定値をデフォルト値にしつつ、子クラスでその動作をカスタマイズするということがカンタンにできます。


はじめに

クックパッド社の Ruby styleguide はご覧になったことはありますでしょうか?

その中で下記のような記述があります。




  • [MUST] クラス変数 (@@foo) を使用してはならない。代わりに class_attribute を使用すること。


なぜクラス変数を使用してはいけないのでしょうか?

この class_attribute とはなんでしょうか?

なぜ class_attribute の使用が MUST なのでしょうか?

この記事を読むとクラス変数、定数の問題点、class_attribute が推奨される理由が理解できます。


クラス階層をまたがるデータの参照について

Ruby の変数には数多くの種類があります。

その中で特にクラス階層をまたがって互いにデータを参照できるものとしては下記があります。


  • 定数

  • クラス変数

  • グローバル変数

この中でグローバル変数はその名のとおりプログラム全体で同一のスコープで共有されるものであり、非常に特殊な用途で使われるもので、その問題点はよく認知されていると思いますので今回は説明しません。


定数の問題点

まず定数の問題点について説明します。

定数の場合はコードが記述された場所からリテラルに探索して見つかった定数が使われます。

つまり、親クラスで定義されたメソッドでは親クラスの定数の値を取得し、子クラスで定義されたメソッドでは子クラスの定数の値を取得します。

日本語で説明しても分かりにくいと思うのでコード例を示します。

class Base

A = 1
def self.a
A
end
end

class Subclass<Base
A = 2
def self.aa
A
end
end

puts Base.a #=> 1
puts Subclass.a #=> 1
puts Subclass.aa #=> 2

定数の参照が記述されている場所を起点にリテラルに探索を行った上で発見できた定数の値を採用する、というこの挙動は理解しやすい仕様です。

ただ上記の例でいうと Subclass.a が 2 を返して欲しいという場合もあります。子クラスから定数A を参照しているのであるから子クラス側の定数A の値を使いたいという場合です。

例えば、子クラスで設定した場合はその値を使うが特別に設定していない場合は親クラスの値を使いたいような状況ではそういう挙動を期待してしまいます。

しかしながら、その状況では定数は期待通り動作しません。

また、定数はその仕様上 メソッド内で定義することはできません。

class B

def self.set_C
C = 1 # dynamic constant assignment の Syntax Error が発生する
end
end

Module#const_set を使うとメソッド内でも自由自在に定数を定義できますが、やりたいことに対して大げさな気がします。

また、そもそも定数は最初に定義してからずっと値が一定で変わらない場合に使うものです。

値を更新したい場合には適切ではありません。


クラス変数の問題点

次にクラス変数の問題点を説明します。

クラス変数は定数とは異なり、メソッド内でも自由自在に更新できます。

ですが、クラス変数とは実際上スコープが少し区切られているだけのグローバル変数です。その変数が更新されると継承関係全体で影響を受けます。

これも言葉で説明しても分かりにくいので、コード例を示します。

class Base

@@a = 1
def self.a
@@a
end
end

class Subclass<Base
@@a = 2 # この時点で Base の @@a を上書きする。
def self.aa
@@a
end
end

puts Base.a #=> 2
puts Subclass.a #=> 2
puts Subclass.aa #=> 2

このコードで、Base.a の値は Subclass の定義中にクラス変数 @@a の値が書きかえられてしまった後なので、更新された値が返ります。

これが意図する動作であれば良いですが、子クラスによって親クラスが影響を受けるのは意図しない場合もあります。

よく理解した上で使わないとクラス変数を利用していることが原因で発見しにくいバグが生まれる危険性があります。


Class#class_attribute

Class#class_attribute を使うと、メソッド中に自由に書き変えることが可能でかつ子クラスでの処理によって親クラスの値が汚染されることはありません。

これも日本語で説明しても分かりにくいと思うので例を示して説明します。

require 'active_support/core_ext/class/attribute'

class Base
class_attribute :a
end
Base.a = 1

class Subclass1<Base
end
Subclass1.a = 2

class Subclass2<Base
end

puts Base.a #=> 1
puts Subclass1.a #=> 2
puts Subclass2.a #=> 1

このコードから Class#class_attribute の下記の特徴が分かります。



  • class_attribute :a と書くことで、Base.aBase.a= などのアクセサメソッドが定義される。

  • 値を上書きした Subclass1.a の値は、設定した値に更新されている。

  • 値を上書きしなかった Subclass2.a の値は Base.a と同じ値になっている

  • 子クラスによる値の上書きがあっても、クラス変数と異なり親クラスへの副作用はない


まとめ

Class#class_attribute は下記の点で便利です。


  • アクセサメソッドが自動的に定義されて便利。書きやすいし、読みやすい。

  • 上記の例ではクラス経由でだけアクセスしているが、実際はインスタンスメソッド内からもアクセスできて便利

  • 子クラスで再設定しても親クラスの値を汚染しなくて便利。安心。

  • デフォルトは親クラスの値だが、カスタマイズしたいときだけ子クラスで別途設定したいときに特に便利

クラス変数の挙動よりもclass_attribute の挙動の方が多くの状況で期待される挙動でしょう。

みなさん、Class#class_attribute を使いましょう。