ククログ

株式会社クリアコード > ククログ > クリアなコードの作り方: 変わらない値とわかるようにする

クリアなコードの作り方: 変わらない値とわかるようにする

問題を調査する場合やプログラムを改良する場合、修正する場合などプログラムの挙動を把握しなければいけないことは多々あります。プログラムの挙動を把握する方法はいろいろあります。処理の流れを追う方法や特定の値に注目する方法や状態の変化を追う方法などです。

処理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)