7
1

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.

はじめに

テキトーにタイトル考えたんですが「ぼくのかんがえたさいきょうの」って元ネタがキン肉マンの子供からの超人提案募集らしいですね。
子供たちが考えた超人を見るが如く、優しい目でご指摘頂けると助かります。

本題に入りまして、Railsは最初のうちは、Rails wayに乗ってれば結構サクサク書けるんですが、ある程度規模が大きくなってくると途端に管理が難しくなります。
ControllerとModelくらいしか使ってないプロジェクトをリファクタリングしていく方法を考えました。
そこまで大きくないプロジェクトなら、最初からこの方式でAPI組んでも良いかなと思います。

しっかりリファクタリングする時間があればいくらでもできると思うので、ある程度方針だけ決めて、それに沿って効果が高そうなものをまとめていきます。

基本方針

Controller

ControllerはAPIのinputを受け取るだけのレイヤーとして使います。
ロジックは全て後述するLogicにまとめて、返り値の整形はSerializerに任せてしまうのが良いと思います。
ただし、Controller内では入力のバリデーションを行います。入力のバリデーション用のクラスを準備しても良いと思いますが、ここでは省略します。

samples_controller.rb

class SamplesController < ApplicationController
    
  def create
    # 必要な入力に関してのバリデーションを入れる
    raise "some error" unless params[:name]
    raise "some error" if params[:age].to_i < 18

    # 処理を行う
    data = Logic::Sample::CreateService.call(name: params[:name], age: params[:age].to_i)

    # 返り値の整形
    resp = Serializer::Sample::CreateSerializer.call(data)
    
    # データを返す
    render status: 200, json: resp
  end
end

Model

Fat ControllerはダメだからロジックはModelに書けみたいな意見がありますが、Fat ModelはFat Modelで大変です。
この辺はチーム内で合議を取るのが良いかと思いますが、結構微妙なケースもあるので最低限のルールだけでも決めておくと良いと思います。

例えば、Modelに関係ないロジックも含めてしまえるので、ルールとしてクラスメソッドはモデルに関係ないロジックを含めないこと、インスタンスメソッドは受け取ったインスタンスに対してのみ処理(CRUD)を行うというルールが良いかなと思っています。

結局ルールによりますが、個人的にはモデル内で別のモデルの処理を入れるのは複雑化するので、反対ですね。
後述しますが、複数モデルにまたがるものはサービスクラス内に別メソッドとして切り出した方が良いです。

あとは他のモデルでも使えそうな一般的なメソッドはconcernに加えるなどするのが良いとは思いますが、ロジックが色々なところにあるとそれはそれで可読性下がるので、注意が必要です。

user.rb
class User < ApplicationRecord
    has_many :posts

    # ダメなクラスメソッドの例(ユーザーに関係ないモデルを作成している)
     def self.create_user_statistics
        user_count = self.all.size
        Statistics.create(user_count: user_count)
     end

    # 良くないインスタンスメソッドの例(最終的に変更が加えられるモデルがUserではない)
    def create_post(content:)
      self.posts.create!(content: content)
    end

    # 微妙なインスタンスメソッドの例(Postモデルにself.count_posted_by(user:)というクラスメソッド作ってもいいのかな)
    def count_post
      self.posts.size
    end
end

正直、モデルのリファクタリングはかなり大変です。
管理されてないと色々なロジックが入ってて、どこに影響するかも分かりません。
テストカバレッジがかなり高ければ問題ないとは思いますが、まずは影響が少なそうなロジックや、あえて既存ロジックは変えずに新規のロジックだけ上記ルールをしっかり作ると良いかもしれません。

Logic

Logicというディレクトリ作っても良いんですが、サービスクラスにまとめてしまって良いかなと思ってます。
受け取るControllerのActionごと、つまりエンドポイントごとに1ファイルを作成します。
以下の例ではapp/services/logic/sample/create_service.rbです。

# サービスクラスの大元をこのように定義しておくとnewする必要がなくなる
# Rubyのバージョンにもよるので以下参照のこと
# https://www.ruby-lang.org/ja/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/
class ApplicationService
  def self.call(*args, **kwargs, &block)
    new(*args, **kwargs, &block).call
  end
end

class Logic::Sample::CreateService < ApplicationService
  attr_reader :name, :age

  def initialize(name:, age:)
    @name = name
    @age = age
  end

  def call
    user = User.create!(name: name, age: age)
    {
      user: user,
    }
  end
end

ちなみにサービスクラスはなんでもつっこめてしまうので責務がわからなくなるということがあります。
この記事の中では、下記の3つの要素の時使うイメージです。

  • Logic(上記)
  • 外部APIや独立した処理(Slack通知など)
  • 複数モデルにまたがる処理(例えば、サインアップでアカウントモデルとユーザーモデルを同時に作成したいなど)

最後の複数モデルにまたがる処理に関しては、一番最後の応用編に貼ってあるリンクにあるInteractorとかに入れてしまっても良いかと思います。

Serializer

ここで返り値を良い感じに整形します。
ここではAlbaというgemを使います。

以下はドキュメントのサンプルコードですが、has_manyも定義ができて、まとめてSerializeすることができます。

class ArticleResource
  include Alba::Resource

  attributes :title
end

class UserResource
  include Alba::Resource

  attributes :id

  many :articles, resource: ArticleResource
end

UserResource.new(user).serialize
# => '{"id":1,"articles":[{"title":"Hello World!"}]}'

APIとしてschemaがしっかり定義されていれば上記の方法で、必要なattributesを記述して良い感じにオブジェクト作れるのですが、APIごとにモデルのどのattributeを使うかは決まってない場合には以下のように書くことで、呼び出し時にattributesを指定することもできます。

class UserResource
	attributes :id, :email, :name

	def attributes
		super.select { |key, _| params.dig(:user, :keys).include?(key) }
	end
end

UserResource.new(user, params: {user: {keys: [:id, email]}}).serialize
# => '{"id":1,"email":"test@example.com"}'

has_manyに対しても拡張もできるので、色々使い勝手は良いかと思います。

応用編

Railsのデザインパターンですが、validationとか目的ごとにクラスを切り出すのはよくやられてるので、こちらを参考にすると良いかと思います。
Railsのデザインパターンまとめ
サービスクラスとapp下によくあるディレクトリについて

まとめ

リファクタリングの基本方針をまとめました。
本当に最低限の方針ですので、規模が大きくなってきたらモデルとは別のドメインモデル作ったりとか、色々なパターンが考えられますが、方針が何もないよりはこのくらいルールがあれば、実装スピードを落とさずに、そこそこ見通しよくコーディングできるのかなと思います。

7
1
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
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?