RoadMovie

write down memos or something I found about tech things

中規模Railsアプリのアーキテクチャ設計

Railsでアプリを作っていますか?設計に問題を抱えている、あるいは悩んでいませんか?もしそうであればこの記事が役に立つかもしれません。

アプリケーションが大きくなっていくに連れて、コードがカオスになってきたり、どこで何が起こっているのか追いにくくなってくるものです。いわゆる「ファットコントローラ」「ファットモデル」と呼ばれる問題ですね。ところで、どうしてこの問題が起こるのでしょうか。それはMVCアーキテクチャが常に完璧ではないからです。Railsはアプリケーションの立ち上げに関しては本当に簡単な方法を提供してくれています。素晴らしいです。単にMVCと呼ばれる構造に従ってコードを書いていけばアプリケーションを書けるようになるのです。ただ、前述のように、どこかのタイミングで設計の変更を余儀なくされていきます。もうあなたはそのフェーズにいるかも知れません。どうやって何を変更したら良いのでしょう。

ここでは、ひとつの解決方法として、中規模サイズのRailsアプリケーションにおけるストラテジーを紹介します。ここでの方法はDDD(Domein Driven Design)と呼ばれる手法をベースにしています。記事の最後に本を紹介しているので興味があればぜひ手にとってみてください。


Environment

f:id:mr7myself:20190126174244j:plain
設計図



コントローラ層

コントローラ層はすべてのリクエストを受け付けます。基本的に、この層のみがドメイン層を呼べます。この層は可能な限りシンプルに保たれるべきです。

  def save
    @request.assign_attributes(assignable_request_params)
    if @request.save
      RequestFlow::ReceiveFromAPI.execute(@request, @request_token)
      render json: { result: 'success' }
    else
      render json: { result: 'failed' }
    end
  end

ここでの RequestFlow::ReceiveFromApiドメインです。

ドメイン

これは最も重要な層のひとつです。ドメインは技術者のみでなく、ビジネスやカスタマーサポートの人にも理解可能な共通言語で名前付けしてください。このタイミングでみんなで話し合うのもいいですね。例えば私は SelectXXX, SendXXX, CreateXXX のようにつけることが多いです。ここでEmailなどの通知のようなフックイベントを定義します。理想的にはドメインはプロセス(手順)であったほうがよいです。

module RequestFlow
  module Update

    module_function

    def execute(request, options={})
        GeocodeJob.perform_later(request) unless request.geocoded?
        Pipedrive::Updator.update_parameters(
          PipedriveStage.hash[request.status],
          request.pipedrive_deal,
          request
        )
        request.update_status
      end

      unless options[:skip_notification] == true
        # This is a Service Layer
        Notifier::Request.complete_updating(request_offer)
      end
    end
  end
end 

ドメインはモデルとサービスを呼ぶことができます。しかし彼らから"呼ばれる"のは禁止です。 ドメインは中で何が起こっているのか理解しやすいようにしましょう。そうすると、このような会話が生まれることが想定されます。

CS: ユーザーがリクエストを更新したとき、何がおきるんだっけ? IT: ドメインをチェックしてみるね。そのリクエストは位置情報を付与されて、pipedriveに情報を投げているね。その後アップデート完了という流れだね。

いい感じですね。

サービス層

ここはステートレスな層です。サービスはコマンド(命令)のようなものだと思ってください。例えば、geocode_setter, locale_finder, distance_calculator などです。私の経験では Notifier (通知者)をここに置くのは良いと思います。

  class Notifier::Request
    def initialize(request, timing = :wait, skip_validation = false)
    end

    ...

    def complete_request
      klass = new(request)
      klass.notify_internaly
      klass.via_sms
      klass.via_email
    end

    def via_email
      # call ActionMailer
    end
  end

モデル層

サービス層とドメイン層のおかげで、モデル層は薄くできるようになったはずです。モデルは自身が何を表しているかだけに関心を持ちます。純粋なRubyクラスもどんどん作りましょう。ActiveRecord::Base を継承してることは必須ではないですからね。pureなRubyクラスは綺麗なデザイン、理解しやすい設計に大いに役立つはずです。

バリデーション層

この層もモデル層を薄く保つのに役立ちます。もしRailsにおいてバリデーション層をどう実装していいか知らなければ検索してみてください。実装方法は簡単で、ActiveModel::Validator を継承するだけです。 それから validates_with などを実装しましょう。当然ですが、ここではバリデーションのみに関心を持ちましょう。



これが基本的な設計の考え方にになります。もちろん、実際のプロダクトではもっと複雑になるでしょうけど、いままで私が関わったプロダクトではこのやり方はスケーラビリティにおいてもうまくいきました。この設計で重要なのは「誰が誰を呼べるか」の「方向(矢印)」です。これが正しく守られていないと、またカオスに逆戻りです。

もしもっとこの設計の理解を深めたければ下記の本がおすすめです。

それでは、Enjoy coding!