Skip to content

Conversation

@tl-yoshirofujimaki
Copy link

@tl-yoshirofujimaki tl-yoshirofujimaki commented Mar 14, 2025

概要

お世話になっております。藤巻です。

チャレンジ(コーディング試験)の課題として、
「serverside_challenge_2」の電気料金シミュレーション機能を実装いたしました。
課題の詳細: https://github.com/enechange/coding-challenge/tree/master/serverside_challenge_2

お手数ですが、以下の点についてご確認をお願いいたします。

  • 実装内容の要件を満たしているか
  • コードの構成や記述に問題がないか

設計の詳細については、README にまとめています。

よろしくお願いいたします。

実装内容

バックエンド

  • APIエンドポイント /electricity_prices を実装し、契約アンペア数と使用量に応じた電気料金とそのプランをIDの昇順で返却

フロントエンド

  • 契約アンペア数と使用量を入力するフォームを実装
  • APIから取得したデータをテーブル形式で表示
  • 最安プランが一目わかるよう「✅」を付与
    • APIレスポンスにはございませんが、実際ユーザーが使う時を考えたら便利かと思い実装いたしました
スクリーンショット 2025-03-14 11 21 49

本番環境確認用URL

http://enecha-publi-yakvtpzir2fc-1409577172.us-east-2.elb.amazonaws.com/frontend/

デプロイ手順: README.md#本番デプロイ手順(aws)

※ チャレンジとしての利用のため、ELBのデフォルトドメインを直で利用しています

インフラの構成としてはこのようになります
インフラ構成図

ローカル環境での確認

以下手順を参考に環境構築の上、動作確認をお願いします。
環境構築手順: README.md#ローカル環境構築手順

@tl-yoshirofujimaki tl-yoshirofujimaki changed the title WIP:【藤巻】serverside_challenge_2: 電気料金シミュレーション機能の実装 【藤巻】serverside_challenge_2: 電気料金シミュレーション機能の実装 Mar 14, 2025
@tl-yoshirofujimaki tl-yoshirofujimaki marked this pull request as ready for review March 14, 2025 03:20
Comment on lines 10 to 17
price = plan.electricity_price(ampere, usage)
next unless price

{
provider_name: name,
plan_name: plan.name,
price: plan.electricity_price(ampere, usage)
}
Copy link
Author

@tl-yoshirofujimaki tl-yoshirofujimaki Mar 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

提出させていただいた後で申し訳ないのですが、こちら不要に2度価格の計算処理をしているようです
申し訳ありませんが、こちら修正させてください。

Copy link
Author

@tl-yoshirofujimaki tl-yoshirofujimaki Mar 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

また、合わせてテストコードを追記させていただきます

Copy link
Author

@tl-yoshirofujimaki tl-yoshirofujimaki Mar 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

修正と本番環境へのデプロイを実施いたしました。

Comment on lines +1 to +12
# frozen_string_literal: true

class ElectricityPriceCalculateService
def initialize(ampere, usage)
@ampere = ampere
@usage = usage
end

def calc
Provider.order(:id).map { |provider| provider.electricity_prices(@ampere, @usage) }.flatten
end
end
Copy link
Author

@tl-yoshirofujimaki tl-yoshirofujimaki Mar 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[COMMENT]
計算ロジックの大半が モデル側にあるため、Controllerに直接記載するか悩みましたが、今後のロジック修正や拡張を考慮し、サービスとして分離しています。

ロジックはモデルのメソッドを呼び出しているだけのため、
モデル側のテストで十分と判断し、このサービスクラスのテストは記載しておりません。

@tl-yoshirofujimaki
Copy link
Author

簡単にですが、インフラの構成図をPR説明文に追記いたしました

# Defines the root path route ("/")
# root "articles#index"

get '/health', to: proc { [200, {}, ['OK']] }
Copy link
Author

@tl-yoshirofujimaki tl-yoshirofujimaki Mar 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[COMMENT]
ELBからのヘルスチェック用エンドポイントとして作成しています

RUN yarn install
RUN yarn build

CMD ["yarn", "preview", "--host", "0.0.0.0", "--port", "5173"]
Copy link
Author

@tl-yoshirofujimaki tl-yoshirofujimaki Mar 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[COMMENT]
今回ECSで配信するためにローカルで静的ファイル配信サーバーを立ち上げるpreviewモードを利用していますが、実運用だとbuild済みのファイルを別の方法で配信する方が本当は良いです

公式的にも非推奨
https://ja.vite.dev/guide/static-deploy

例えば、cloudfront+s3で配信するようなやり方の方とかが良さそうかな
今回はサーバーサイドの構築ががメインのため、一旦確認できる状態となることを優先して、このようなやり方をとっております

