Tech Racho エンジニアの「?」を「!」に。
  • 開発

Rubyスタイルガイドを読む: クラスとモジュール(2)クラス設計・アクセサ・ダックタイピングなど

こんにちは、hachi8833です。Rubyスタイルガイドを読むシリーズ、今回の「クラスとモジュール編」の2回目に予定していた部分がかなりこってりしているので、3回に分けることにしました。よろしくお願いします。

今回取り上げるスタイルの多くは、設計で「不要なクラスを作らない」「不要な継承を作らない」ことに注目していますね。

クラスとモジュール(2)クラス設計・アクセサ・ダックタイピングなど

5-06【統一】クラス設計の階層はLiskovの置換原則に従う

When designing class hierarchies make sure that they conform to the Liskov Substitution Principle.

Liskovの置換原則に言及しています。

{\displaystyle T}型のオブジェクト {\displaystyle x} に関して真となる属性を {\displaystyle q(x)} とする。このとき {\displaystyle S}{\displaystyle T} の派生型であれば、 {\displaystyle S} 型のオブジェクト {\displaystyle y} について {\displaystyle q(y)} が真となる。

すなわち、リスコフとウィングが定式化した派生型の定義は置換可能性 (substitutability ) に基づいている。{\displaystyle S}{\displaystyle T} の派生型であれば、プログラム内で {\displaystyle T} 型のオブジェクトが使われている箇所は全て {\displaystyle T} 型のオブジェクトで置換可能であり、それによってプログラムの妥当性が損なわれることは無い。
Wikipedia: リスコフの置換原則より

どう落とし込むか考えてしまいましたが、morimorihogeさんが「派生クラスはスーパークラスが持つインターフェース(メソッド)を受信可能でなければならない」とまとめてくれました。

5-07【統一】クラスはできるだけSOLIDに設計すること

Try to make your classes as SOLID as possible.

この「SOLID」はオブジェクト指向における設計原則(design principle)を指します。5つの原則の頭文字をうまいこと設定しています。

以下は「SOLID Design Principles」を元にごく簡単にまとめたものです。この枠には到底収まりきれないので、いずれ別記事にしたいと思います。

S(単一責任原則)
クラスは単一の機能についてのみ責任を持つようにすべし
O(オープン・クローズ原則)
クラスは自身への拡張に対しては寛容(open)にし、変更に対しては非寛容(close)にすべし
L(リスコフの置換原則)
派生クラスはスーパークラスが持つインターフェース(メソッド)を受信できるようにすべし
I(インターフェイス分離原則)
ひとつの汎用インターフェイスで何もかもまかなうより、クライアント側に寄せたインターフェイスを多数作るべし
D:(依存関係逆転原則)
上位のモジュールは下位のモジュールに依存してはならない

以下のリンク先でもしきりに注意されていますが、「あくまで原則は原則」なので振りかざすのは逆効果になりがちです。従わない者のお尻をペンペンするための原則ではなく、現実の設計やコーディングの見通しをよくして開発・改修を楽にするための原則、と考えることにします。

参考

5-08【統一】ドメインオブジェクトを表現するクラスには常に適切なto_sメソッドを実装すること

Always supply a proper to_s method for classes that represent domain objects.

ドメインオブジェクトはビジネスオブジェクトとも呼ばれ、Wikipedia: ビジネスオブジェクトによると「プログラムが表現しようとしている領域(ドメイン)での実体を抽象化したものである」と説明されています。

class Person
  attr_reader :first_name, :last_name

  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end

  def to_s  # to_sを実装してあげよう
    "#{first_name} #{last_name}"
  end
end

b22c6d3の更新を反映しました。

参考

5-09【統一】重要度の低いアクセサやミューテータはattr_*で定義する

Use the attr family of functions to define trivial accessors or mutators.

# 不可
class Person
  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end

  def first_name
    @first_name
  end

  def last_name
    @last_name
  end
end

# 良好
class Person
  attr_reader :first_name, :last_name

  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end
end

5-10【統一】アクセサやミューテータの名前にget_set_を使うことは避ける

