クラス設計
独自クラスをどのように設計し、どんな構造にするかは、コードのわかりやすさとメンテナンス性に多大な影響を及ぼします。
独自クラスを定義する前に「本当に独自クラスを定義する必要があるだろうか?」や「どれくらい広く使われるだろうか?」と問うことが大切です。
独自クラスの利点
-
状態のカプセル化
オブジェクト指向プログラミングの原則の1つであり、オブジェクトの内部状態を外部からアクセスできないように保護することです。要は、あるクラスに定義したメソッドは他のクラスから呼び出すことができないので、独立性を高めて、保守性が高まる訳です。 -
クラスのインスタンスに関連づけられた関数(メソッド)を呼び出すためのシンプルな方法を提供
独自クラスを定義することで、そのクラスのインスタンスに関連づけられた関数(メソッド)を呼び出すことができます。これにより、プログラムの機能を分割して管理し、再利用可能なコードを作成することができます。
SOLID原則のトレードオフ
SOLID原則とは、オブジェクト指向設計の5つの原則のそれぞれの頭文字を取ったもの。
SOLID | 説明 |
---|---|
S (Single Responsibility) 単一責任の原則 | クラスは、単一の責任を持つべきだ。 |
O (Open-Closed) オープン・クローズドの原則 | クラスは、拡張にはオープンで、変更にはクローズドであるべきだ。 |
L (Liskov Substitution) リスコフの置換原則 | SがTのサブタイプである場合、プログラム内のT型のオブジェクトをS型のオブジェクトに置き換えても、そのプログラムの特性は何も変わらない。 |
I (Interface Segregation) インターフェイス分離の原則 | クライアントが使用しないメソッドへの依存を、強制すべきではない。 |
D (Dependency Inversion) 依存性逆転の原則 | ・上位モジュールは、下位モジュールに依存してはならない。どちらも抽象化に依存すべきだ。 ・抽象化は詳細に依存してはならない。詳細が抽象化に依存すべきだ。 |
これらの原則は大切ですが、必ずしも盲目的に従えば良いというわけではありません。
それぞれの原則が自分の構築しようとしているアプリケーションやライブラリにとって適切なトレードオフになっているかを考える必要があります。
今回はSinglePossibilityのトレードオフについて見ていきます。
Single Possibility
「クラスは基本的には1つの目的を果たすべきである。」
これは当然ですね。というより、普通に書いていたらこうなるような気がします。
問題は、既存の単一のクラスを複数のクラスに分割するときです。
クラスに色々と定義していると、「これは別のクラスに切り分けた方が良さそうだな」と思う時があると思います。
例えば、以下のようにEmployeeクラスがある場合、もちろん動作はします。
しかし、以下のような問題が挙げられると思います。
・コードの再利用性が低くなる
・テストが複雑になる
・クラスの機能が複雑になる
class Employee
def initialize(name, title, salary)
@name = name
@title = title
@salary = salary
end
def promote(new_title)
@title = new_title
end
def raise_salary(new_salary)
@salary = new_salary
end
def to_s
"#{@name} is a #{@title} earning #{@salary} dollars per year."
end
end
したがって、以下のようにクラスを分割するとコードを再利用できたり、何をするためのクラスなのかが明示的になります。
# 従業員の詳細情報を持つクラス
class EmployeeProfile
def initialize(name, title, salary)
@name = name
@title = title
@salary = salary
end
def to_s
"#{@name} is a #{@title} earning #{@salary} dollars per year."
end
end
# 従業員のアクションを扱うクラス
class EmployeeActions
def initialize(employee)
@employee = employee
end
def promote(new_title)
@employee.title = new_title
end
def raise_salary(new_salary)
@employee.salary = new_salary
end
end
# 従業員を表すクラス
class Employee
attr_accessor :title, :salary
def initialize(name, title, salary)
@profile = EmployeeProfile.new(name, title, salary)
@actions = EmployeeActions.new(self)
end
def to_s
@profile.to_s
end
def promote(new_title)
@actions.promote(new_title)
end
def raise_salary(new_salary)
@actions.raise_salary(new_salary)
end
end
ただし、この分割を思考停止してやれば良いわけではありません。
「新しく分割したクラスには、アプリケーションやライブラリで他にも使い所があるだろうか?」を問うことが大切で、それがイエスならば分割してもよさそうです。なぜならば、それを他でも再利用できるからです。
もし、そうでないならば分割はしない方が良い可能性が高いということです。
つまり、クラスが1つの目的を果たすことは重要なのですが、何でもかんでも分割していては逆に複雑さを生みかねないということです。
「複雑性とクラス分割のメリットはトレードオフになるため、どちらを取るか考慮した上でクラス分割しよう」 ということですね。