Ruby | 2週間で身につく、アプリを作りながら学ぶRubyの基本 - 13日目

Ruby Ruby
スポンサーリンク

13日目のゴールとテーマ

13日目のテーマは「アプリ全体を“ひとまとまり”として扱う:Appクラス化と設計の整理」です。
ここまでで、名簿アプリとタスク管理アプリをそれぞれ育ててきましたが、コードはまだ「トップレベルにメソッドが並んでいる状態」になっているはずです。

今日はここから一歩進めて、

アプリ全体を表す App クラスを作る
メインループやメニュー処理を App の中にまとめる
「状態(データ)」と「振る舞い(処理)」をセットで考える

という、「設計の目線」を強く意識した一日をやっていきます。
機能を増やすというより、「今あるものをきれいに箱に入れ直す」イメージです。


なぜAppクラスにまとめるのか

ばらけたコードの“モヤモヤ”を言葉にする

今のタスク管理アプリを思い出してみてください。
Task クラスがあり、その外側に show_menu、read_menu_number、handle_show_tasks、handle_add_task、handle_mark_task_done、save_tasks、load_tasks などが並び、最後に tasks = load_tasks から始まるメインループがある、という構造になっているはずです。

動きとしては問題なくても、ファイル全体を眺めると「どこが入口で、どこが中心なのか」が少し分かりにくい状態です。
ここで「アプリ全体を表すクラス」を用意して、その中に「アプリの流れ」を閉じ込めると、コードの見通しが一気によくなります。

最終的に、起動コードが

app = TaskApp.new
app.run
Ruby

の二行だけになると、「ああ、このファイルはタスクアプリなんだな」と一目で分かるようになります。
この「入口がはっきりしている感覚」が、設計としてとても大事です。


TaskAppクラスの骨組みを作る

まずは最低限の形から始める

タスク管理アプリ専用のクラスとして、TaskApp を作ってみます。
最初は「中身はほぼ今のメインループを移しただけ」で構いません。

class TaskApp
  def initialize
    @tasks = load_tasks
  end

  def run
    loop do
      show_menu
      choice = read_menu_number

      if choice == 0
        puts "アプリを終了します。"
        save_tasks(@tasks)
        break
      elsif choice == 1
        handle_show_tasks(@tasks)
      elsif choice == 2
        handle_add_task(@tasks)
      elsif choice == 3
        handle_mark_task_done(@tasks)
      elsif choice == 4
        handle_show_unfinished(@tasks)
      elsif choice == 5
        handle_show_done(@tasks)
      elsif choice == 6
        handle_show_sorted_by_deadline(@tasks)
      elsif choice == 7
        handle_toggle_task_done(@tasks)
      else
        puts "不正な番号です。もう一度入力してください。"
      end
    end
  end
end
Ruby

ここでの重要ポイントは二つあります。
一つ目は、initialize で @tasks を用意していることです。これは「このアプリが抱えているタスク一覧」です。外から配列を渡すのではなく、「アプリ自身が自分のタスクを持っている」という形にしています。
二つ目は、run が「アプリのメインループそのもの」になっていることです。今までトップレベルに書いていたループが、TaskApp の中に引っ越してきただけですが、「アプリの中心はここだ」とはっきり示せるようになりました。


メソッドをTaskAppの中に“引っ越し”させる

外に散らばっている関数を中にまとめる

次にやるのは、今まで外側に定義していたメソッドを、少しずつ TaskApp の中に移していく作業です。
例えば show_menu は、こう書き換えられます。

class TaskApp
  def show_menu
    puts "========================"
    puts "タスク管理アプリ メニュー"
    puts "1: すべてのタスクを表示"
    puts "2: 新しいタスクを追加"
    puts "3: タスクを完了にする"
    puts "4: 未完了のタスクだけ表示"
    puts "5: 完了済みのタスクだけ表示"
    puts "6: 締め切りが近い順に表示"
    puts "7: タスクの完了状態を切り替える"
    puts "0: 終了"
    puts "番号を入力してください:"
  end
end
Ruby

read_menu_number も TaskApp の中に入れます。

class TaskApp
  def read_menu_number
    loop do
      input = gets
      return 0 if input.nil?

      text = input.chomp

      if text.match?(/\A[0-9]+\z/)
        return text.to_i
      else
        puts "数字で入力してください。もう一度どうぞ:"
      end
    end
  end
end
Ruby

handle_show_tasks などの処理も、同じようにクラスの中に移していきます。
ここで一つ、設計として大事な選択肢が出てきます。

今までは handle_show_tasks(tasks) のように、配列を引数で渡していました。
TaskApp の中に入れるなら、引数をやめて @tasks を直接使う、という書き方もできます。

例えば、こうです。

class TaskApp
  def handle_show_tasks
    if @tasks.empty?
      puts "まだタスクが登録されていません。"
      return
    end

    puts "========================"
    puts "タスク一覧:"

    @tasks.each_with_index do |task, idx|
      puts "#{idx + 1}. #{task.summary}"
    end
  end
end
Ruby

そして run の中では、こう呼びます。

elsif choice == 1
  handle_show_tasks
Ruby

このように、「アプリが持っている状態(@tasks)」をクラスの中で直接扱うようにすると、引数の受け渡しが減ってコードがすっきりしていきます。
「このクラスは何を覚えていて、何をするのか」をセットで考える感覚が、ここで育ちます。


保存・読み込みもTaskAppの責任にする

外の関数から「アプリのメソッド」に格上げする

save_tasks と load_tasks も、TaskApp の中に移してしまいましょう。
まずは定数としてファイル名を持たせます。

class TaskApp
  TASK_DATA_FILE = "tasks_data.txt"

  def load_tasks
    tasks = []

    unless File.exist?(TASK_DATA_FILE)
      puts "タスクの保存ファイルがまだありません。(初回起動かもしれません)"
      return tasks
    end

    begin
      File.open(TASK_DATA_FILE, "r") do |file|
        file.each_line do |line|
          next if line.strip == ""
          task = task_from_csv_line(line)
          tasks << task
        end
      end
      puts "タスクをファイルから読み込みました。(#{tasks.length}件)"
    rescue => e
      puts "タスク読み込み中にエラーが発生しました。"
      puts "エラー内容: #{e.class} #{e.message}"
    end

    tasks
  end

  def save_tasks(tasks)
    File.open(TASK_DATA_FILE, "w") do |file|
      tasks.each do |task|
        file.puts task.to_csv_line
      end
    end
    puts "タスクをファイルに保存しました。(#{TASK_DATA_FILE})"
  end
end
Ruby

ここで一歩進めて、「save_tasks も @tasks を直接使う」形にしてみます。

class TaskApp
  def save_tasks
    File.open(TASK_DATA_FILE, "w") do |file|
      @tasks.each do |task|
        file.puts task.to_csv_line
      end
    end
    puts "タスクをファイルに保存しました。(#{TASK_DATA_FILE})"
  end
end
Ruby

すると、run の中はこう書けます。

if choice == 0
  puts "アプリを終了します。"
  save_tasks
  break
end
Ruby

この変化は小さく見えますが、「アプリの状態(@tasks)と、その状態を保存する処理(save_tasks)が同じクラスの中にある」というのは、とても自然な形です。
「このクラスは、自分の状態を自分で保存できる」という自己完結した感じが出てきます。


TaskAppの全体像を整える

典型的な構造を一度通して眺める

ここまでの話をまとめると、TaskApp はだいたい次のような構造になります。

require "date"

class Task
  # ここに Task クラス(前日までに作ったもの)
end

