リファクタリング~chatGPTと一緒に一問一答アプリを開発した話⑨~

eyecatch-wchatgpt-9

「一問一答アプリ × ChatGPT」

先日、ChatGPTを活用して一問一答アプリケーション「情報1 一問一答トレーニングGYM」を開発したことをお伝えしました。

▶︎こちらの記事で紹介しています。

iqatlogo2【ChatGPTと開発】高校「情報1」一問一答アプリ

私が開発したこのアプリは、ChatGPTの力を借りながら、実装していきました。

この記事では、一問一答アプリ × ChatGPTの開発の舞台裏や、実際の活用方法について詳しく解説します。

今回は

  • リファクタリング

するにあたって、どのように役立ったのか、その様子を紹介します!

ぜひこのシリーズの記事をチェックして、一問一答アプリの新たな魅力とChatGPTとの相乗効果を体感してください!

目的

img-4372v2

これまでひとしきり機能を実装してきましたが、必要な処理をコントローラーに全て書いていたので、あまりに冗長で不恰好になってしまっていたので、リファクタリングを実施しました。

リファクタリングを楽にできたり、どういう観点で行おうかとchatGPTに相談してみました。

対話履歴8(リファクタリング)
対話履歴8−2(リファクタリング)
コトゼニ
コトゼニ

リファクタリングしたいと思います。下記のコントローラーのコードが冗長であるため、モデルが行う処理と分割したいと考えています。今のコードによる挙動をそのまま維持しながら必要に応じてメソッドを定義し、リファクタリングしてください。▼コード:〜

chatGPT
chatGPT

現在のコントローラーのコードはかなり冗長ですが、問題を分離してメソッドに分割することで、コードを簡潔にできます。

まずは、セッションの初期化と取得する処理を、メソッドinitialize_sessionとして分割しましょう。

def ask
  if request.get?
    render html: '<script>alert("選択内容が解除されトップページに戻ります。");window.location.href = "/";</script>'.html_safe
  elsif request.post?
    initialize_session

    # 出題範囲が選択されているか否かで条件分岐
    # 出題範囲が選択されていない場合
    if params[:genre].blank?
      # Genreモデルからカラム「id」の値だけを取得し、セッションに格納する(これで全ての分野のidを取得できる)
      genre_ids = Genre.pluck(:id)
      session[:selected_genre_ids] << genre_ids
    else
      # 選択された分野をparamsから受け取り、セッションに格納する
      genre_ids = params[:genre][:genre_ids].reject(&:empty?)
      if !session[:selected_genre_ids].include?(genre_ids)
        session[:selected_genre_ids] << genre_ids
      end
    end

    # 難易度をパラムスから取得してモデルに渡す
    if params[:level].blank?
      # Levelモデルからカラム「id」の値だけを取得し、セッションに格納する(これで全ての分野のidを取得できる)
      level_ids = Level.pluck(:id)
      session[:selected_level_ids] << level_ids
    else
      # 選択された難易度をparamsから受け取り、セッションに格納する
      level_ids = params[:level][:level_ids].reject(&:empty?)
      if !session[:selected_level_ids].include?(level_ids)
        session[:selected_level_ids] << level_ids
      end
    end

    # 出題数を格納
    @question_nums = Question.where(genre_id: genre_ids, level_id: level_ids).count
    session[:question_nums] << @question_nums

    # genre_idsに入っている分野idと一致するインスタンスを1つ取得する
    @question = Question.where(genre_id: genre_ids,level_id: level_ids).order("RAND()").first

    if @question.present?
      # session[:asked_question_ids]に取得したインスタンスの問題idを格納する。
      if !session[:asked_question_ids].include?(@question.id)
        session[:asked_question_ids] << @question.id
      end

      #取得した問題インスタンスに付随する選択肢を取得する。
      @choices = @
…(省略)

ただ、リファクタリングそのものを依頼するとコードの表示が切れるため、観点を整理するくらいが良いのかもしれません。

手順:モデルが管理する項目を切り離し、モデルにメソッドを定義する

programming

コントローラーでは種々の条件を満たすレコードをモデルが引っ張ってくる、という挙動だけにすることを中心にします。

また、モデルはデフォルトではセッションを操作することができないため。なるべくセッションはRailsのコントローラーの中で実行されるアクションにする。(モデルでセッションを使いたい場合、コントローラーからセッションを渡して、それをモデル内で利用する必要があります。例えば、コントローラーからモデルのメソッドを呼び出す際に、引数としてセッションを渡す)。

これらを整理して、リファクタリングを実施しました。

コントローラーについてはいくつかのメソッドを省略しています。

class QuestionsController < ApplicationController

before_action :renderHTML, only: [:index, :ask, :next_question, :retry, :print] 
def index
end
def ask
			initialize_session
			genre_ids = Genre.select_genre(session, params)
			level_ids = Level.select_level(session, params)

      # 出題数を格納
      @question_nums = Question.where(genre_id: genre_ids, level_id: level_ids).count
      session[:question_nums] << @question_nums

      # genre_idsに入っている分野idと一致するインスタンスを1つ取得する
      @question = Question.where(genre_id: genre_ids,level_id: level_ids).order("RAND()").first

      if @question.present?
        # session[:asked_question_ids]に取得したインスタンスの問題idを格納する。
        if !session[:asked_question_ids].include?(@question.id)
          session[:asked_question_ids] << @question.id
        end

        #取得した問題インスタンスに付随する選択肢を取得する。
        @choices = @question.choices.includes(:question) #questionとアソシエーション関係なので()の中にはカラム名ではなく関係のあるモデル名にする「_id」は不要
        @answer = Answer.find(@question.id)
    
      else
        redirect_to root_path, alert: "出題できる問題がありません。"
      end
  end

def next_question
      session = request.session
			@question = Question.selectQuestion(session)
        # session[:asked_question_ids]に取得したインスタンスの問題idを追加格納する。
      if @question.present?
          session[:asked_question_ids] << @question.id
          @choices = @question.choices.includes(:question) #questionとアソシエーション関係なので()の中にはカラム名ではなく関係のあるモデル名にする「_id」は不要
          @answer = Answer.find(@question.id)

          if session[:question_nums].present?
            @question_nums = session[:question_nums]
          end
          
          # -1は出題数を調整するため
          @asked_question_count = count_asked_question
          @correct_counts = count_correct_counts

          # to_fは浮動小数(float)への変換
					
          @correct_rate = Question.calcCorrectRate(@asked_question_count, @correct_count)          # 呼び出すビューファイルはaskアクションのビューファイル
          render :ask
      else
          redirect_to score_questions_path
      end 
  end

def retry

      # セッションを調節し、間違えた問題を取得する
      incorrect_question_ids =  adjustSession
      # 出題数を格納
      @question_nums = incorrect_question_ids.length
      session[:question_nums] << @question_nums
      # 不正解問題のidでレコードを取得する
      @question = Question.where(id: incorrect_question_ids).order("RAND()").first
      if @question.present?
          # 不正解問題のidを出題済みセッションに格納する
          session[:asked_question_ids] << @question.id
          @choices = @question.choices.includes(:question) #questionとアソシエーション関係なので()の中にはカラム名ではなく関係のあるモデル名にする「_id」は不要
          @answer = Answer.find(@question.id)
          render :ask
      else
        redirect_to score_questions_path
      end 
    
  end

def print
      # 出題数をパラムスより取得する
      question_count = params[:question_count].to_i
      
			genre_ids = Genre.select_genre(params)
			level_ids = Level.select_level(params)

      # 選択された出題形式を読み取る
      question_order = params[:question_order].to_i
			@questions = Question.selectQuestionByOrder(question_order, genre_ids, level_ids, question_count)
      @style_id =  params[:style_id].to_i
  end


	def initialize_session
	    # まずはセッション変数を取得する
      # next_questionメソッドで使うため
      # session[:asked_question_ids] ||= [] でセッションが未定義なら新しく作るという意味になる
      session[:asked_question_ids] ||= []
      session[:selected_genre_ids] ||= []
      # 正答数を計上するためのセッションを取得する
      session[:correct_answers]||= []
      # 難易度を格納するためのセッション
      session[:selected_level_ids] ||= []
      # 出題数を格納するためのセッション
      session[:question_nums] ||= []
      # 間違えた問題を格納するためのセッション
      session[:incorrect_answers] ||= []
	end

	def adjustSession
	    # いくつかのセッションをリセットする
      session[:asked_question_ids] = []
      # 出題数を格納するためのセッション
      session[:question_nums] = []
      # 不正解問題のidを取得する
      incorrect_question_ids = session[:incorrect_answers]
      session[:incorrect_answers] = []
      if session[:question_range].present?
        session[:question_range] = []
      else
        session[:question_range]||= []
      end
      session[:question_range] << incorrect_question_ids
	end

