はじめに
オリジナルプロダクトLogi-Balance の Analytics 機能に
「最高負荷」「最低負荷」「平均値付近」の3コースをカード表示する機能を追加しました。
実装自体はそれほど難しくありませんでしたが、
設計の考え方・責務の分離・平均の意味の違い でかなり思考が整理されました。
今回はその学びをまとめます。
🎯 やりたかったこと
単日・週次・月次それぞれで
- 最高負荷コース
- 最低負荷コース
- 平均値付近コース
を表示させたい。
まず考えたこと
単日の場合、データはこうなっています。
{
"Aコース" => 370,
"Bコース" => 420,
"Cコース" => 310
}
このハッシュから
- 最大値
- 最小値
- 平均に最も近い値
を取り出せばいい。
Serviceオブジェクトで分離する
Controllerにロジックを書くと肥大化するため、
AnalyticsHighlightService を作成しました。
class AnalyticsHighlightService
def initialize(scores)
@scores = scores
end
def call
return { highest: nil, lowest: nil, closest: nil, average: 0 } if @scores.empty?
values = @scores.values.map(&:to_f)
avg = values.sum / values.size
max_name, max_score = @scores.max_by { |_, score| score }
min_name, min_score = @scores.min_by { |_, score| score }
closest_name, closest_score = @scores.min_by { |_, score| (score - avg).abs }
{
highest: {
name: max_name,
score: max_score,
diff: max_score - avg
},
lowest: {
name: min_name,
score: min_score,
diff: min_score - avg
},
closest: {
name: closest_name,
score: closest_score,
diff: closest_score - avg
},
average: avg
}
end
end
💡 重要な設計ポイント
このServiceは
「ハッシュを受け取って比較する機械」
です。
単日専用ではありません。
🔁 単日・週次・月次の違い
単日
@daily_scores = {
"Aコース" => 370,
"Bコース" => 420
}
そのままServiceへ。
週次
週次では series という構造になっているため、
weekly_totals = series.each_with_object({}) do |s, h|
name = s[:name]
total = s[:data].values.sum
h[name] = total
end
こうして
{
"Aコース" => 週間合計,
"Bコース" => 週間合計
}
というハッシュに変換してからServiceへ渡します。
月次も同じ
月次も合計を出してServiceへ渡すだけ。
🔥 大きな学び①
ロジックは同じでも「渡すデータ」が違う
Serviceの中身は単日と同じ。
違うのは、
- 単日 → 1日分の合計
- 週次 → 1週間分の合計
- 月次 → 1ヶ月分の合計
入力データの単位が違うだけ。
これが理解できたのは大きな収穫でした。
🔥 大きな学び②
平均には「意味の違い」がある
週次には実は2つの平均が存在します。
① グラフ用平均
@avg_value
1日 × コース数で割った平均。
② カード用平均
@highlights[:average]
1週間合計 ÷ コース数。
意味が違います。
最初ここで混乱しました。
🔥 大きな学び③
ServiceはControllerを知らない
Serviceは
- 単日か
- 週次か
- 月次か
を知りません。
ただ
{ コース名 => 数値 }
を受け取るだけ。
これは責務分離としておそらく重要な設計。
🎨 Viewでカード表示
<div class="highlight-cards">
<% if @highlights.present? %>
<div class="highlight-card highest">
<p class="highlight-label">最高負荷</p>
<p class="highlight-course"><%= @highlights[:highest][:name] %></p>
<p class="highlight-score"><%= @highlights[:highest][:score].round %></p>
</div>
<% end %>
</div>
Controllerで
@highlights = AnalyticsHighlightService.new(weekly_totals).call
していればそのまま表示されます。
💭 混乱したポイント
- 「単日と同じロジックじゃない?」
- 「週次専用の計算が必要?」
- 「平均の意味はどれ?」
→ 結論:ロジックは同じでいい。
🎯 今回の設計のまとめ
- Controllerはデータ整形
- Serviceは純粋な計算
- Viewは表示のみ
この分離ができたのが一番の成長でした。
Conclusion
今回の実装で一番の学びは
「処理を書くこと」より
「処理の意味を整理すること」の方が重要
ということ。
特に「平均」の意味が複数あることに気づけたのは大きな収穫でした。
今後は、
- 差分を%表示する
- 偏差が一定以上なら警告色にする
- ランキング形式にする
など発展させていきたいと思います。
※本記事は学習メモとして作成。