問題を調査する場合やプログラムを改良する場合、修正する場合などプログラムの挙動を把握しなければいけないことは多々あります。プログラムの挙動を把握する方法はいろいろあります。処理の流れを追う方法や特定の値に注目する方法や状態の変化を追う方法などです。
処理Aと処理Bの間でおかしくなっていることがわかっている場合は処理の流れを追う方法がよいでしょう。AからBの間の処理の流れを追っていけば問題の箇所を見つけられるからです。
特定の値がおかしくなっていることがわかっている場合はその値に注目して把握する方法がよいでしょう。その値が変わる可能性がある箇所に注目すれば問題を特定できます。
メインループ(GUIのイベントループや言語処理系の評価器のループなど)内の処理を把握する場合は状態の変化を追う方法がよいでしょう。状態を変えながら何度もループする処理では状態によって実行する内容が変わります。状態を把握することで実行する内容も把握しやすくなります。
ここでは特定の値に注目してプログラムの挙動を把握する場合にこんなコードだったら把握しやすいというコードを紹介します。これはコードを読む人視点の言い方です。コードを書く人視点で言うと、挙動を把握しやすいコードの書き方を紹介します、になります。
特定の値に注目する
「特定の値に注目する」とはどういうことでしょうか。それはその値が変わるタイミングに注目するということです。
たとえば、次のようなクラスがあり、@age
の値に注目するとします。
class User
def initialize(age)
@age = age
end
def age
@age
end
def age=(new_age)
@age = new_age
end
end
@age
が変わる可能性がある箇所は以下の2箇所です。
def initialize(age)
@age = age
end
def age=(new_age)
@age = new_age
end
そのため、この2つのメソッドが呼ばれる箇所に注目します。なぜなら、ここでしか@age
は変わらないからです。ここに注目しておかしな値を設定している処理を見つけることで問題を特定できます。
users = {}
users[:user1] = User.new(-1) # 注目
users[:user2] = User.new(14) # 注目
puts(users[:user1].age)
users.each do |key, user|
if user.age == -1
user.age = 29 # 注目
end
end
この例では@age
を変更できる箇所は2箇所でしたが、もっと変更できる箇所がある場合は注目する箇所も増えます。注目する箇所が増えるということは挙動を把握することが難しくなるということです。逆に言うと、変更できる箇所が少ないと挙動を把握しやすくなるということです。
ここまではコードを読む人視点の話です。
特定の値に注目しやすいコードの書き方
それでは、コードを書く人視点の話に入ります。
値を変更できる箇所が最小限になっているとコードを読むときに挙動を把握しやすくなるのでした。つまり、値を変更できる箇所が最小限なコードを書くと挙動を把握しやすいコードになります。
それでは、値を変更できる箇所を少なくする方法をいくつか紹介します。
セッターメソッド(ライターメソッド)を提供しない
前述の例ではコンストラクターとセッターメソッドが@age
を変更できる箇所でした。
def initialize(age)
@age = age
end
def age=(new_age)
@age = new_age
end
変更できる箇所を少なくするならage=
メソッドを提供しなければよいです。
class User
def initialize(age)
@age = age
end
def age
@age
end
end
これで変更できる箇所が1つ減りました。age=
メソッドがなくなったので使い方も変わります。
users = {}
users[:user1] = User.new(-1) # 注目
users[:user2] = User.new(14) # 注目
puts(users[:user1].age)
users.each do |key, user|
if user.age == -1
# ↓userを変更するのではなく新しくインスタンスを作ることになった
users[key] = User.new(29) # 注目
end
end
セッターメソッドを無くしても注目する箇所は3箇所から減っていないのですが、コードを読む側としてはうれしいことが増えました。それは、一度作ったインスタンスでは@age
は変わらないということです。
インスタンスは1度作られるといろいろな箇所で使われるでしょうが、インスタンスが作られる箇所はそれよりは少ないことが多いです。たとえば、以下の箇所でインスタンスを使っています。
users = {}
users[:user1] = User.new(-1)
users[:user2] = User.new(14)
puts(users[:user1].age) # 使っている
users.each do |key, user|
if user.age == -1 # 使っている
users[key] = User.new(29)
end
end
あれ、使っている箇所(2箇所)より作っている箇所(3箇所)の方が多いですね。。。実際のプログラムでは使っている箇所の方が多いはずです。。。
使っている箇所が多い前提で話を進めると、多いと@age
が変わる可能性があるかを検討することが大変になります。そのため、検討する箇所が少なくなるのはコードを読む側にとってうれしいです。
コンストラクターでだけ値を設定する
セッターメソッドを無くした結果、コンストラクターでだけ値を設定するようになりました。
class User
def initialize(age)
@age = age
end
def age
@age
end
end
@age
を変更する箇所を減らすということであれば、次のようにすることもできました。
class User
def initialize
end
def age
@age
end
def age=(new_age)
@age = new_age
end
end
使うときはこうなります。
user = User.new
user.age = 29
これよりもコンストラクターで値を設定するコードの方がよい理由は次の通りです。
-
@age
が変更されるタイミングはインスタンス作成時だけということが明確になる。(=インスタンスを作った後に変更されない。) -
@age
は必ず設定されていることが明確になる。(セッターメッソドを使う場合だとインスタンスを作った後に設定を忘れると設定されない。)
つまり、コードを書いた人の意図が明確になるので読むときに助かるということです。
「変更しない値はコンストラクターでだけ設定する」はコードを書いた人の意図を明確にする書き方なので、「このクラスのインスタンスは値を変更しないぞ!」という意図でコードを書いているときは積極的に活用したい書き方です。
変更できないようにする
これまでの例では、あえて値が数値になるようにしていました。これは、Rubyでは数値は変更不可能なオブジェクトだからです。文字列は変更可能なオブジェクトなので事情が変わってきます。
たとえば、次のコードはmessage
メソッド経由でも@message
の値を変更できます。
class Commit
def initialize(message)
@message = message
end
def message
@message
end
end
次のようにすると変更できます。
commit = Commit.new("Hello")
puts(commit.message) # => Hello
commit.message << " World!"
puts(commit.message) # => Hello World!
これも防ぐようなコードにするかどうかはケースバイケース(オーバースペックの場合の方が多い)ですが、たとえば、次のようすればmessage
メソッド経由では@message
の値を変更できなくなります。
class Commit
def initialize(message)
@message = message.dup
@message.freeze
end
def message
@message
end
end
commit = Commit.new("Hello")
commit.message << " World!" # 例外:can't modify frozen String (RuntimeError)
Rubyは値(オブジェクト)を変更不能にする方法はfreeze
ですが、方法は言語ごとに違います。たとえば、CやC++ではconst
を使います。
まとめ
コードを書くときに「変わらない値」とわかるような書き方にすることで、読むときに挙動を把握しやすいコードになるということを紹介しました。
いくつか書き方を紹介しましたが、中でも、「変更しない値はコンストラクターでだけ設定する」書き方は書いた人の意図を明確にしやすい書き方なのでそのような場面になったら活用してください。
なお、この話題についてまとめるきっかけになったのは次のようなコードを見たことがきっかけでした。外部で作られているオブジェクトに新しくroute_key
というデータを付与したいというコードです。
route = create_output
route.instance_eval do
route.singleton_class.class_eval do
attr_accessor :route_key
end
end
route.route_key = key
このコードは最終的に次のようなコードになりました。セッターメソッドで値を設定するのではなくコンストラクターで値を設定するようになっています。
class Route
attr_reader :output
attr_reader :key
def initialize(output, key)
@output = output
@key = key
end
end
output = create_output
route = Route.new(output, key)