Comment on lines +65 to +68
<input
type='text'
:value='usage'
@input='event => usage = event.target.value'>
Copy link
Author

@tl-yoshirofujimaki tl-yoshirofujimaki Mar 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[COMMENT]
ここは 0 以上の整数が前提なので、type='number' の方が適切だったかもしれません。
レビュー提出後の修正は混乱を招く可能性があるため、今回は一旦そのままとさせていただきます。
細かい見落としがあり、申し訳ありません。

end

def calc
Provider.order(:id).map { |provider| provider.electricity_prices(@ampere, @usage) }.flatten

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

チャレンジ提出いただきありがとうございます!
Providerに電気料金計算のロジックを持たせたのはどういった意図になりますでしょうか?

Copy link
Author

@tl-yoshirofujimaki tl-yoshirofujimaki Mar 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@daichi1991
ご指摘ありがとうございます
Provider モデルに electricity_prices メソッドを持たせたのは、「プランの料金計算結果と provider_name をセットで返す」という要件に対応するためです
加えて、将来的に「特定の Provider のみのプラン情報を出力したい」という要望が出た場合の対応のしやすさも考慮しています

例えば、サービス側等 で次のように記述すれば、特定の Provider のみを対象とする処理が容易になるかと考えました

Provider.find(provider_id).electricity_prices(@ampere, @usage)

主要な計算ロジックは Plan に集約しているため、Plan モデルを直接利用するのも一つの選択肢かと思います。
その場合、サービス側では次のような処理になるかと考えています。

Plan.joins(:provider).order(:provider_id).map do |plan|
  price = plan.electricity_price(@ampere, @usage)
  next if price.nil?

  {
    provider_name: plan.provider.name,
    plan_name: plan.name,
    price: price
  }
end.compact

もし既存のロジックに問題がある場合や、Plan からの直接呼び出しの方が望ましい場合は、修正いたします。

Copy link
Collaborator

@RyuyaIshibashi RyuyaIshibashi Mar 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

コメントありがとうございます!

もし既存のロジックに問題がある場合や、Plan からの直接呼び出しの方が望ましい場合は、修正いたします。

いえあくまで考え方をお伺いしたかっただけですので、修正は不要かと思います。
Plan からの直接呼び出しの方が良いことが明らかになった場合にリファクタすれば良いと思います。

Comment on lines +27 to +50
def calc_usage_price(usage)
usage_rates = electricity_charges_usage_rates.order(:min_usage)

usage_price = 0
remaining_usage = usage

usage_rates.each do |rate|
# 使用量の上限がない場合、すべてその単価で計算
if rate.max_usage.nil?
usage_price += remaining_usage * rate.unit_rate
break
end

# 範囲内の使用量のみ計算
range_usage = [remaining_usage, rate.max_usage - rate.min_usage].min
usage_price += range_usage * rate.unit_rate
remaining_usage -= range_usage

# 使用量が計算し終わったら終了
break if remaining_usage <= 0
end

usage_price
end

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

可読性が高く、何をやっているのか分かり易いです、ありがとうございます。

Comment on lines +10 to +26
unless ALLOWED_AMPERES.include?(ampere)
render json: { error: "アンペア数(ampere)は #{ALLOWED_AMPERES.join('/')} のいずれかを指定してください" }, status: :bad_request
return
end

if usage.negative?
render json: { error: '使用量(usage)は 0 以上の整数を指定してください' }, status: :bad_request
return
end

plans = ElectricityPriceCalculateService.new(ampere, usage).calc
render json: plans
rescue TypeError
render json: { error: 'アンペア数(ampere)と使用量(usage)の両方が指定されていません' }, status: :bad_request
rescue ArgumentError
render json: { error: 'アンペア数(ampere)と使用量(usage)を整数で指定してください' }, status: :bad_request
end

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

適切なバリデーションになっていると思います!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tl-yoshirofujimaki
バリデーションを外部のクラスに切り出すとしたらどのような設計が良いでしょうか?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RyuyaIshibashi
コメントありがとうございます。

まず、今回コントローラー側にバリデーションを加えた意図についてですが
サービスクラスに渡す前の「入力データの検証」にあたるため、リクエストのパラメータチェックはコントローラーの責務として記載するのが自然と考えました

一方で、コントローラー側の責務が肥大化していくのを避けるという観点では、バリデーションを サービスクラス に切り出すのが良いかと考えています
今回のバリデーションはシンプルなため、コントローラー側で問題ないと判断しましたが、ビジネスロジックが絡むようなバリデーションについては、サービスクラスに切り出すべきだと考えています