For accessors and mutators, avoid prefixing method names with get_ and set_. It is a Ruby convention to use attribute names for accessors (readers) and attr_name= for mutators (writers).

Rubyには以下のコーディング慣習があります。

  • アクセサ名(読み取り用)には属性名をそのまま使う
  • ミューテータ名(書き込み用)には属性名=を使う
# 不可
class Person
  def get_name
    "#{@first_name} #{@last_name}"
  end

  def set_name(name)
    @first_name, @last_name = name.split(' ')
  end
end

# 良好
class Person
  def name
    "#{@first_name} #{@last_name}"
  end

  def name=(name)
    @first_name, @last_name = name.split(' ')
  end
end

なお、getterやsetterの名前にgetsetを使うのはJava方面の慣習だそうです。

5-11【統一】attrは原則使わない

Avoid the use of attr. Use attr_reader and attr_accessor instead.

attr_readerattr_accessorの利用が推奨されています。

# 不可 - 単独の属性アクセサを作成(Ruby 1.9から非推奨)
attr :something, true
attr :one, :two, :three # attr_readerとしてしか使わないとする

# 良好
attr_accessor :something
attr_reader :one, :two, :three

5-12【ヒント】Struct#newを積極的に使う

Consider using Struct.new, which defines the trivial accessors, constructor and comparison operators for you.

Struct#newは、細かなアクセサやコンストラクタ、比較演算子までお膳立てしてくれる便利なメソッドです。軽いクラス生成にはもってこいです。

# 良好
class Person
  attr_accessor :first_name, :last_name

  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end
end

# 良好度さらにアップ: 上の定義はこれだけで書ける
Person = Struct.new(:first_name, :last_name) do
end

その代わりクラス定義であることがわかりにくくなりそうなので、クラス図に書くほどではない、Struct#newで済む程度の使い捨てのクラス定義に用いるのがよさそうです。

5-13【禁止】Struct#newで初期化したインスタンスを継承しないこと

Don't extend an instance initialized by Struct.new. Extending it introduces a superfluous class level and may also introduce weird errors if the file is required multiple times.

原文ではextendと表現されていますが、ここでは継承を指しています。

Struct#newで初期化したインスタンスをクラス定義で継承すると、クラスレベルが1つ余分になるうえ、そのコードのファイルが複数回requireされて奇妙なエラーを引き起こしたりします。

# 不可
class Person < Struct.new(:first_name, :last_name)
end

# 良好
Person = Struct.new(:first_name, :last_name)

BPS Webチームのkazzさんがさらに、上記の「不可」のようなことをするとPersonのインスタンスを作るたびにStruct#newのインスタンスもいちいち作成されてしまうので設計上も実装上もよろしくないと指摘してくれました。

もちろん、Struct#newはループの中で使わないようにしましょう。

5-14【ヒント】クラスによってはファクトリーメソッドの追加を検討する

Consider adding factory methods to provide additional sensible ways to create instances of a particular class.

特別な前処理などが必要なクラスには、ファクトリメソッドを追加してより融通の利くインスタンス生成をできるようにすることを検討するとよいでしょう。

class Person
  def self.create(options_hash)
    # (本文は略)
  end
end

5-15【ヒント】継承よりもダックタイピングを積極的に使うこと

Prefer duck-typing over inheritance.

継承が設計上必要でなければ、ダックタイピングで楽しましょうということと理解しました。

# 不可: このぐらいなら継承を使わずに書きたい
class Animal
  # 抽象メソッド
  def speak
  end
end

# スーパークラスを継承
class Duck < Animal
  def speak
    puts 'Quack! Quack'
  end
end

# スーパークラスを継承
class Dog < Animal
  def speak
    puts 'Bau! Bau!'
  end
end
# 良好: DuckとDogがシンプルにspeakを実装しているので継承が発生しない
class Duck
  def speak
    puts 'Quack! Quack'
  end
end

class Dog
  def speak
    puts 'Bau! Bau!'
  end
end

今回はここまでとします。次回はクラスとモジュール編の「メソッドのスコープやエイリアスなど」をお送りします。ご期待ください。

関連記事



CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。