先日、コードを書いていて独自クラスを定義した方がよいか悩む場面がありました。
独自クラスをいつ定義すべきかの判断基準を知りたいと思い、調べましたので、本記事にてまとめたいと思います。
なお、私は業務でRubyを使っており、以下の内容は書籍「研鑽Rubyプログラミング」の「第2章:役に立つ独自クラスを設計する」を基にしています。
本書籍は非常に学びがあるので、気になる方はぜひチェックしていただければと思います。
独自クラスをいつ定義すべきか
結論としては、独自クラスをいつ定義すべきか一概には言えず、複数の観点で独自クラスを定義すべきかを検討する、ということになると思います。以下では、独自クラスを定義すべきかを判断する際に役立つ観点を何点か紹介します。
独自クラスを定義することのメリット・デメリット
1点目は独自クラスを定義するメリット・デメリットです。メリットがデメリットを上回れば、独自クラスを定義する根拠になると思います。独自クラスを定義するメリット・デメリットとしては以下が挙げられます。
メリット
- 状態をカプセル化できる
- クラスのインスタンスに関連づけられた関数(メソッド)を呼び出すシンプルな方法を提供できる
デメリット
- 認知負荷がかかる
以下、順に説明します。
メリット1:状態をカプセル化できる
以下のようなスタックを考えます。
stack = []
# スッタクの先頭に追加する
stack.push(1)
# スタックの先頭の値を取り出す
stack.pop()
このコードは組み込みの配列クラスを使用しているため、例えば以下のように外部のコードによってスタックの設計が台無しにされる可能性があります。
# スタックの末尾に値を追加する
stack.unshift(2)
そこで、以下のような独自クラスを定義します。
class Stack
def initialize
@stack = []
end
def push(value)
@stack.push(value)
end
def pop
@stack.pop
end
end
これにより、状態(インスタンス変数のstack)がカプセル化され、外部コードで状態が変更されることを起きにくくできます。これが「メリット1:状態をカプセル化できる」です。このように状態をカプセル化したい場合はクラス化を検討してもよいかと思います。
ただし、このスタックを使うクラスがすでにカプセル化されており、スタックを使うかどうかが実装の詳細にすぎない場合もあります。このような場合はクラス化してもメリット1の恩恵がないばかりか、複雑性が増し、実行速度も落ちます。よって、この場合はクラス化するのを考え直すとよいでしょう。
メリット2:クラスのインスタンスに関連づけられた関数(メソッド)を呼び出すシンプルな方法を提供できる
上で定義したクラスStack
にはメリット1があるものの、それだけではクラス化の根拠としては弱いかもしれません。そこで、上のクラスに独自の振る舞いを追加してみます。
例えば、「スタックに積める値をシンボルに限定し、スタックからシンボルを取り出すときには、そのシンボルがスタックに積まれてからの経過時間も一緒に取得する」という振る舞いを追加します。
コードは以下のようになります。
class Stack
def initialize
@stack = []
end
def push(sym)
raise TypeError unless sym.is_a?(Symbol)
@stack.push([sym, clock_time])
end
def pop
sym, pushed_at = @stack.pop
[sym, clock_time - pushed_at]
end
private def clock_time
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
end
独自の振る舞い(カスタマイズされたpush, pop)が追加されました。
これが「メリット2:クラスのインスタンスに関連づけられた関数(メソッド)をシンプルに呼び出す方法を提供できる」です。このようなインスタンスに関連づけられた関数を定義したい場合は、クラス化を検討してもよいかと思います。
特に、上記のスタックであれば情報隠蔽と独自の振る舞い(つまり、メリット1、2の両方)を兼ね備えているため、クラス化を検討する価値は十分あると思われます。
デメリット:認知負荷がかかる
独自クラスを定義すると、コードを扱う全員がそのクラスを正しく使って仕事を進められるように、クラスの仕組みを学ぶ必要があります。つまり、認知負荷がかかります。
以上説明したメリット・デメリットを鑑み、メリットがデメリットを上回るのであれば独自クラスを定義する根拠になると思います。
独自クラスがどれくらい広く使われるか
定義する独自クラスが単一のクラスでしか使われず、ユーザから直接参照されないのであれば、独自クラスを定義するのは時期尚早かもしれません。
一方、同じようなニーズを持つ3つの異なるクラスで使用されるのなら独自クラスを定義する強い根拠になります。
クラスを部分的に別の実装で置き換えたいか
クラスの一部を別の実装で簡単に置き換えたい場合、複数のクラスを定義しておくとよい場合があります。
例えば、帳票を印字するプログラムを考えます。以下のように帳票の出力に必要なデータとデータの整形に必要なメソッドを持つReport
クラスを考えます。
report = Report.new(data)
puts report.format
出力の形式が一種類であれば上記の実装で十分かもしれません。
一方、出力の形式が3種類(たとえばdocsとpdfとcsv)あるのであれば、以下のようにクラスを分けて置く方が該当する部分の置換が容易になります。
report_content = ReportContent.new(data)
report_formatter = ReportFormatter.for_type(report_type).new
puts report_formatter.format(report_content)
複数の出力形式に対応することになることが予めわかっているのであれば、先んじてクラスを分けて置くのが良い設計です。
単一責任の原則
単一責任の原則(Single-Responsibility principle)はSOLID原則の一つで、「クラスは基本的に一つの目的を果たすように設計すべきである」というものです。
この原則は、複数の目的を担っている既存の単一のクラスを複数のクラスに分割するのを正当化するのに使われます。
ただし、目的を狭い視野でとらえ、この原則を適用すると、クラスの数が必要以上に増えます。結果、複雑性、メンテナンスコストが増加するのでやみくもに適用すればいいというわけではありません。
例えば、以下のコードを考えます。String
クラスは、テキストを生成する目的とテキストを修正する目的を持ちます。
str = String.new
str << 'test' << 'ing...1...2'
name = ARGV[0].
to_s.
gsub('cool', 'amazing').
capitalize
str << '. Found: ' << name
puts str
このクラスに対して、単一責任の原則を適用し、テキストを生成する目的を担うクラス(TextBuilder
)とテキストを修正する目的を担うクラス(TextModifier
)に分けると上記のコードは以下のようになります。
builder = TextBuilder.new
builder.append('test')
builder.append('ing...1...2')
modifier = TextModifier.new
name = modifier.gsub(ARGV[0].to_s, 'cool', 'amazing')
name = modifier.capitalize(name)
builder.append('. Found: ')
builder.append(name)
puts builder.as_string
果たしてこのように設計されたクラスは使いやすいでしょうか?
大抵は「複数に分割されたクラスがそれぞれ単一の目的を担う」という設計よりも「関連する複数の目的を単一のクラスが担う」という設計の方が使いやすく、メンテナンスもしやすいです。
よって、使いやすさ、メンテナンスコストも考慮して単一責任の原則を適用するかを判断するとよいでしょう。
まとめ
以上、独自クラスを定義すべきかを判断する際に役立つ観点を何点か紹介しました。
本記事が少しでもお役に立てれば幸いです。
コメント