-
Notifications
You must be signed in to change notification settings - Fork 37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
【青木】電気料金シミュレーションAPIの開発 #46
base: master
Are you sure you want to change the base?
Conversation
@charges['basic_charges'][@amps] || nil | ||
end | ||
|
||
def usage_charge |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
このロジックで、例えば東京電力の従量電灯B・Cで使用電力量が180kWhの時に正しく計算できますか?
https://www.tepco.co.jp/ep/private/plan2/chargelist04.html#sec03
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ご指摘ありがとうございます。
計算のロジックに対する私の解釈が誤っておりましたので、修正いたしました。
また、下記のURLにおける単価と、課題のスクリーンショットで記載されている単価が異なっておりましたので、今回はスクリーンショットのほうの単価で統一いたしました。
https://www.tepco.co.jp/ep/private/plan2/chargelist04.html#sec03
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
controller, service, model とレイヤーを分けて実装していますが、どういった意図でレイヤー分けをしたのか教えていただきたいです。
@amps = params.require(:amps) | ||
@watts = params.require(:watts) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
validation の中でインスタンス変数を定義するのに違和感がありました。
例えばエンドポイントが増えてバリデーションをしない or 別のバリデーションをする場合、ここに書いてあると困ってしまいそうです。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
改めて確認し、私も変更すべきだと感じましたので
set_params
で定義するかたちに変えました。
rescue ActionController::ParameterMissing => e | ||
render json: { error: "'#{e.param}'が正しくありません" }, status: :bad_request | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
せっかく validation service を作っているのに渡されているかどうかのチェックだけここでやってしまうと、バリデーションロジックが複数ファイルに点在してしまって良くない気がしました。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
確かに良くないですね。
こちらもサービスに分けておくべきでした。
修正いたします。
private | ||
|
||
def integer?(param) | ||
/\A[-+]?\d+\z/ === param |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
これだとマイナスの値も許容してしまいますがパラメーターの内容的にマイナスになることはあり得るでしょうか?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
いえ、マイナスになる状況はあり得ませんので、
今回の場合、符号を許容する必要はありませんでした。
修正いたします。
it 'should calculate charges and change conditions correctly' do | ||
@bill = ElectricityCharges.new(50, 50, @charges_file) | ||
expect(@bill.calculate).to eq 1500 | ||
expect(@bill.calculate).not_to eq 3500 | ||
|
||
@bill = ElectricityCharges.new(60, 50, @charges_file) | ||
expect(@bill.calculate).to eq 2500 | ||
expect(@bill.calculate).not_to eq 4500 | ||
|
||
@bill = ElectricityCharges.new(50, 500, @charges_file) | ||
expect(@bill.calculate).to eq 26000 | ||
expect(@bill.calculate).not_to eq 6000 | ||
|
||
@bill = ElectricityCharges.new(60, 500, @charges_file) | ||
expect(@bill.calculate).to eq 27000 | ||
expect(@bill.calculate).not_to eq 7000 | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it の中に複数のチェックを書いてしまうと一つ失敗したらその先のチェックが実行されずに終わってしまうのでロジックが変わった時にテストを修正するのが大変になってしまいそうです。
また、to eq をチェックした上で not_to eq をする意味がないように感じました。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
確かにそうですね。
全体的にテストの書き方を見直し、分割したいと思います。
また、念のためnot_to eq
のチェックもしていましたが、
冗長であり、意味を成さないと感じたため併せて修正いたします。
ご質問ありがとうございます。
しかし、この考えは誤りであると感じました。 |
end | ||
|
||
def validate_params | ||
validation_service = ValidateParamsService.new(params) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
railsにはvalidatorというクラスが用意されているのでそれを使うのがrails wayかもしれません。
(直しが広範囲に渡りそうなので、できたらで大丈夫です!)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
この記事が参考になりそうです。
https://zenn.dev/virginia_blog/articles/1b08dc567b4059
validatorは使っていませんが、下記を記述したクラスを作ることでralisのvalidateメソッドが使えるようになります。こうすることで、rails likeにかけます。
include ActiveModel::Model
include ActiveModel::Attributes
rails likeに書くと、railsに慣れている人が読みやすくなるのでおすすめです。
end | ||
|
||
def calculate_charges | ||
electricity_files = Dir.glob(ELECTRICITY_CHARGES_PATH) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
リクエストがあるたびにファイル読み込んでますかね?
リクエストごとに変わらない値なのであれば、一度読んで終わりにしたいですね。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
確かに現状ですとリクエストのたびに読み込んでしまっていますね。
クラス変数を使い、1度だけ読み込むように修正してみました。
prev_limit = index.zero? ? 0 : @charges['usage_charges'][conditions[index - 1]]['limit'] | ||
curr_limit = @charges['usage_charges'][condition]['limit'] || @watts | ||
rate = @charges['usage_charges'][condition]['rate'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
以下2つは繰り返し登場するので変数化した方が脳に優しいです。
@charges['usage_charges']
@charges['usage_charges'][condition]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
こちらも、まとめられますね。
変数を追加しました。
INVALID_NUMBER_ERROR = '値は数字で入力してください。小数点や記号は使用できません。'.freeze | ||
INVALID_AMP_ERROR = "'正しいアンペア数(#{AMP_NUMBERS.join(', ')})を入力してください'".freeze |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
こういうエラーメッセージは、ja.ymlとかに書いた方がいいですね。
多言語化しやすいので
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
確かに、エラーメッセージは定数ファイルではなく言語化用のファイルに分けておくべきでした。
該当箇所をja.ymlおよびen.ymlに移動させ、呼び出せるようにしました。
また、この修正に伴い、テストも追加しました。
it 'should return information correctly' do | ||
allow_any_instance_of(ValidateParamsService).to receive(:validate_params).and_return([]) | ||
allow_any_instance_of(CalculateChargesService).to receive(:calculate_charges).and_return(1500) | ||
get :show_charges, params: { amps: 10, watts: 100 } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
このgetをsubjectにしてdescribeのすぐ下に持ってくると見やすくなると思いました! (好みかもしれません)
describe 'validate_params method' do
subject { get :show_charges, params: params }
context 'when parameters are valid' do
let(:params) { { amps: 10, watts: 100 } }
...
end
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
コードの読みやすさに関するご意見、とても参考になります。
修正いたしました。
ありがとうございます。
@@ -0,0 +1,44 @@ | |||
class GetChargesService |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GetChargesServiceを独立したクラスとした意図を教えてください。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ご質問ありがとうございます。
前提としまして、こちらは上記のコメントを受けてmodelからserviceに移動させたものになります。
また、移動させる際に下記のことを考慮して独立させることにしました。
・
calculate_charges_service
と統合してしまうと肥大化してしまう可能性があるため
・calculate_charges_service
とget_charges_service
で「料金の表示内容を取得する責務」と「料金を計算する責務」を分けたほうが良いと考えたため
ただ、改めてコードを確認し、ここまで細分化する必要はなかったのではないかとも感じています。
describe 'calculate_charges method' do | ||
context 'when parameters are correct' do | ||
it 'should return correct properties' do | ||
@service = CalculateChargesService.new(50, 500) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ここでインスタンス化する必要はない
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rate = @charges['usage_charges'][condition]['rate'] | ||
|
||
if @watts > prev_limit | ||
charge += rate * ([curr_limit, @watts].min - prev_limit) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
curr_limit = @charges['usage_charges'][condition]['limit'] || @watts
と[curr_limit, @watts].min
の処理が離れていて、何が行われているかわかりづらいです。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
確かに、変数の宣言はその変数を使用する直前で行われるべきであり、
そのほうが可読性も向上しますね。
修正いたします。
curr_limit = @charges['usage_charges'][condition]['limit'] || @watts | ||
rate = @charges['usage_charges'][condition]['rate'] | ||
|
||
if @watts > prev_limit |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
この判定が遅すぎると思います。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
prev_limitを宣言した時点で、if @watts > prev_limit
の判定を行えますね。
また、判定を早めることで条件のスコープ外の不要な変数を省くこともできそうです。
ご指摘、ありがとうございます。
|
||
RSpec.describe GetChargesService, type: :model do | ||
before do | ||
@charges_file = Rails.root.join('spec', 'test_data', 'charges.yml') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
あえてシンプルな値のデータをテスト用に準備するというアイデアはいいと思いました!
例えば10Aの基本料金は1000にするとか、100kWhまでは10円にし、200kWhまでは20円にするとかですね。いちいちtest_dataの定義を見にいかないでも覚えてられるので良いです!
ただシンプルにしすぎて、小数点とかまで削ってしまうのは端数処理のテストができなくなるのでどうかなと思いました。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
確かに端数処理の件がありますので、小数点を削るべきではありませんでした。
修正いたします。
RSpec.describe ValidateParamsService do | ||
describe 'validate_params method' do | ||
let(:service) { ValidateParamsService.new(params) } | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
以下のようにするとわかりやすくなりますし、記述が減りますよ
subject { service.validate_params }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let(:params) { ActionController::Parameters.new({ amps: '0', watts: '50' }) } | ||
|
||
it 'should handle unmatched_amp error correctly' do | ||
expect(service.validate_params).to include('unmatched_amp') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
このincludeは厳密なテストとは言えない気がします。
下記を見てもらうと、いろいろなテストに使えてしまうことがわかります。
https://www.rubydoc.info/gems/rspec-expectations/RSpec%2FMatchers:include#:~:text=Passes%20if%20actual%20includes%20expected,args%20are%20found%20in%20collection.
have_key というmatcherがあるようですよ。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
URLを拝見しました。教えてくださり、ありがとうございます。
ユニークなものをテストするにはincludeでないほうが良さそうですね。
該当箇所をhave_keyに変更しました。
ありがとうございます。 これ以降個人的な見解も含まれるし、修正する場合結構全体的な修正になってしまい他の人のレビューコメントも巻き込んでしまう可能性があり修正は不要の参考程度にですが、
|
return errors | ||
end | ||
|
||
unless integer?(amps) && integer?(watts) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
&&
条件だと、片方だけがintegerではない場合にエラー出力できなくなってしまいそうです。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
こちらはunlessの条件になりますので、ampsとwattsの両方がintegerとなるまではエラーになります。
よって、片方だけがintegerでない場合、両方ともintegerでない場合、どちらのケースでもエラーを出力することが可能です。
def validate_params | ||
errors = {} | ||
|
||
begin | ||
amps = @params.require(:amps) | ||
watts = @params.require(:watts) | ||
rescue ActionController::ParameterMissing => e | ||
errors['invalid_parameter'] = "'#{e.param}'が正しくありません" | ||
return errors | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
クラスを定義して呼び出すよりも、コントローラ内でストロングパラメータを使用する方が一般的だと思います。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
たぶん以前のコメントの対応で service に移していただいたのだと思いますが、service ないで strong parameter としての振る舞いをさせるのはあまり見かけないので、service としては単純に nil かどうかのチェックをするのがちょうどいいと思います。
active model の validater を使ったら以下のような感じになるかと思います。
validates :value, presence: true
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ysk91 @fucso
教えてくださり、ありがとうございます。
#46 (comment)
上記で頂いたご意見も参考に、service内ではnilチェックをするかたちに修正したいと思います。
@@ -0,0 +1,26 @@ | |||
require 'rails_helper' | |||
|
|||
RSpec.describe ApiController, type: :controller do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
テスト設計から見直した方が良いかもしれません。
1つめのテストと2つめのテストで、同じパラメータを与えているのに結果が変わるのはなぜですか?
modelに関する私の知識が不足しておりました。 |
開発内容
API設計
エラー表示の条件
テスト
今後のタスク