またアンペア数や使用量の「0以上の数値であること」のような条件は、今後別の機能でも共通して利用する可能性があるため、そうしたバリデーションが汎用的に使われるのであれば、concerns への切り出しが DRYの観点では適しているかと思います


return unless unbounded_count > 1

errors.add(:base, '同一プラン内で使用量上限がない項目が2つ以上存在します')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

適切なバリデーションと思います!

<label>
<div>
1ヶ月の使用量(kWh)
<span v-tooltip="'0以上の整数を入力してください'" class='info'>ℹ️</span>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このキャプションはユーザーに親切で良いと思いました!

Comment on lines +86 to +91
<tr v-for="(plan, index) in electricityPrices" :key="index">
<td>{{ plan.provider_name }}</td>
<td>{{ plan.plan_name }}</td>
<td>{{ plan.price }} 円</td>
<td>{{ plan.isCheapest ? '✅' : '' }}</td>
</tr>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

最安に✅を示すのはとても良いと思いました!

usage_price = calc_usage_price(usage)

# 電気料金(基本料金 + 従量料金)を返却
(basic_price + usage_price).floor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここで小数点以下を切り捨てた意図は何かありますでしょうか?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

こちらについては以前弊社と調整を図った上での実装だったのですね!
失礼いたしました🙇

create_table 'electricity_charges_basic_rates', comment: 'プラン毎の電気基本料金を格納する', force: :cascade do |t|
t.bigint 'plan_id', null: false
t.integer 'ampere', null: false, comment: '契約アンペア数(A)'
t.decimal 'basic_rate', precision: 10, scale: 2, null: false, comment: '基本料金(円)'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

データ型をdecimal(10,2)にした背景があればお伺いしたいです。

Copy link
Author

@tl-yoshirofujimaki tl-yoshirofujimaki Mar 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@daichi1991
小数を含む金額を正確に扱うことが求められるため、浮動小数点型 (float など) ではなく、固定小数点型で誤差のない decimal 型を選択しました

scale: 2 については現行必要とされる小数部2桁に対応できることが理由として設定しています
precision: 10に関しては明確な根拠を持って定めた数値ではないのですが、最大 99,999,999.99 円まで対応できるため、今後料金が増加したとしても余裕がある設定となっているのではと考えているところです

t.bigint 'plan_id', null: false
t.integer 'min_usage', null: false, comment: '電気使用量(kWh)の下限値(境界値を含まない)'
t.integer 'max_usage', comment: '電気使用量(kWh)の上限値(境界値を含む)'
t.decimal 'unit_rate', precision: 10, scale: 2, null: false, comment: '従量料金単価(円/kWh)'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

データ型をdecimal(10,2)にした背景があればお伺いしたいです。

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@daichi1991
こちらも electricity_charges_basic_rates のschemaでコメントさせていただいている箇所と同じ理由になります
https://github.com/enechange/coding-challenge/pull/70/files#r2001371168

Comment on lines +81 to +83
**`copilot/` ディレクトリが必要ですが、機密情報が含まれるためコミット対象に含めておりません**
**そのため、本手順は参考としてご確認していただけたら幸いです。**
**どうするのが正解かは分かりませんが、本当の運用であれば機密情報の管理はAWS Systems Manager 等で管理すべきかと思います。**
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

インフラのデプロイまでいただき、ありがとうございます!
AWS Copilotを採用した理由がありましたら、簡単に教えていただけますでしょうか?

Copy link
Author

@tl-yoshirofujimaki tl-yoshirofujimaki Mar 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RyuyaIshibashi
AWS Copilotを利用するに至った経緯ですが
まず、開発環境をDockerで構築していたため、本番環境も同様にコンテナで運用するのが望ましいと考え、デプロイ先としてECS + fargateを選択しました

次に、ECSへのデプロイ経験がなかったため、デプロイ方法の調査を進める中でAWS Copilotに行き着きました
Copilotは、ELBやVPC、ECRといった周辺インフラの構築からデプロイまで一貫して行えるため、素早く環境を構築したい状況に適していると考え、採用しました

@RyuyaIshibashi
Copy link
Collaborator

@tl-yoshirofujimaki
お忙しい中、多くの時間を割いて取り組んでいただきありがとうございます。
いくつかコメントさせていただきましたので、可能な範囲でご確認ください。
(十分時間をかけて取り組んでいただおりますので、返信は必ずしも不要です。)

バックエンドのみならずフロントエンドも実装いただき、また各種ドキュメンテーションや注釈も大変わかりやすく、私自身大変勉強になりました。

面談当日もまたよろしくお願いいたします。お会いできることを楽しみにしております!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants