Railsで学ぶSOLID(1): 単一責任の原則(翻訳)

追記: 訳文修正いたしました(2018/03/28)。 概要 原著者の許諾を得て翻訳・公開いたします。 英語記事: SOLID Principles #1: Single Responsibility Principle | Netguru Blog on Ruby/Ruby on Rails 原文公開日: 2018/02/13 著者: Marcin Jakubowski サイト: netguru 翻訳記事の相互リンクは今後更新いたします。 Railsで学ぶSOLID(1): 単一責任の原則(翻訳) 「SOLIDの原則シリーズ」へようこそ。このシリーズ記事では、SOLIDの原則をひとつずつ詳しく説明し、分析します。シリーズの最後にはいくつかのヒントや考察を含む総括記事をお送りしますのでどうぞご期待ください。 それでは始めましょう。「SOLIDの原則」とはそもそも何なのでしょうか?SOLIDとは、オブジェクト指向プログラミング設計における一般的な原則であり、ソフトウェアをより理解しやすくし、拡張性やメンテナンス性やテストのしやすさを高めることを目的としています。 単一責任の原則(SRP: Single responsibility principle)(本記事) オープン/クローズの原則(OCP: Open/closed principle) リスコフの置換原則(LSP: Liskov Substitution Principle) インターフェイス分離の原則(ISP: Interface Segregation Principle) 依存関係逆転の原則(DIP: Dependency Inversion Principle) 今回は、最初の原則である「単一責任の原則」について詳しく見ていくことにします。 単一責任の原則(SRP) クラス(オブジェクト)が担う責任は1つに限定すべきである(かつその責務は完全にカプセル化されるべきである)。 あるいは クラスを変更するときの理由付けは、1つしかあってはならない。 これをもっと易しく言い換えれば「1つのクラスは1つの仕事しかしない」ということです。 しかし、クラスの責務というものはどうやって決まるのでしょうか?「変更する理由」というのはどのようにして決まる決めるものなのでしょうか?この問いに答えるのは難しく、容易ではありません。 次の文面について考えてみましょう。「クラスが更新される(書き換わる)理由は1つだけにとどまるべきである(ここで言う書き換えの対象はファイルのコードではなく、オブジェクトのメモリ上のステートを指すオブジェクトのメモリ上のステートではなくファイルのコードを指す)」。本記事ではこの理由を追求してみることにします。 訳注: 上記修正しました。 例: ユーザーを1人作成したいとします。最初に必要なのはデータのバリデーションです。この機能をService Objectで作成してみましょう。おそらく次のようなクラスになるでしょう。 class UserCreateService def initialize(params) @params = params end def call return false unless valid? process_user_data end private def valid? # 複雑なバリデーションロジックを書く … end def process_user_data … end end このクラスはどんなことをするでしょうか?このクラスは「ユーザー入力のバリデーション」を行い、続いてそれを「処理」(おそらくユーザーをデータベースに保存)しています。「バリデーション」と「処理」という2つの操作はいかにも別物感があり、どうやら2つの異なる責務のようです。しかし上のやり方を修正または改良する前に、なぜ上のようなやり方が良くないのかを明らかにしなければなりません。 何より注目したいのは、2つの処理が強く結合していることです。この2つの処理の一方を別の場所で再利用するのは簡単ではありません。このようなクラスのメンテはつらいものになり、ミスを誘発する可能性が増えてしまいます。そしてテストも面倒になってしまいます(カバーしなければならないケースが増えてしまうため)。 そういうことであれば、バリデーションがらみのコードを別のクラスに切り出すのはいい考えかもしれません。とりあえずやってみましょう。 class UserClassService def initialize(params, validator: UserValidator) @params = params @validator = validator end def call return false unless validator.new(params).validate process_user_data end private attr_reader :params, :validator def process_user_data … end end class UserValidator def initialize(params) @params = params end def validate … #=> true/false end end クラスが2つになり、それぞれのクラスが担当する責任は1つだけになっています。最初のクラスは「データの処理」、次のクラスは「データのバリデーション」です。2つのクラスの結合はもう強くありません。バリデーションルールを変更しなければならなくなったとしても、UserValidationクラスだけを変更すれば済みます。同様に、バリデーション後の操作を変更しなければならなくなったとしても、UserCreateServiceクラスに手を付ける必要はもうありません。 しかも、それぞれのクラスを単独で再利用できるようになっています。UserValidationクラスを他のコンテキストで使うことも、UserCreateServiceを別のバリデーションクラスで使うこともできます(ここでは「依存性の注入: dependency injection」を使いました)。 他にもメリットはあるのでしょうか?もちろんです!コードには必ずテストも書くものです。今後はバリデーションと処理サービスのテストを個別に(分離した形で)書き、UserCreateServiceのテストではUserValidatorのレスポンスをモック化することになります。 だがしかし… ドメイン駆動開発(DDD)という言葉はおそらくご存知かと思います。DDDという概念は、「情報エキスパート」(information expert)や「厚いドメインモデル」(薄いドメインモデルの対概念)といった用語に関連します。 訳注: 既存訳が見当たらないため、ここではrich domain modelを「厚いドメインモデル」、anemic domain modelを「薄いドメインモデル」と中立に訳出しました。 ここで「厚いドメインモデル」について少し考えてみましょう。この概念では、あらゆる振る舞いをそのモデルの「特定のモデル」に強く結合させておくことが前提になっています。そこではデータとそれを操作するロジックが同じ場所に置かれます。オブジェクト指向の話をしているので、これは自明に思えます。 次の例をちょっとご覧ください。 class User < ActiveRecord::Base validates :email, :first_name, :last_name, :presence: true def notify(notification) … end def softDelete deleted_at = Time.zone.now end end なるほど、バリデーション、ユーザーへの通知ロジック、ソフトデリートのロジックが同居していて、責務を抱えすぎている趣があります。さてここで問題です。「このコードはSRPに違反しているでしょうか?」 私の回答は決まっています。「いいえ、何も違反していません」 その理由は、ドメインモデルの責務が「自身のデータと一貫性」ただそれだけに限定されているからです。 私にとって、これは常にコンテキストによって決まるものです。 「おいおい、ついさっき『1つのクラスに2つの操作があるのはダメダメ』って書いたばっかりじゃないの?」とツッコまれるかもしれません。しかし、SRPに違反しているかどうかはやはり「すべてコンテキスト次第」なのです。 … Continue reading Railsで学ぶSOLID(1): 単一責任の原則(翻訳)