- Ruby / Rails以外の開発一般
READ MORE
こんにちは、hachi8833です。Rubyスタイルガイドを読むシリーズ、今回の「クラスとモジュール編」の2回目に予定していた部分がかなりこってりしているので、3回に分けることにしました。よろしくお願いします。
今回取り上げるスタイルの多くは、設計で「不要なクラスを作らない」「不要な継承を作らない」ことに注目していますね。
When designing class hierarchies make sure that they conform to the Liskov Substitution Principle.
Liskovの置換原則に言及しています。
型のオブジェクト
に関して真となる属性を
とする。このとき
が
の派生型であれば、
型のオブジェクト
について
が真となる。
すなわち、リスコフとウィングが定式化した派生型の定義は置換可能性 (substitutability ) に基づいている。
が
の派生型であれば、プログラム内で
型のオブジェクトが使われている箇所は全て
型のオブジェクトで置換可能であり、それによってプログラムの妥当性が損なわれることは無い。
Wikipedia: リスコフの置換原則より
どう落とし込むか考えてしまいましたが、morimorihogeさんが「派生クラスはスーパークラスが持つインターフェース(メソッド)を受信可能でなければならない」とまとめてくれました。
Try to make your classes as SOLID as possible.
この「SOLID」はオブジェクト指向における設計原則(design principle)を指します。5つの原則の頭文字をうまいこと設定しています。
以下は「SOLID Design Principles」を元にごく簡単にまとめたものです。この枠には到底収まりきれないので、いずれ別記事にしたいと思います。
以下のリンク先でもしきりに注意されていますが、「あくまで原則は原則」なので振りかざすのは逆効果になりがちです。従わない者のお尻をペンペンするための原則ではなく、現実の設計やコーディングの見通しをよくして開発・改修を楽にするための原則、と考えることにします。
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の更新を反映しました。
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
今回はここまでとします。次回はクラスとモジュール編の「メソッドのスコープやエイリアスなど」をお送りします。ご期待ください。