こんにちは。
entershareで働いているじゅんと言います。
普段仕事ではRubyを使っています。
最近とあるプロジェクトで、「ある一定のルールに従って毎秒値を変化させていく」という機能を実装する機会がありました。
自分の実装した内容を詳しくは話すことはできませんが、例えばソシャゲで毎秒ごとにユーザーの体力が0.01だけ回復していくという仕様や、セールで売られている商品の価格が毎秒1円ずつ安くなっていくような仕様を思い浮かべてください。
(ここでは毎秒と言いましたが、毎秒でなくても毎分でも毎時でも同じです。)
これと似たような仕様で、「1日に一回ユーザーの体力を全回復する」といったようなものもあります。
この場合、毎日深夜の0時ぴったりにcronを回して全てのユーザーの体力を回復させる処理を行えばOKです。
(例えばusersテーブルにlifeとmax_lifeいうカラムがあるのであれば、0時に各ユーザーのlifeにmax_lifeと等しい値を入れてあげて更新すればOK)
ちなみにRailsでwheneverというgemを使って定期実行する方法は別で書きました。
しかし、1日1回とか、1時間に1回のようではなく、連続的に値が変更していくのであればどうしたら良いでしょうか?
例えば、Userがlifeとmax_lifeをもち、そのライフが毎秒1ずつだけ回復していくというようなパターンです。
この場合、毎秒cronを回してユーザーのライフを+1するという方法を思いつくかもしれませんが、これは現実的ではありません。
1ユーザーのデータだけで、1日に 60(秒) * 60(分) * 24(時間) = 86400回データが更新されることになります。
ユーザーが多くなったら、この処理だけでDBへの負荷がとんでも無いことになってしまいます。
ということは、別の方法を考える必要があります。
そもそも、Userがlife(現在のライフ)をカラムとして持っていることが間違いな気がしてきます。
カラムとして持っていると、それを常に変更していかないといけません。
毎秒変更できないのであれば、カラムとして現在のライフを保持しておくのをやめましょう。
その代わりに、前回ライフを消費した時の時間とその際の残りライフを記録することにしましょう。
ここでは仮に、それぞれのカラム名をlast_use_life_atとlast_left_lifeとします。
(この命名はかなり分かりにくいので、改善の余地あり)
そして、現在のライフを計算して返してくれるインスタンスメソッドを作りましょう。
メソッド名をcurrent_lifeとします。
1 2 3 4 5 6 7 8 9 10 |
class User < ApplicationRecord #max_life(最大の体力)というカラムももつ #last_use_life_at(前回ライフを消費した時の日時)というカラムをもつ(この命名は分かりにくいので改良すべき) #last_left_life(前回の残りライフ)というカラムをもつ(この命名も分かりにくいので改良すべき) #現在のライフを返す def current_life end end |
current_lifeというメソッドをここから実装していきます。
ここでは、毎秒1ずつライフが回復していき、max_life以上は回復しないとします。
また、max_lifeに関してはユーザーごとに異なるとします。
下のコードを見ていただければ分かると思いますが、
①まず現在時刻と前回の時刻の差(秒数)を計算する
②その秒数(何秒たったか)に、1秒あたりのライフの回復量をかける(今回の場合は1)
③max_lifeと比較して、max_lifeより大きい場合はmax_lifeを返す
というのがcurrent_lifeのロジックです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class User < ApplicationRecord #max_life(最大の体力)というカラムももつ #last_use_life_at(前回ライフを消費した時の日時)というカラムをもつ(この命名は分かりにくいので改良すべき) #last_left_life(前回の残りライフ)というカラムをもつ(この命名も分かりにくいので改良すべき) #現在のライフを返す def current_life #現在時刻と前回ライフを消費した時の日時の差を計算する(前回の消費から何秒経ったか) diff_sec = (Time.current - self.last_use_life_at).to_i #回復するライフ量を計算する #毎秒の回復量「1」に関しては定数としといた方が本来は良さそう。 recoverd_life = 1 * diff_sec #max_lifeと比較 if self.last_left_life + recoverd_life >= self.max_life current_life = self.max_life else current_life = self.last_left_life + recoverd_life end return current_life #この行はなくてもいいけど、明示的にreturnした方が分かりやすいから書いてみた end end |
これで毎秒変わっていく値の実装が完了です!
毎秒変わっていくというのはかなり極端な例でしたが、5分に1回、10分に1回というのはよくある例だと思います。
そのような場合には、今回のような実装が使えると思います!
ついつい、現在の値を常にdbに保存しておかないといけないと思ってしまいますが、そうではなく前回更新した日時とその時の値があれば、現在の値を計算することは可能です。
(そのような計算式があらかじめ決まっている場合のみ。)
この考え方を使えば、実装の幅が色々広がっていくと思うので、機会があれば使ってみてください!