class TaskApp
  TASK_DATA_FILE = "tasks_data.txt"

  def initialize
    @tasks = load_tasks
  end

  def run
    loop do
      show_menu
      choice = read_menu_number

      if choice == 0
        puts "アプリを終了します。"
        save_tasks
        break
      elsif choice == 1
        handle_show_tasks
      elsif choice == 2
        handle_add_task
      elsif choice == 3
        handle_mark_task_done
      elsif choice == 4
        handle_show_unfinished
      elsif choice == 5
        handle_show_done
      elsif choice == 6
        handle_show_sorted_by_deadline
      elsif choice == 7
        handle_toggle_task_done
      else
        puts "不正な番号です。もう一度入力してください。"
      end
    end
  end

  def show_menu
    puts "========================"
    puts "タスク管理アプリ メニュー"
    puts "1: すべてのタスクを表示"
    puts "2: 新しいタスクを追加"
    puts "3: タスクを完了にする"
    puts "4: 未完了のタスクだけ表示"
    puts "5: 完了済みのタスクだけ表示"
    puts "6: 締め切りが近い順に表示"
    puts "7: タスクの完了状態を切り替える"
    puts "0: 終了"
    puts "番号を入力してください:"
  end

  def read_menu_number
    loop do
      input = gets
      return 0 if input.nil?

      text = input.chomp

      if text.match?(/\A[0-9]+\z/)
        return text.to_i
      else
        puts "数字で入力してください。もう一度どうぞ:"
      end
    end
  end

  def handle_show_tasks
    if @tasks.empty?
      puts "まだタスクが登録されていません。"
      return
    end

    puts "========================"
    puts "タスク一覧:"

    @tasks.each_with_index do |task, idx|
      puts "#{idx + 1}. #{task.summary}"
    end
  end

  def handle_add_task
    puts "新しいタスクを登録します。"
    task = build_task_from_input
    @tasks << task
    puts "タスクを登録しました。"
  end

  # ここに handle_mark_task_done, handle_show_unfinished,
  # handle_show_done, handle_show_sorted_by_deadline,
  # handle_toggle_task_done などが続く

  def save_tasks
    File.open(TASK_DATA_FILE, "w") do |file|
      @tasks.each do |task|
        file.puts task.to_csv_line
      end
    end
    puts "タスクをファイルに保存しました。(#{TASK_DATA_FILE})"
  end

  def load_tasks
    tasks = []

    unless File.exist?(TASK_DATA_FILE)
      puts "タスクの保存ファイルがまだありません。(初回起動かもしれません)"
      return tasks
    end

    begin
      File.open(TASK_DATA_FILE, "r") do |file|
        file.each_line do |line|
          next if line.strip == ""
          task = task_from_csv_line(line)
          tasks << task
        end
      end
      puts "タスクをファイルから読み込みました。(#{tasks.length}件)"
    rescue => e
      puts "タスク読み込み中にエラーが発生しました。"
      puts "エラー内容: #{e.class} #{e.message}"
    end

    tasks
  end
end

app = TaskApp.new
app.run
Ruby

このように、「クラス定義 → アプリクラス定義 → 最後に app.run」という流れになっていると、ファイルを開いた瞬間に全体像がつかみやすくなります。
ここまで来ると、もう「スクリプト」ではなく「小さなアプリケーション」という雰囲気になっているはずです。


13日目で一番大事な感覚

「状態と振る舞いをセットで持つ」という発想

今日やったことを一言で言うと、「アプリ全体を1つのオブジェクトとして扱うようにした」です。
TaskApp は、自分のタスク一覧(@tasks)という状態を持ち、その状態に対して「表示する」「追加する」「保存する」「読み込む」といった振る舞いをまとめて持っています。

これは、オブジェクト指向のとても大事な感覚です。
クラスは「データの入れ物」ではなく、「データとそれに関する操作をセットにしたもの」です。
Person も Task もそうでしたが、今日はそれを「アプリ全体」にまで広げた形になります。


13日目のまとめ

今日のポイントをぎゅっとまとめると、こうなります。
メインループやメニュー処理、保存・読み込みを TaskApp クラスにまとめることで、「アプリ全体の入口と中心」がはっきりした。
@tasks のようなインスタンス変数を使って、「アプリが持つ状態」と「その状態をどう扱うか」を同じクラスの中に閉じ込めた。
save_tasks や load_tasks も App の責任にすることで、「自分の状態を自分で保存・復元できるオブジェクト」という形になった。

ここまで来ているあなたは、もう「Rubyでコードを書ける人」から、「Rubyでアプリを設計できる人」に足を踏み入れています。
14日目では、この流れを振り返りながら、「名簿アプリとタスクアプリに共通するパターン」を言葉にしていく方向にも進めます。

タイトルとURLをコピーしました