0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Ruby】オブジェクト指向設計の単一責任のクラスを設計する|インスタンス変数とアクセサメソッドに関して

Last updated at Posted at 2021-02-21

オブジェクト指向設計実践ガイド~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方を読んでいて、インスタンス変数とアクセサメソッドの違いに関して興味を持ったのでまとめます。

詳細の内容は、本書の第二章の『単一責任のクラスを設計する』に書かれています。

オブジェクトとデータ

オブジェクトは「データ」と「振る舞い」を持ちます。
データは、任意のデータ(文字列、ハッシュ、配列など)をインスタンス変数内に保持します。

データへのアクセスは次のうちのどちらかの方法で行われます。

  • インスタンス変数を直接参照する
  • インスタンス変数をアクセサメソッドで包み隠す

それぞれに関してみていきましょう。

①インスタンス変数を直接参照する

Gearクラスにギア比を計算するratioメソッドを定義しています。

class Gear
  def initialize(chainring, cog)
    @chainring = chainring
    @cog = cog
  end

  def ratio
    @chainring / @cog.to_f  # 破滅への道
  end
end

メソッドratioは何がダメなのでしょうか?
理由は、インスタンス変数を直接参照しているからです。

例えば、「@cog」のデータ(内容)を変更する場合を考えてみます。
@cog」を複数箇所で参照していた場合、全ての箇所で「@cog」のデータを変更する必要があります。

下記のサンプルコードをみてみましょう。

変更前
class Gear
  def initialize(chainring, cog)
    @chainring = chainring
    @cog = cog
  end

  def ratio
    @chainring / @cog.to_f # @cogを使用
  end

  def ratio_second
    # 何らかの処理
    @chainring / @cog.to_f # @cogを使用
  end

end

予期せぬ調整が入った場合、下記2箇所でメソッドの変更が必要になります。

変更後
class Gear
  def initialize(chainring, cog)
    @chainring = chainring
    @cog = cog
  end

  def unanticipated_adjustment_factor # 予期せぬ調整
    2
  end

  def ratio
    @chainring / (@cog * unanticipated_adjustment_factor).to_f  # 変更が必要
  end

  def ratio_second
    # 何らかの処理
    @chainring / (@cog * unanticipated_adjustment_factor).to_f  # 変更が必要
  end

end

これは、データ(どこからでも参照される)から振る舞い(1箇所で定義される)へと変えることによって解決することができます。

独自のメソッドを用意し、振る舞いを変えて対応させます。
そして、クラス内では常にデータではなく、振る舞いに依存させます。

②インスタンス変数をアクセサメソッドで包み隠す

データではなく振る舞いに依存させるためには、このように書き換えます。

class Gear
  attr_reader :chainring, :cog  # 追加

  def initialize(chainring, cog)
    @chainring = chainring
    @cog = cog
  end

  def ratio
    chainring / cog.to_f  # 変更
  end
end

この場合、サンプルコードはどのように変わるでしょうか?

変更前
class Gear
  attr_reader :chainring, :cog

  def initialize(chainring, cog)
    @chainring = chainring
    @cog = cog
  end

  def ratio
    chainring / cog.to_f # cogメソッドを使用
  end

  def ratio_second
    # 何らかの処理
    chainring / cog.to_f # cogメソッドを使用
  end

end

メソッド(振る舞い)に依存させることによって、予期せぬ変更に対して変更箇所は1箇所で終了します。

変更後(予期せぬ変更が発生)
class Gear
  attr_reader :chainring, :cog

  def initialize(chainring, cog)
    @chainring = chainring
    @cog = cog
  end

  def cog
    @cog * unanticipated_adjustment_factor # ここのみを変更
  end

  def unanticipated_adjustment_factor # 予期せぬ調整
    2
  end

  def ratio
    chainring / cog.to_f  # ここは変更しなくて良い
  end

  def ratio_second
    # 何らかの処理
    chainring / cog.to_f  # ここは変更しなくて良い
  end

end

@kts_h さんからサンプルコードに関してご指摘をいただきました!ありがとうございます!
また、「歯車(cog)の数が不正確な場合」の記述例を挙げていただきました。
※詳細はコメント欄で確認できます。

用語

参考として使っているメソッドを簡単にまとめます。

attr_reader

attr_readerを使うと、Rubyは自動で下記メソッドをつくります。

# attr_readerによるデフォルトの実装
def cog
  @cog
end

参考:Module#attr_reader (Ruby 3.0.0 リファレンスマニュアル)

このcogメソッドは、コードの中で唯一cogが何を意味するか?を知っています。
このメソッドを実装することによって、データが振る舞い(1箇所で定義される)へと変わります。

そのため、データである@cogを変更する必要が出た場合は、「@cog」に関して複数箇所の変更をする必要がなく、コグについての独自メソッドを定義するだけでよくなります。

def cog
  if 条件
    @cog * 特殊な計算が発生
  else
    @cog
  end
end

initialize

Rubyのコンストラクタです。
コンストラクタはオブジェクト生成時に自動的に呼び出され、クラスのデータ初期化処理を行う特別なメソッドです。
参考:Object#initialize (Ruby 3.0.0 リファレンスマニュアル)

【おまけメモ】 attr_readerを利用時の挙動の違い

initialize という名前のメソッドは自動的に private に設定されます。
そのため、クラスの外からアクセスすることができません。

失敗例
class Gear
  def initialize(chainring, cog)
    @chainring = chainring
    @cog = cog
  end

  def ratio
    @chainring / @cog.to_f
  end
end

gear1 = Gear.new(10, 20)
puts gear1.cog
実行結果
 `<main>': undefined method `cog' for #<Gear:0x00007fb49015a968 @chainring=10, @cog=20> (NoMethodError)

クラス外からアクセスするにはゲッター/セッターメソッドが必要です。

成功例
class Gear
attr_reader :cog

  def initialize(chainring, cog)
    @chainring = chainring
    @cog = cog
  end

  def ratio
    @chainring / @cog.to_f
  end
end

gear1 = Gear.new(10, 20)
puts gear1.cog
実行結果
=> 20

attr_readerを使うとこのような挙動の違いが発生します。

【まとめ】データではなく、振る舞い(メソッド)に依存させる

振る舞い(メソッド)に依存させることによって、変更しやすいコードにすることができました。

インスタンス変数の隠蔽

インスタンス変数は常にアクセサメソッドで包み、直接参照しないようにすることが本書で推奨されています。

【疑問】 振る舞いではなく、データに依存させてみる

変更をしやすくするための解決方法として、振る舞いに依存させず、データを変えることによって対応させる方法はどうでしょうか?

class Gear
  def initialize(chainring, cog)
    @chainring = chainring
    if 条件
      @cog = cog * 特殊な計算が発生
    else
      @cog = cog
    end
  end

  def ratio
    @chainring / @cog.to_f
  end
end

initializeというメソッドはあくまでも「データの初期化という役割」を果たすため、「データの中身に対する処理」を持つのは適切ではないため、このパターンはアンチパターンなのではないでしょうか?

本書を読み進めていくうえで理解が深まり、アンチパターンであるという理由が言語化されることを期待します。

コードや内容に関して不備や改善点等ございましたら、ご指摘お待ちしております。

0
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?