初めに
良い設計の指針としてSOLID原則があります。
ただ多くの書籍で例としてあげられるのはjavaのコードだったりします。javaだとInterfaceを明示的に定義できるので例としてわかりやすいからかなと思います。そこで個人的な復習も兼ねて、普段から利用しているRubyでSOLID原則を説明していきます。
単一責任原則
単一責任原則は、よく以下のように説明されます。
1つのクラスは1つだけの責任を持たなければならない
クラスを変更する理由はたったひとつである
なるほどーとは思いますが、いまいちピンときません。
懺悔すると、「クラスっていろんなメソッドあるんだから、それぞれに違った責任あるだろ」、「クラスが変更される理由って色々あるだろ」って思ってました。
そこで、かの名著『Clean Architecture』を参照すると、
モジュールはたったひとつのアクターに対して責務を負うべきである。
と書かれています。アクターとはアプリケーションを利用するユーザや外部のシステムです。
言い換えれば、複数のアクターの仕様変更によってそのクラスが変更されるとき、そのクラスは単一責任原則に反しています。
と言っても分かりにくいので例を見てみます。
単一責任を破っている例
単一責任原則を説明されるときによく使用される例として、給与計算をとりあげます。
給与計算のメソッドとしてcalculate_payment
があります。さて、給与は労働時間*1000で定義されます。労働時間は1時間以下の四捨五入で計算されます。
class Engineer
def calculate_payment
working_hours * 1000
end
def working_hours
(endTime - startTime).round
end
end
労務部は、システムからcalculate_payment
を見てエンジニアの給与を支払います。一方、技術部ではエンジニアの工数を管理するためにworking_hours
を利用しています。
ある時、技術部で工数の計算を四捨五入ではなく、1時間以下も正確に把握したいという要望が上がりました。
そこで、working_hours
のround
を消します。しかし、その修正をしたエンジニアは給与計算でも同じメソッドを利用していることを知りませんでした...
こうして、バグに気づくまで間違った給与を払い続けることになってしまいます。
解決策
なぜ問題が起こったかといえば、もちろん単一責任原則を破っていたからです。
単一責任原則とは、
モジュールはたったひとつのアクターに対して責務を負うべきである。
でした。ここでのモジュールは、文脈によってクラスやメソッド、関数と言い換えることができます。
では先ほどの例を見てみましょう。まず、working_hours
メソッドは単一責任原則を守っていません。なぜなら、労務部と技術部の二つのアクターによって利用されているからです。各アクター固有の仕様変更によってworking_hours
メソッドは変更されます。
メソッドの分離
この問題の最も簡単な解決策は、working_hours
メソッドをそれぞれのアクター用に分けることです。例えば、payment_working_hours
とman_hours
に分けておくことです(たとえコードが重複するとしても)。すると、技術部が工数の計算方法を変更したとしても労務部でバグが出ません。これで少なくとも、メソッド単位での単一責任になりました。
しかし、クラス単位で見ると単一責任原則を守れていません。そもそも単一責任を守れないメソッドを作ってしまった原因はこのクラスの責任範囲が大きすぎることにあります。
クラスを分ける
このEngineer
クラスは労務部と技術部の二つのアクターから利用されるので、単一責任原則を破っています。なぜ単一責任を破ってしまうかというと、Engineer
は非常に大きな主語なので、複雑かつ大きな責務を負いがちな命名だからです。
どうのようにクラス設計すればよかったのでしょうか。クラスの分け方は様々考えられますが、例えばEngineerPayment
クラスとEngineerManHours
クラスに分けることも考えられれます。予めクラスを分けておけば、そもそもworking_hours
メソッドをそれぞれが使ってしまうことはありません。
このように単一責任なクラスを意識して設計しておくと、前述のようなバグを未然に防ぐことができます。
Railsではどうするの?
おまけ程度にRailsについて。
さて、Rubyを利用してる多くの現場では、フレームワークとしてRailsを採用しているんじゃないかと思います。Railsでは、DBのスキーマ構造がそのままモデルクラスへと反映されます。そのため、前述したような主語の大きなクラスが容易に作られます。
例えば、DBにengineers table
があれば当然Engineer
クラスが作られます。Engineer
クラスがあればその中でworking_hours
のようなメソッドが作られやすくなります。規模が大きくなれば、Engineerクラスは巨大で複雑なロジックを多数抱えるFatModelとなります。
これは、Railsのようなスピーディな開発を提供するフレームワークのデメリットです。もちろん、Railsにはそれを補ってあまりあるメリットがあるため、多くの企業で利用され続けています。
対策
RailsでFatなモデルが作られることは、ある程度仕方のないことです。しかし、できる限り対策はしておきたいものです。手のつけられないほどFatになったモデルをリファクタリングするのは、かなりの気合と時間を要します。
まず、第一にメソッドレベルの単一責任原則は必ず守ることが大切です。なので、working_hours
メソッドをそれぞれのアクターで利用してはいけません。必ず、別メソッドにしましょう。安易な共通化ほど危険な物はありません。
次の対策として、ActiveRecordに依存しないプレーンなrubyクラスを作成します。Engineer
クラスはどうしても責任を複数負いがちなクラスです。そんな時は各責任ごとにプレーンなrubyクラスを定義すると良いでしょう。
と言いつつ、リファクタリングが困難になるほどFatなモデルになることは稀で大体その前にサービス終了します()