private
def renderHTML
    if request.post?
      return
    else
      render html: '<script>alert("選択内容が解除されトップページに戻ります。");window.location.href = "/";</script>'.html_safe
    end
  end

  def count_asked_question
    return (session[:asked_question_ids].length - 1)
  end

  def count_correct_counts
    return session[:correct_answers].length
  end
end
	def self.selectQuestion(session)
     if session[:question_range] .present?
       question = Question.where(id: session[:question_range]).where.not(id: session[:asked_question_ids]).order("RAND()").first
     else
       #当メソッドを呼び出す際には最低1回はask~resultメソッドを実行している。そのため、セッションを参考にして分野を指定しながら出題済みの問題を避けて次の問題(インスタンス)を取得できる。
       question = Question.where(genre_id: session[:selected_genre_ids],level_id: session[:selected_level_ids]).where.not(id: session[:asked_question_ids]).order("RAND()").first
     end
		return question
	end

	def self.calcCorrectRate(asked_question_count, correct_counts)
	  # to_fは浮動小数(float)への変換
	  correct_rate = ((correct_counts.to_f / asked_question_count.to_f) * 100).round(1)
	  return correct_rate
	end

	def self.selectQuestionByOrder(question_order)
    # 出題形式が「ランダム」の場合
	  if question_order == 1
	    questions = Question.where(genre_id: genre_ids, level_id: level_ids).order("RAND()").limit(question_count)
	    # 出題形式が「番号順」の場合
	  elsif question_order == 2
	    questions = Question.where(genre_id: genre_ids, level_id: level_ids).order(id: "ASC").limit(question_count)
	  end
		return questions
	end
def self.select_genre(session, params)
		  # 出題範囲が選択されているか否かで条件分岐
      # 出題範囲が選択されていない場合
      if params[:genre].blank?
        # Genreモデルからカラム「id」の値だけを取得し、セッションに格納する(これで全ての分野のidを取得できる)
        genre_ids = Genre.pluck(:id)
        session[:selected_genre_ids] << genre_ids
      else
        # 選択された分野をparamsから受け取り、セッションに格納する
        genre_ids = params[:genre][:genre_ids].reject(&:empty?)
        if !session[:selected_genre_ids].include?(genre_ids)
          session[:selected_genre_ids] << genre_ids
        end
      end
			return genre_ids
end

def self.select_genre(params)
    # 出題範囲が選択されているか否かで条件分岐
    # 出題範囲が選択されていない場合
    if params[:genre].blank?
      # Genreモデルからカラム「id」の値だけを取得し、セッションに格納する(これで全ての分野のidを取得できる)
      genre_ids = Genre.pluck(:id)
    else
      # 選択された分野をparamsから受け取り、セッションに格納する
      genre_ids = params[:genre][:genre_ids].reject(&:empty?)
    end
    return genre_ids
  end
def self.select_level(session, params)
      # 難易度をパラムスから取得してモデルに渡す
      if params[:level].blank?
        # Levelモデルからカラム「id」の値だけを取得し、セッションに格納する(これで全ての分野のidを取得できる)
        level_ids = Level.pluck(:id)
        session[:selected_level_ids] << level_ids
      else
        # 選択された難易度をparamsから受け取り、セッションに格納する
        level_ids = params[:level][:level_ids].reject(&:empty?)
        if !session[:selected_level_ids].include?(level_ids)
          session[:selected_level_ids] << level_ids
        end
      end

			return level_ids
end

 def self.select_level(params)
    # 難易度をパラムスから取得してモデルに渡す
    if params[:level].blank?
      # Levelモデルからカラム「id」の値だけを取得し、セッションに格納する(これで全ての分野のidを取得できる)
      level_ids = Level.pluck(:id)
    else
      # 選択された難易度をparamsから受け取り、セッションに格納する
      level_ids = params[:level][:level_ids].reject(&:empty?)
    end
    return level_ids
  end

まとめ

今回の開発作業で

  • リファクタリング

を実現することができました。

ChatGPTを利用すると開発がグンと楽になりますね!ChatGPTの可能性を体験してみてはいかがでしょうか?

ChatGPTを利用すると開発がグンと楽になりますね!

最後まで読んでいただきありがとうございました。

この記事が少しでも参考になれば幸いです。

それではまたここで会いましょう!

ブログランキング・にほんブログ村へにほんブログ村

ブログ村に参加中です。上のバナーをクリックいただくだけで当ブログにポイントが入ります。いつも応援クリックありがとうございます。

Twitter

Twitter やってます。

新着記事のお知らせをしていますのでぜひフォローしてください!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA