はじめに
こんにちは!アメリカの大学で語学を学びながら、独学でソフトウェアエンジニアを目指している者です。
今回は、図書館管理システムを作成する中で発生したミスと、その修正について振り返ります。問題の解決を通して、Rubyのクラス設計や状態管理について学びましょう!
今回扱った問題
図書館管理システム (レベル6)
図書館にある書籍を管理するシステムを作成する。
要件
- 書籍の情報(タイトル、著者、ISBN、貸出状態)を管理する。
- 書籍を貸し出す、返却する機能を提供する。
- タイトルまたは著者名で書籍を検索する機能を追加する。
- 同じ本を2人以上が同時に借りられないようにする。
間違えたコード
間違えた原因
- attr_reader を使っていた
書籍の貸出状態を変更する処理でbook.borrowed = true
と書き込もうとしたが、attr_reader
のためエラーが発生した。 - ISBNで書籍を探す処理が欠けていた
borrowed_book
メソッド内で、ISBNに一致する書籍を探す処理がなく、ループ内で毎回処理を行っていた。
間違えたコードの例
# Bookクラスに本の情報を記録する
class Book
attr_reader :title, :author, :isbn, :borrowed
def initialize(title,author,isbn)
@title = title
@author = author
@isbn = isbn
@borrowed = false
end
# 本の情報を出力するメソッド
def book_status
puts "本のタイトル#{@title},著者#{@author},ISBN#{@isbn},貸出状態#{@borrowed}"
end
end
class Library
# 配列booksに本を格納するためのメソッド
def initialize
@books =[]
end
# 書籍を追加するメソッド
def add_books(book)
@books << book
puts "追加されました:#{book.title}"
end
# 書籍を貸出するメソッド
def borrowed_book(borrow_isbn)
# 貸出状況をみて書籍を貸すことができるかの処理をしている
@books.each do |book|
if book.borrowed == false && book.isbn == borrow_isbn
puts "ご利用ありがとうございます。"
book.borrowed = true
else
puts "現在ほかの方がご利用されているのでお貸出できません"
end
# 図書館にその本がない場合の例外処理
puts "お探しになっている書籍はこちらの図書館にございません。" unless borrow_isbn == book.isbn
end
end
# 書籍の返却の処理
def return_book(return_isbn)
@books.each do |book|
if book.borrowed == true && book.isbn == return_isbn
puts "ご返却ありがとうございます。またのご利用をお待ちしております"
book.borrowed = false
elsif book.borrowed == false && book.isbn == return_isbn
puts "図書館に同じISBNの書籍がありますが、貸出されている記録がありません。もう一度ISBNをご確認ください"
else
puts "こちらの書籍と一致するISBNが図書館に存在しません。"
end
end
end
# 検索機能実装前にミス発覚
end
正しいコード
修正ポイント
-
attr_reader → attr_accessor に変更
書籍の貸出状態を変更できるようにした。 -
ISBNで書籍を検索する処理を追加
先にISBNに一致する書籍を探してから、処理を行うように修正した。
修正後のコード
# Bookクラス: 書籍の情報を管理するクラス
class Book
attr_accessor :title, :author, :isbn, :borrowed
def initialize(title, author, isbn)
@title = title
@author = author
@isbn = isbn
@borrowed = false # 初期状態では貸し出されていない
end
def to_s
"Title: #{@title}, Author: #{@author}, ISBN: #{@isbn}, Borrowed: #{@borrowed ? 'Yes' : 'No'}"
end
end
# Libraryクラス: 図書館を管理するクラス
class Library
def initialize
@books = [] # Bookインスタンスを格納する配列
end
# 書籍の追加
def add_book(book)
@books << book
puts "追加されました: #{book.title}"
end
# 書籍の貸し出し
def borrow_book(isbn)
book = @books.find { |b| b.isbn == isbn }
if book
if book.borrowed
puts "この本は既に貸し出されています: #{book.title}"
else
book.borrowed = true
puts "貸し出しました: #{book.title}"
end
else
puts "本が見つかりません。"
end
end
# 書籍の返却
def return_book(isbn)
book = @books.find { |b| b.isbn == isbn }
if book
if book.borrowed
book.borrowed = false
puts "返却しました: #{book.title}"
else
puts "この本は貸し出されていません: #{book.title}"
end
else
puts "本が見つかりません。"
end
end
# タイトルで書籍を検索
def search_by_title(title)
results = @books.select { |b| b.title.include?(title) }
if results.any?
results.each { |b| puts b }
else
puts "該当する書籍が見つかりません。"
end
end
# 著者で書籍を検索
def search_by_author(author)
results = @books.select { |b| b.author.include?(author) }
if results.any?
results.each { |b| puts b }
else
puts "該当する書籍が見つかりません。"
end
end
end
動作の確認
library = Library.new
# 書籍の追加
library.add_book(Book.new("The Great Gatsby", "F. Scott Fitzgerald", "9780743273565"))
library.add_book(Book.new("1984", "George Orwell", "9780451524935"))
library.add_book(Book.new("To Kill a Mockingbird", "Harper Lee", "9780060935467"))
library.add_book(Book.new("Brave New World", "Aldous Huxley", "9780060850524"))
# 書籍の一覧表示
puts "\n--- 図書館の書籍一覧 ---"
library.list_books
# 書籍の貸し出し
puts "\n--- 書籍の貸し出し ---"
library.borrow_book("9780451524935")
library.borrow_book("9780451524935") # 2回目の貸し出しはエラー
# 書籍の返却
puts "\n--- 書籍の返却 ---"
library.return_book("9780451524935")
library.return_book("9780451524935") # 2回目の返却はエラー
# タイトルで検索
puts "\n--- タイトルで検索 ('Brave') ---"
library.search_by_title("Brave")
# 著者で検索
puts "\n--- 著者で検索 ('George Orwell') ---"
library.search_by_author("George Orwell")
コードの解説と自分がわからなかったところの解説
attr_accessor
と initialize
メソッドについて
attr_accessor
の役割:
attr_accessor :title, :author, :isbn, :borrowed
attr_accessor
は、title
、author
、isbn
、borrowed
という4つのインスタンス変数に対して、読み書き可能なゲッターメソッドとセッターメソッドを自動生成します。
例えば、book.title
でタイトルを取得し、book.title = "新しいタイトル"
でタイトルを設定することができます。
initialize メソッドの役割:
def initialize(title, author, isbn)
@title = title
@author = author
@isbn = isbn
@borrowed = false # 初期状態では貸し出されていない
end
initialize
メソッドは、Book
クラスのインスタンスが生成されたときに呼ばれる特別なメソッドです。
@title
、@author
、@isbn
、@borrowed
に初期値を設定します。
@borrowed
を false
にすることで、新しく作られた本は「貸し出されていない」状態になります。
なぜ両方定義するのか?
attr_accessor
は メソッド(ゲッターとセッター) を生成しますが、インスタンス変数自体の初期値を設定するわけではありません。
初期値を設定するためには initialize メソッド内で明示的に代入する必要があります。
Array#find
メソッドについて
find メソッドは、配列の中からブロックの条件に合う最初の要素を返します。
book = @books.find { |b| b.isbn == "9780451524935" }
このコードは、@books
配列の中で isbn
が "9780451524935"
に一致する最初の Book
インスタンスを返します。
条件に合う要素が見つからない場合は nil
を返します。
(追記)
実際に値を変更しているのは@borrowed
のみなので意図しないエラーをおこさないためにも以下のように書くべきです
attr_accessor :borrowed #読み取りと書き取りができる
attr_reader :title, :author, :isbn #読みとりのみ
Array#select
メソッドについて
select
メソッドは、配列の中からブロックの条件に合うすべての要素を配列として返します。
results = @books.select { |b| b.title.include?("Great") }
このコードは、@books
配列の中でタイトルに "Great"
を含むすべての Book
インスタンスを新しい配列として返します。
条件に合う要素がない場合は空配列 [] を返します。
results.any?
メソッドについて
any?
メソッドは、配列やコレクションに1つ以上の要素が存在する場合に true
を返します。
逆に、すべての要素が偽である場合に false
を返します
results = @books.select { |b| b.title.include?("Great") }
if results.any?
results.each { |b| puts b }
else
puts "該当する書籍が見つかりません。"
end
results.any?
が true
の場合、検索結果が1つ以上あるため、results
の内容を出力します。
results
が空の場合、else
ブロックの "該当する書籍が見つかりません。"
が実行されます。
(追記) any?
とempty?
について
今回のコードで特段問題になるわけではないのですが、any?は配列の中に真の要素があればtrueを返すのが目的です。
今回は配列results
の中に要素が入っているか否かが知りたいので、empty?を使用するべきです
results = @books.select { |b| b.title.include?("Great") }
if !results.empty?
results.each { |b| puts b }
else
puts "該当する書籍が見つかりません。"
end
まとめ
- アクセサメソッドの選択
attr_reader は読み取り専用、書き込みが必要なら attr_accessor を使う。 - 処理の順序
処理は分割し、最初に行った方がいいものを考えてからコードを書く -
attr_accessor
はインスタンス変数を読み書き可能なメソッドとして自動生成する -
initialize
はインスタンス変数の初期値を代入するためのメソッド -
find
は配列の中からブロックの条件にあう最初の要素を返し、select
は配列の中からブロックの条件に合うすべての要素を配列として返します。