I am trying to understand what is the best approach for this calculator service. It will be called when there is a request coming in via API and return some data arranged in a table.
Below you will find the instance methods approach and the class methods approach.
From what I found online, it seems that using instance classes is wrong because this is not related to a specific instance, therefore this should be handled with class methods.
The instance methods way:
class DamageCalculatorService include TimeFilterable TOTAL_DAMAGE_DEALT_COLUMN = 'total_damage_dealt'.freeze TOTAL_DAMAGE_TAKEN_COLUMN = 'total_damage_taken'.freeze def initialize(time_filter:, timezone:, limit:, formatted_table:) @time_filter = time_filter @timezone = timezone @limit = limit @formatted_table = formatted_table end def damage_dealt @sort_by = TOTAL_DAMAGE_DEALT_COLUMN calculate_damage end def damage_taken @sort_by = TOTAL_DAMAGE_TAKEN_COLUMN calculate_damage end private def calculate_damage start_time = TimeFilterable.start_time_for(time_filter: @time_filter, timezone: @timezone) return [] unless start_time.present? begin query = Stat.where('stats.created_at >= ?', start_time) results = query.joins(:player) .select('players.name as player_name','players.steam_id as steam_id','SUM(stats.damage_dealt) as total_damage_dealt','SUM(stats.damage_taken) as total_damage_taken' ) .group('players.id', 'players.name', 'players.steam_id') .order("#{@sort_by} DESC") .limit(@limit) final_results = results.map do |result| { steam_id: result.steam_id, player_name: result.player_name, total_damage_dealt: result.total_damage_dealt.to_i, total_damage_taken: result.total_damage_taken.to_i } end return @formatted_table ? to_table(data: final_results) : results rescue ActiveRecord::StatementInvalid => e Rails.logger.error("Error in DamageCalculatorService: #{e.message}") [] end end def to_table(data:) title = "#{@sort_by.titleize.split('').join('')} for the #{@time_filter}" headers = %w[player_name total_damage_dealt total_damage_taken] TabletizeService.new(title: title, data: data, headers: headers).table endend
The class methods way:
class DamageCalculatorService include TimeFilterable TOTAL_DAMAGE_DEALT_COLUMN = 'total_damage_dealt'.freeze TOTAL_DAMAGE_TAKEN_COLUMN = 'total_damage_taken'.freeze def self.damage_dealt(time_filter:, timezone:, limit:, formatted_table:, sort_by: TOTAL_DAMAGE_DEALT_COLUMN) calculate_damage(time_filter: time_filter, timezone: timezone, limit: limit, formatted_table: formatted_table, sort_by: sort_by) end def self.damage_taken(time_filter:, timezone:, limit:, formatted_table:, sort_by: TOTAL_DAMAGE_TAKEN_COLUMN) calculate_damage(time_filter: time_filter, timezone: timezone, limit: limit, formatted_table: formatted_table, sort_by: sort_by) end private def self.calculate_damage(time_filter:, timezone:, limit:, formatted_table:, sort_by:) start_time = TimeFilterable.start_time_for(time_filter: time_filter, timezone: timezone) return [] unless start_time.present? begin query = Stat.where('stats.created_at >= ?', start_time) results = query.joins(:player) .select('players.name as player_name','players.steam_id as steam_id','SUM(stats.damage_dealt) as total_damage_dealt','SUM(stats.damage_taken) as total_damage_taken' ) .group('players.id', 'players.name', 'players.steam_id') .order("#{sort_by} DESC") .limit(limit) final_results = results.map do |result| { steam_id: result.steam_id, player_name: result.player_name, total_damage_dealt: result.total_damage_dealt.to_i, total_damage_taken: result.total_damage_taken.to_i } end return formatted_table ? formatted_table(data: final_results, time_filter: time_filter, sort_by: sort_by) : results rescue ActiveRecord::StatementInvalid => e Rails.logger.error("Error in DamageCalculatorService: #{e.message}") [] end end def self.formatted_table(data:, time_filter:, sort_by:) title = "#{sort_by.titleize.split('').join('')} for the #{time_filter}" headers = %w[player_name total_damage_dealt total_damage_taken] TabletizeService.new(title: title, data: data, headers: headers).table endend
I have no issues using class methods, however it seems quite verbose to have all the parameters passed over and over, while in the instance it seems cleaner.
Using class instance variables is not good though since I can have multiple concurrent requests.
I then came up with this:
class DamageCalculatorService include TimeFilterable TOTAL_DAMAGE_DEALT_COLUMN = 'total_damage_dealt'.freeze TOTAL_DAMAGE_TAKEN_COLUMN = 'total_damage_taken'.freeze def self.damage_dealt(params) calculate_damage(**params.merge({ sort_by: TOTAL_DAMAGE_DEALT_COLUMN })) end def self.damage_taken(params) calculate_damage(**params.merge({ sort_by: TOTAL_DAMAGE_TAKEN_COLUMN })) end private def self.calculate_damage(time_filter:, timezone:, limit:, formatted_table:, sort_by:) start_time = TimeFilterable.start_time_for(time_filter: time_filter, timezone: timezone) return [] unless start_time.present? begin query = Stat.where('stats.created_at >= ?', start_time) results = query.joins(:player) .select('players.name as player_name','players.steam_id as steam_id','SUM(stats.damage_dealt) as total_damage_dealt','SUM(stats.damage_taken) as total_damage_taken' ) .group('players.id', 'players.name', 'players.steam_id') .order("#{sort_by} DESC") .limit(limit) final_results = results.map do |result| { steam_id: result.steam_id, player_name: result.player_name, total_damage_dealt: result.total_damage_dealt.to_i, total_damage_taken: result.total_damage_taken.to_i } end return formatted_table ? formatted_table(data: final_results, time_filter: time_filter, sort_by: sort_by) : results rescue ActiveRecord::StatementInvalid => e Rails.logger.error("Error in DamageCalculatorService: #{e.message}") [] end end def self.formatted_table(data:, time_filter:, sort_by:) title = "#{sort_by.titleize.split('').join('')} for the #{time_filter}" headers = %w[player_name total_damage_dealt total_damage_taken] TabletizeService.new(title: title, data: data, headers: headers).table endend
But this allows to pass any parameter I want, which doesn't seem great.
What is the clean and right way to define this?