はじめに
Hubble Advent Calendar 2023の16日目1です!
Hubbleでバックエンドエンジニアをしている @power3812 です。オブジェクト指向大好きマンで、神クラスを作れないかと模索の日々です
前回はコードレビューの観点について投稿しました。
Hubbleのバックエンドチームはありがたいことに業務委託の方を含めて11人います。しかし、大人数になってくると同じ仕様でも各人によってロジックが変わったり変数名が違ったりという課題が出てきます。また、それらを毎回レビューするのは、レビューワーの負担が大きくなります。
そのため、どうにかコードの書き方をある程度統一化できないかと勉強会を開催しました!
開催方法
勉強会は、発表者が一方的に話すのではなく、事前に課題を用意し、それを聞き手側に解いてもらってそれの解説をする形式にしました。
他の条件については以下になります。
- 勉強会は月1で開催されます。
- 主に勉強会では最新技術的な話ではなく、オブジェクト指向プログラミングおける思考方法や、ロジックの組み方など基礎的な話になります。
- 課題の仕様等をChatGPT等にそのまま投げて解答するのは禁止です。
- 自分で解いてChatGPT等に添削してもらうのはOKです。
開催したった
課題
以下は実際に開催した際の課題の内容になります。(解いてみると面白いかもしれません)
# 以下の飲み物を販売している自動販売機から飲み物を購入する。
# エナジードリンク 300円
# コーラ 160円
# アクエリアス 150円
# BOSS 130円
# いろはす 110円
# 投入できるのは1000円札、500円硬貨、100円硬貨、50円硬貨、10円硬貨のみ
# 10000円札、5000円札、2000円札、5円硬貨、1円硬貨は使用不可
# 紙幣、硬貨の最大数はX枚とする(X > 0)
# 任意の所持金額N円(1000,500,100,50,10円(の組み合わせで成立する額))とする
# ランダムで飲料を購入する
# ただし、飲料の合計金額が所持金額N円を超えてはならない
# 各飲料の在庫数はY本とする(Y > 0)
# 所持金額N円を1回のみ自販機に投入して、ランダムに何か購入する。
# 何本でも何を購入することができる。
# まだ何か買えたとしても、どこで打ち切るかもランダム。
# 購入したら投入金額、各飲料の本数とその合計金額、全飲料の合計金額、おつりを標準出力に表示する。
# クラスは使用せずに、HashやArrayを使用してオブジェクトを表現する。
今回の課題の意図は仕様を正しく捉えて、日本語での動作を同じロジックをコードに落とし込むことと、物事を簡単に捉えることを狙っています。
日本語での動作を同じロジックをコードに落としこめると誰にでも読みやすいコードになります。
経験を積んでくると往々にして簡単なことを複雑に考えて、難しいコードになりがちになってしまいます。
また、今回はクラスの使用を禁止しています。これは今回は処理の流れを正しく作れるかに重きをおいているので、クラスを使用してしまうとクラス設計の話に飛んでしまうので禁止しています。
解答解説
# まずは前提条件に出てきたオブジェクトを考えます。
# 今回の場合、飲料とお金です。
# それらをマスタデータとして作成します。
drinks = [
{
name: "エナジードリンク",
price: 300,
stock: 20
},
{
name: "コーラ",
price: 160,
stock: 10
},
{
name: "アクエリアス",
price: 160,
stock: 5
},
{
name: "BOSS",
price: 130,
stock: 15
},
{
name: "いろはす",
price: 110,
stock: 12
}
]
cashes = [1000, 500, 100, 50, 10]
# 次にプレゼンテーションに必要な変数を考えます。
# 今回の場合は、投入金額、各飲料の本数、合計金額、全飲料の合計金額、おつりです。
# ここで、投入金額は逆残できないのでavailable_cashとして別変数として持ちます。
cash = 0
cashes.map { |c| cash += c * rand(0..5) }
available_cash = cash
purchased_drink_numbers = []
drinks.each_with_index do |_, index|
purchased_drink_numbers[index] = 0
end
# ここからは実際の処理に入っていきます。
# 仕様でランダムで打ち切るとなっているのでwhileとrandで表現します。
while rand(0..1) == 1
# まずは自動販売機では購入可能かで購入するかを決めるので購入可能な飲料のindexを格納します。
purchasable_drink_indexes = []
drinks.each_with_index do |drink, index|
# 飲料の値段が所持金以上で、在庫が1以上のものが購入可能なので、その飲料のindexを格納します。
purchasable_drink_indexes << index if (drink[:price] <= available_cash) && drink[:stock].positive?
end
# 購入可能な飲料が無い場合はそこで打ち切ります。
break if purchasable_drink_indexes.empty?
# 次に購入飲料選択です。
# まずは種類選択で、ランダムに購入となっているのでランダムに購入可能な飲料のindexを選びます。
purchased_drink_index = purchasable_drink_indexes.sample
purchased_drink = drinks[purchased_drink_index]
# 次に本数選択でここでもランダムとなっているので、最大購入本数を出し、1から最大購入本数をランダムに選びます。
purchased_drink_max_number = available_cash.div(purchased_drink[:price])
purchased_drink_max_number = purchased_drink[:stock] if purchased_drink_max_number > purchased_drink[:stock]
purchased_drink_number = rand(1..purchased_drink_max_number)
# 次に実際の購入動作です。
# 所持金から飲料の値段x購入本数分のお金を引いて、飲料の在庫を減らします。
available_cash -= purchased_drink[:price] * purchased_drink_number
drinks[purchased_drink_index][:stock] -= purchased_drink_number
# 次に取り出し動作です。
# 新規の種類の飲料は購入した本数にして、購入済みのものは新たに加算します。
if purchased_drink_numbers[purchased_drink_index].zero?
purchased_drink_numbers[purchased_drink_index] = purchased_drink_number
else
purchased_drink_numbers[purchased_drink_index] += purchased_drink_number
end
end
# 最後に実際に購入した全飲料の合計金額を出します。
purchased_drinks_total_price = 0
purchased_drink_numbers.each_with_index do |purchased_drink_number, index|
purchased_drinks_total_price += drinks[index][:price] * purchased_drink_number
end
# 最後にプレゼンテーションです。
# プレゼンテーションでは、簡単な四則演算なら変数化して渡すのではなくそのまま渡します。
# 変数化して渡すと、APIがFATになりすぎるためです。
purchased_drink_numbers.each_with_index do |purchased_drink_number, index|
puts "#{drinks[index][:name]}:#{purchased_drink_number}本 #{drinks[index][:price] * purchased_drink_number}円"
end
puts "投入金額: #{cash}円"
puts "合計金額: #{purchased_drinks_total_price}円"
puts "おつり: #{cash - purchased_drinks_total_price}円"
ここで特に重要なのは処理の順番です。コードを書くときに往々にして、処理の順番が前後しても最終的な結果は変わらないので無意識に順番を気にせず書いている場合があります。しかし、日本語で考えると順番がある場合があります。
例えば、今回の飲み物を買う動作の所持金から飲み物代を引いてから、飲み物の在庫を減らしています。
available_cash -= purchased_drink[:price] * purchased_drink_number
drinks[purchased_drink_index][:stock] -= purchased_drink_number
これは飲み物の在庫を減らしてから所持金から飲み物代を引いても結果は変わりません。
drinks[purchased_drink_index][:stock] -= purchased_drink_number
available_cash -= purchased_drink[:price] * purchased_drink_number
しかし、飲み物の在庫を減らしてから所持金から飲み物代を引くのは、実際の現実の動作に即していません。
なぜ、ここにこだわるかというと、今回の場合は2行の差ですが、これが実務のコードになってくると何十行となってきて、ロジックの可読性に関わってきます。
開催してみて
実際に開催してみて、解説後のQ&Aで事前課題にしたおかげか、思ったより質問があり議論できて良い会になったと思います。まだ開催2回しか開催していないので、今後どれくらいレビューに良い影響が出るのかが課題です。
終わりに
今回はバックエンドの社内勉強会の内容を紹介してみました!意外と勉強会は開催してみるまでがハードル高いですが、実際にやってみると楽しく月1のルーティンに組み込めそうです!
次回はアドカレ最終日で投稿2回目2の @katsuya0515 さんです!