Programming Journal

学習したことの整理用です。

【Rails】FatControllerをスッキリさせる

実装したいこと

  • 記事を投稿するアプリの編集画面で、記事のステータスを「下書き」「公開」「公開待ち」に分類したい。

  • ステータスと公開日時は編集時に選択可能。ただし、公開日時によって記事のステータスを「公開」「公開待ち」に自動で判定して変更する。

  • ステータスを「下書き」に指定したときは、「下書き」のままにする。

前提

enumで記事のステータスを定義しています。

enum state: { draft: 0, published: 1, publish_wait: 2 }

コントローラーは2つに分けています。
articles_controller.rb 記事全般の挙動を持つコントローラ
publishes_controller.rb 記事を公開するためのコントローラ

FatController

私が誤ったコード 反省と復習用にみていきます。

  def update
    authorize(@article) 

    if @article.update(article_params)
      if @article.state == 'draft' #下書きのときはそのまま
        @article.draft!
      elsif Time.current >= @article.published_at #公開日時が現在〜過去のとき
        @article.published! #公開
      elsif Time.current < @article.published_at #公開日時が未来のとき
        @article.publish_wait! #公開待ち
      end
      flash[:notice] = '更新しました'
      redirect_to edit_article_path(@article.uuid)
    else
      render :edit
    end
  end

Time.currentpublished_at(公開日時)を比較し、公開日時が現在〜過去のときは、articleのステータスをpublishedに更新しています。
@article.draft!のように直接属性を繋げて書けるのは、enumで定義しているためです。

 def update
    @article.published_at = Time.current unless @article.published_at? 
    
    if @article.valid?
      if Time.current >= @article.published_at
        @article.published!
        flash[:notice] = '記事を公開しました'
      elsif Time.current < @article.published_at
        @article.publish_wait!
        flash[:notice] = '記事を公開待ちにしました'
      end
      redirect_to edit_article_path(@article.uuid)
    else
      flash.now[:alert] = 'エラーがあります。確認してください。'
      @article.state = @article.state_was if @article.state_changed?
      render 'admin/articles/edit'
    end
  end

こちらも、選択した公開日時を現在日時と比較して、「公開」or 「公開待ち」のステータスに変更しています。

何がダメなのか

  • ステータスの判定でenum用のメソッドを使っていない。 (例)@article.draft?
  • コントローラで判定の処理をしていて、FatControllerになっている。

改善

日時で判定してステータスを変更する処理はmodelに切り出していきます。

諸々をモデルへ切り分ける

コントローラで判定していたものを以下のメソッドに移していきます。

  • 日時で公開可能か判定 #1
  • 判定結果ごとのメッセージ分け #2
  • ステータス(state)の調整 #3
  def publishable? #1
    Time.current >= published_at
  end

  def message_on_published #2
    if published?
      '公開しました'
    elsif publish_wait?
      '公開待ちにしました。'
    end
  end

  def adjust_state #3
    return if draft? 

    self.state = if publishable?
                    :published
                 else
                    :publish_wait
                 end
  end

#3のメソッドについて詳しくみていきます。
return if draft? draftだった場合はreturnメソッドで式を抜けます。

self.state = 〜 selfをつけないと、stateに代入することになってしまうので、省略不可です。
article.stateのように使われます。
if~else文の戻り値をself.stateに代入しています。

修正したコントローラ

  def update
    authorize(@article)

    @article.assign_attributes(article_params)
    @article.adjust_state #3でステータスを調整
    if @article.save
      flash[:notice] = '更新しました'
      redirect_to edit_article_path(@article.uuid)
    else
      render :edit
    end
  end

assign_attributesで送られたパラメーターの属性をまとめて上書きする。DBは更新されないので、saveは必要。

ActiveModel::AttributeAssignment
まとめてオブジェクトの属性を変更したい時に便利!assign_attributesメソッド - その辺にいるWebエンジニアの備忘録


  def update
    @article.published_at = Time.current unless @article.published_at?
    @article.state = @article.publishable? ? :published : :publish_wait

    if @article.valid?
      flash[:notice] = @article.message_on_published
      redirect_to edit_admin_article_path(@article.uuid)
    else
      flash.now[:alert] = 'エラーがあります。確認してください。'
      @article.state = @article.state_was if @article.state_changed?
      render 'admin/articles/edit'
    end
  end

@article.state = @article.publishable? ? :published : :publish_wait 三項演算子です。
@article.publishable?が真ならpublishedを、偽ならpublish_wait@article.stateに代入します。

どちらのコントローラもまず、ステータス判定処理をしてから、保存処理をしています。

最初に実装したコードはif文をネストさせており、かなり読みにくかったです。 コードを読むことはできても、いざ自分で実装!となると汚くなってしまいます。。
英語の勉強と同じで、反復あるのみだと思うのでがんばります。