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
Rubyread_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
Rubyhandle_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日目では、この流れを振り返りながら、「名簿アプリとタスクアプリに共通するパターン」を言葉にしていく方向にも進めます。
