3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

RubyでSOLID原則1: 単一責任原則

Posted at

初めに

良い設計の指針として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_hoursroundを消します。しかし、その修正をしたエンジニアは給与計算でも同じメソッドを利用していることを知りませんでした...
こうして、バグに気づくまで間違った給与を払い続けることになってしまいます。

解決策

なぜ問題が起こったかといえば、もちろん単一責任原則を破っていたからです。
単一責任原則とは、

モジュールはたったひとつのアクターに対して責務を負うべきである。

でした。ここでのモジュールは、文脈によってクラスやメソッド、関数と言い換えることができます。
では先ほどの例を見てみましょう。まず、working_hoursメソッドは単一責任原則を守っていません。なぜなら、労務部と技術部の二つのアクターによって利用されているからです。各アクター固有の仕様変更によってworking_hoursメソッドは変更されます。

メソッドの分離

この問題の最も簡単な解決策は、working_hoursメソッドをそれぞれのアクター用に分けることです。例えば、payment_working_hoursman_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なモデルになることは稀で大体その前にサービス終了します()

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?