こんにちは、hachi8833です。Rubyスタイルガイドを読むシリーズ、今回の「クラスとモジュール編」の2回目に予定していた部分がかなりこってりしているので、3回に分けることにしました。よろしくお願いします。
今回取り上げるスタイルの多くは、設計で「不要なクラスを作らない」「不要な継承を作らない」ことに注目していますね。
Rubyスタイルガイド: クラスとモジュール(2)クラス設計・アクセサ・ダックタイピングなど
クラス設計の階層はLiskovの置換原則に従う
When designing class hierarchies make sure that they conform to the Liskov Substitution Principle.
Liskovの置換原則に言及しています。
型のオブジェクト に関して真となる属性を とする。このとき が の派生型であれば、 型のオブジェクト について が真となる。
すなわち、リスコフとウィングが定式化した派生型の定義は置換可能性 (substitutability ) に基づいている。 が の派生型であれば、プログラム内で 型のオブジェクトが使われている箇所は全て 型のオブジェクトで置換可能であり、それによってプログラムの妥当性が損なわれることは無い。
Wikipedia: リスコフの置換原則より
どう落とし込むか考えてしまいましたが、morimorihogeさんが「派生クラスはスーパークラスが持つインターフェース(メソッド)を受信可能でなければならない」とまとめてくれました。
クラスはできるだけSOLIDに設計すること
Try to make your classes as SOLID as possible.
この「SOLID」はオブジェクト指向における設計原則(design principle)を指します。5つの原則の頭文字をうまいこと設定しています。
以下は「SOLID Design Principles」を元にごく簡単にまとめたものです。この枠には到底収まりきれないので、いずれ別記事にしたいと思います。
- S(単一責任原則)
- クラスは単一の機能についてのみ責任を持つようにすべし
- O(オープン・クローズ原則)
- クラスは自身への拡張に対しては寛容(open)にし、変更に対しては非寛容(close)にすべし
- L(リスコフの置換原則)
- 派生クラスはスーパークラスが持つインターフェース(メソッド)を受信できるようにすべし
- I(インターフェイス分離原則)
- ひとつの汎用インターフェイスで何もかもまかなうより、クライアント側に寄せたインターフェイスを多数作るべし
- D:(依存関係逆転原則)
- 上位のモジュールは下位のモジュールに依存してはならない
以下のリンク先でもしきりに注意されていますが、「あくまで原則は原則」なので振りかざすのは逆効果になりがちです。従わない者のお尻をペンペンするための原則ではなく、現実の設計やコーディングの見通しをよくして開発・改修を楽にするための原則、と考えることにします。
参考
- Wikipedia_en: SOLID (object-oriented design)
- [オブジェクト指向設計原則]オブジェクト指向設計原則
- オブジェクト指向の法則集 — SOLID以外にも多数の原則が紹介されています
ドメインオブジェクトを表現するクラスには常に適切な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
参考
重要度の低いアクセサやミューテータは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
アクセサやミューテータの名前に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の名前にget
やset
を使うのはJava方面の慣習だそうです。
attr
は原則使わない
Avoid the use of attr. Use attr_reader and attr_accessor instead.
attr_reader
やattr_accessor
の利用が推奨されています。
# 不可 - 単独の属性アクセサを作成(Ruby 1.9から非推奨)
attr :something, true
attr :one, :two, :three # attr_readerとしてしか使わないとする
# 良好
attr_accessor :something
attr_reader :one, :two, :three
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
で済む程度の使い捨てのクラス定義に用いるのがよさそうです。
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
はループの中で使わないようにしましょう。
クラスによってはファクトリーメソッドの追加を検討する
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
継承よりもダックタイピングを積極的に使うこと
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
今回はここまでとします。次回はクラスとモジュール編の「メソッドのスコープやエイリアスなど」をお送りします。ご期待ください。
関連記事
- Rubyスタイルガイドを読む: ソースコードレイアウト(1)エンコード、クラス定義、スペース
- Rubyスタイルガイドを読む: ソースコードレイアウト(2)インデント、記号
- Rubyスタイルガイドを読む: 文法(1)メソッド定義、引数、多重代入
- Rubyスタイルガイドを読む: 文法(2)アンダースコア、多重代入、三項演算子、if/unless
- Rubyスタイルガイドを読む: 文法(3)演算子とif/unless
- Rubyスタイルガイドを読む: 文法(4)ループ
- Rubyスタイルガイドを読む: 文法(5)ブロック、proc
- Rubyスタイルガイドを読む: 文法(6)演算子など
- Rubyスタイルガイドを読む: 文法(7)lambda、標準入出力など
- Rubyスタイルガイドを読む: 文法(8)配列や論理値など
- Rubyスタイルガイドを読む: 命名
- Rubyスタイルガイドを読む: コメント、アノテーション、マジックコメント
- Rubyスタイルガイドを読む: クラスとモジュール(1)構造