How can I DRY this code and clean up my model? - ruby-on-rails
I have the following two methods for a Number model.
def track
number = sanitize(tracking)
case determine_type(number)
when 'UPS'
tracker = ups.track(:tracking_number => number)
self.carrier = Carrier.where(:name => 'UPS').first
self.service = tracker.service_type
self.destination_country = tracker.destination_country
self.destination_state = tracker.destination_state
self.destination_city = tracker.destination_city
self.origin_country = tracker.origin_country
self.origin_state = tracker.origin_state
self.origin_city = tracker.origin_city
self.signature = tracker.signature_name
self.scheduled_delivery = tracker.scheduled_delivery_date
self.weight = tracker.weight
tracker.events.each do |event|
new_event = Event.new
new_event.number = self
new_event.city = event.city
new_event.state = event.state
new_event.postalcode = event.postal_code if event.postal_code
new_event.country = event.country
new_event.status = event.name
new_event.status_code = event.type
new_event.occured_at = event.occurred_at
new_event.save
end
end
save
end
def update_number
case determine_type(number)
when 'UPS'
tracker = ups.track(:tracking_number => tracking)
self.carrier = Carrier.where(:name => 'UPS').first
self.service = tracker.service_type
self.destination_country = tracker.destination_country
self.destination_state = tracker.destination_state
self.destination_city = tracker.destination_city
self.origin_country = tracker.origin_country
self.origin_state = tracker.origin_state
self.origin_city = tracker.origin_city
self.signature = tracker.signature_name
self.scheduled_delivery = tracker.scheduled_delivery_date
self.weight = tracker.weight
last_event = self.events.ordered.first.occured_at
tracker.events.each do |event|
if last_event and (event.occurred_at > last_event)
new_event = Event.new
new_event.number = self
new_event.city = event.city
new_event.state = event.state
new_event.postalcode = event.postal_code if event.postal_code
new_event.country = event.country
new_event.status = event.name
new_event.status_code = event.type
new_event.occured_at = event.occurred_at
new_event.save
end
end
end
save
end
As you can see, a lot of code is duplicated. And the problem becomes when I start adding the dozen or so other carriers (FedEx, USPS, DHL, etc)...my Number model gets big and hairy fast.
The only real difference between track and update_number is that update_number as an if comparison around the events to check if the events from the carrier are more recent than the latest events I have stored in the database, using that if last_event and (event.occurred_at > last_event) line.
So how can I clean up this code so my model doesn't get so fat?
A couple of things I would suggest:
Look at the Strategy Pattern, even though Ruby doesn't have interfaces, but it'll give you some ideas on how to better design this functionality (google for Strategy Pattern for Ruby to find alternatives). Basically, you'd want to have separate classes to handle your switch case statements and call the appropriate one at runtime.
Try to separate the tracker handling code from the events handling code. E.g., I would consider moving the event creation/saving logic for each of the tracker events to a separate class (or even to the Tracker entity, if you have one).
Hope this helps.
This should be solved using the Strategy pattern. For each possible tracker (DHL, UPS, ...) that will handle the creating and updating appropriately.
So that would become something like:
class Number
def track
tracker = get_tracker(tracking)
tracker.create_tracking(self)
save
end
def update_number
tracker = get_tracker(tracking)
tracker.update_tracking(self)
save
end
def get_tracker(tracking)
tracking_number = sanitize(tracking)
case determine_type(tracking_number)
when 'UPS'
UPSTracker.new(tracking_number)
when 'DHL'
DHLTracker.new(tracking_number)
end
end
end
class UPSTracker
def initialize(tracking_number)
#tracking_number = tracking_number
end
def create_tracking(number)
tracker = ups.track(:tracking_number => number)
update_number_properties(number, tracking)
# store events
tracker.events.each do |event|
create_new_event(event)
end
end
def update_tracking(number)
tracker = ups.track(:tracking_number => number)
update_number_properties(number, tracking)
last_event = self.events.ordered.first.occured_at
tracker.events.each do |event|
if last_event and (event.occurred_at > last_event)
create_new_event(event)
end
end
end
protected
def update_number_properties
number.carrier = Carrier.where(:name => 'UPS').first
number.service = tracker.service_type
number.destination_country = tracker.destination_country
number.destination_state = tracker.destination_state
number.destination_city = tracker.destination_city
number.origin_country = tracker.origin_country
number.origin_state = tracker.origin_state
number.origin_city = tracker.origin_city
number.signature = tracker.signature_name
number.scheduled_delivery = tracker.scheduled_delivery_date
number.weight = tracker.weight
end
def create_new_event
new_event = Event.new
new_event.number = self
new_event.city = event.city
new_event.state = event.state
new_event.postalcode = event.postal_code if event.postal_code
new_event.country = event.country
new_event.status = event.name
new_event.status_code = event.type
new_event.occured_at = event.occurred_at
new_event.save
end
end
This code could be further improved, I imagine the creating of events and trackings will be shared over the different carriers. So a shared base-class maybe. Secondly, in two methods inside Number we call the get_tracker: probably this tracker can be decided upon creation time (of the Number-instance) and should be fetched once and stored inside an instance variable.
Furthermore I would like to add that names should be meaningful, so a class Number does not sound meaningful enough to me. A name should express intent, and preferably match the names and concepts from your problem domain.
Something about your code doesn't make sense to me, the organization is a little strange and I don't have enough context on what your app looks like. The OOP-ish way to accomplish something like this in rails is pretty easy; you could also roll your own Strategy, Adapter, or even a Builder pattern to do this.
But, there is some low hanging fruit, you can refactor your code so the common parts are less obtrusive -- this is a little better but, create_events is still very case'y:
def track
create_events
end
def update_number
create_events {|e| last_event and (event.occurred_at > last_event) }
end
def create_events(&block)
case determine_type(number)
when 'UPS'
tracker = ups.track(:tracking_number => tracking)
self.carrier = Carrier.where(:name => 'UPS').first
self.assign_tracker(tracker)
end
tracker.events.each do |e|
self.create_event(e) unless (block_given? && !block.call(e))
end
save
end
def assign_tracker(tracker)
self.service = tracker.service_type
self.destination_country = tracker.destination_country
self.destination_state = tracker.destination_state
self.destination_city = tracker.destination_city
self.origin_country = tracker.origin_country
self.origin_state = tracker.origin_state
self.origin_city = tracker.origin_city
self.signature = tracker.signature_name
self.scheduled_delivery = tracker.scheduled_delivery_date
self.weight = tracker.weight
end
def create_event(event)
new_event = Event.new
new_event.number = self
new_event.city = event.city
new_event.state = event.state
new_event.postalcode = event.postal_code if event.postal_code
new_event.country = event.country
new_event.status = event.name
new_event.status_code = event.type
new_event.occured_at = event.occurred_at
new_event.save
end
Related
Long protobuf to poro serialization
I am trying to compare protobuf and json data exchange speed between two ruby microservices. As I thought protobuf is much faster in most of cases. As the final test I want to check if converting decoded Message proto class to some poro will be faster than doing the same using json. Here is an example of conversion with json: def self.map_all_posts_from_json(data) RubyProf.start data.map do |post| Post.new.tap do |new_post| new_post.id = post["id"] new_post.description = post["description"] new_post.content = post["content"] new_post.rating = post["rating"] new_post.user = User.new.tap do |new_user| new_user.id = post["user"]["id"] new_user.name = post["user"]["name"] new_user.surname = post["user"]["surname"] new_user.nickname = post["user"]["nickname"] end new_post.comments = post["comments"].map do |comment| Comment.new.tap do |new_comment| new_comment.id = comment["id"] new_comment.content = comment["content"] new_comment.rating = comment["rating"] new_comment.user = User.new.tap do |new_user| new_user.id = comment["user"]["id"] new_user.name = comment["user"]["name"] new_user.surname = comment["user"]["surname"] new_user.nickname = comment["user"]["nickname"] end end end end end result = RubyProf.stop printer = RubyProf::FlatPrinter.new(result) printer.print(STDOUT) end and protobuf: def self.unserialize_all_posts(data) RubyProf.start data.posts.map do |post| Post.from_message(post) end result = RubyProf.stop printer = RubyProf::FlatPrinter.new(result) printer.print(STDOUT) end Post.from_message method: def self.from_message(message) Post.new.tap do |post| post.id = message.id post.desription = message.description post.content = message.content post.rating = message.rating post.user = User.from_message(message.user) post.comments = message.comment_messages.map do |comment| Comment.from_message(comment) end end end surprisingly json way was a lot faster. Then I investigated data returned by RubyProf and found out that there were lots and lots of calls to proto_class#method_missing and other suspect methods. Did any of you had similar problem? I am using 'google-protobuf' gem.
How do I perform an ActiveRecord query after form submission?
Would this work? I want to do something like coins transfer #logs = Logs.new(log_params) #logs.save #tt = Users.where(email: params[:email]).update(money: Users.find(current_user.id)['money'] - params[:money]) #tt.save #wr = Users.find(current_user.id).update(money: Users.where(email: params[:email])['money'] + params[:money]) #wr.save
I don't know if you are planning to display those values in your views, but you could do something like this: sender = Users.where(email: params[:email]).first # This returns the user recipient = Users.find(current_user.id) # or just current_user if it inherit from the User class money_to_substract = recipient.money - params[:money] money_to_sum = recipient.money + params[:money] Then your transsaction would be a bit dryier User.transaction do #logs = Logs.new(log_params) #tt = sender.update(money: money_to_substract) # update saves to the database so no need to call save # #tt.save #wr = recipient.update(money: money_to_sum) # update saves to the database so no need to call save # #wr.save end But IMHO, I would do something like this: models/User.rb class User < ActiveRecord::Base ... def sends_money(amount) money_to_substract = self.money - amount update(money: money_to_substract) end def receives_money(amount) money_to_sum = self.money + amount update(money: money_to_sum) end ... end In your controller amount = params[:money].to_i User.transaction do #logs = Logs.new(log_params) #tt = sender.sends_money(amount) #wr = recipient.receives_money(amount) end Which makes things easier to read and follow through your code. Hope this helps!
Is using local variable instead of instance variable improve the performance(save memory) in Rails?
I have created Rails application and I have used lots of instance variables and most of them are not required in the views. Do I need to replace the unused instance variables for improving the performance? Sample code: def show custom_fields_data = fetch_custom_field_data #selected_custom_fields_opt_from_view = [] if custom_fields_data.present? #listings = #listing.category.listings.where("price_cents!=? AND open= ?",0,true).reject { |l| l.author.main_admin? } #selected_custom_fields_opt_from_view = custom_fields_data.map do |custom_field_data| CustomField.find(custom_field_data[0]).options.find(custom_field_data[1]) end #listings.each do |listing| # array to store the selected a custom field's option from Database selected_custom_fields_opt_from_db = [] listing.custom_field_values.each do |custom_field_value| selected_custom_fields_opt_from_db.push(custom_field_value.selected_options.first) end if selected_custom_fields_opt_from_db.uniq.sort == #selected_custom_fields_opt_from_view.uniq.sort || (#selected_custom_fields_opt_from_view - selected_custom_fields_opt_from_db).empty? similar_listing.push(listing) end end #listings = similar_listing end #listing_with_filters = similar_listing.present? ? #listings.first : #listing #selected_tribe_navi_tab = "home" unless current_user?(#listing.author) #listing.increment!(:times_viewed) end #current_image = if params[:image] #listing.image_by_id(params[:image]) else #listing.listing_images.first end #prev_image_id, #next_image_id = if #current_image #listing.prev_and_next_image_ids_by_id(#current_image.id) else [nil, nil] end payment_gateway = MarketplaceService::Community::Query.payment_type(#current_community.id) process = get_transaction_process(community_id: #current_community.id, transaction_process_id: #listing.transaction_process_id) form_path = new_transaction_path(listing_id: #listing.id) delivery_opts = delivery_config(#listing.require_shipping_address, #listing.pickup_enabled, #listing.shipping_price, #listing.shipping_price_additional, #listing.currency) #category = #listing.category #template_listing = #category.template_listing if #current_user # For Pivot table #selected_custom_field = params[:custom_field] if params[:custom_field] #listing_for_pivot = Listing.new #listing_images = #listing.listing_images #shape = get_shape(#listing.listing_shape_id) #unit_options = ListingViewUtils.unit_options(#shape[:units], unit_from_listing(#template_listing)).first if #shape #custom_field_questions = #category.custom_fields #numeric_field_ids = numeric_field_ids(#custom_field_questions) #category_tree = CategoryViewUtils.category_tree( categories: ListingService::API::Api.categories.get(community_id: #current_community.id)[:data], shapes: get_shapes, locale: I18n.locale, all_locales: #current_community.locales ) if #template_listing.present? #listing_for_pivot.title = #template_listing.title #listing_for_pivot.description = #template_listing.description #listing_images = #template_listing.listing_images if #template_listing.listing_images.present? #listing_for_pivot.listing_shape_id = #template_listing.listing_shape_id end if (#current_user.location != nil) temp = #current_user.location temp.location_type = "origin_loc" #listing_for_pivot.build_origin_loc(temp.attributes) else #listing_for_pivot.build_origin_loc(:location_type => "origin_loc") end #custom_field_area = CategoryCustomField.where(category_id: #category.id, custom_field_id: #category.custom_fields.pluck(:id)) #row = #category.custom_field_row #row = #custom_field_area.first.custom_field if #row.nil? && #custom_field_area.first #column = #category.custom_field_column #column = #custom_field_area.second.custom_field if #column.nil? && #custom_field_area.second #filters = #category.custom_field_filters #filters = #custom_field_area.all.from(1).map { |category_custom_field| category_custom_field.custom_field } if #filters.nil? && #custom_field_area.size > 2 #selected_value_for_filter = [] if #filters.present? if #selected_custom_field #filters.each do |filter| if (#selected_custom_field["#{filter.id.to_s}_"]) #selected_value_for_filter.push(filter.options.find(#selected_custom_field["#{filter.id.to_s}_"])) else #selected_value_for_filter.push(filter.options.first) end end else #filters.each do |filter| #selected_value_for_filter.push(filter.options.first) end end end # Pivot table section end end #applicant = #category.listings.pluck(:author_id).uniq #suggested_business_accounts = #category.people.where("people.id NOT IN (?)", #applicant); if #suggested_business_accounts.present? #business_locations = #suggested_business_accounts.map do |person| person.location end #business_locations.compact! end render locals: { form_path: form_path, payment_gateway: payment_gateway, # TODO I guess we should not need to know the process in order to show the listing process: process, delivery_opts: delivery_opts, listing_unit_type: #listing.unit_type } end
It is not recommended to use instance variables if you don't want to send them to views. The scope of the variables should be narrowest, therefore in your case if you are not using instance variables in the views you should convert them to local.
Using instance variables instead of local variables is a bad idea at least memory-wise. Instance variable exists while the object that holds it exists. On the contrary, local variable exists only inside method/block it is defined. Garbage collector does not care whether you use instance variable elsewhere beyond the method or not. Thus, if you have instance variables, which you only intend to use within the method - change them to local ones.
Why is my service not changing my instance variable?
My service is supposed to match the current user with an opponent. I have a method in my service that is supposed to find this opponent but it's not saving it in my controller. I realize this is a very basic question but I'm very new to rails, thank you! Here is my service: class MatchUser attr_accessor :user_params def initialize(user_params) #user_params = user_params end def match(opponent) return false if user_params[:matched] == true #unmatched_users = User.where(matched: false) #unique_unmatched_users = #unmatched_users.where.not(id: user_params[:id]) #same_league_unmatched_users = #unique_unmatched_users.where(league_id: user_params[:league_id]) return false if #same_league_unmatched_users.empty? opponent = #same_league_unmatched_users.sample opponent.faceoff_date = Time.now user_params[:faceoff_date] = Time.now opponent.save! end end Here is the part in my controller where I'm getting an error when I try to assign #matched_user_team because #matched_user is nil #user = current_user #matching = MatchUser.new(#user) if #matching.match(#matched_user) #matched_user_team = #matched_user.teams.last.chars end
Request Error Twitter-API Rails 4
I'm wondering how to limit the number of followers returned via the twitter api, or if there's a better way of returning the twitter followers of a user. We've been challenged to create a twitter manager, and I've done most of the stuff, but I keep getting a request error when someone has a large amount of followers, as we're supposed to get the users from the twitter api and store them in a database, and the page usually times out or gives a twitter get error too many requests and locks me out for an hour. It's very hard to develop when this keeps happening, I was just wondering if there's a better way to do it? Here is my code for the dashboard which is where the user details are returned and saved, and also where the followers are returned and save: class DashboardController < ApplicationController helper_method :logged_in? def new #just_updated = "" Twitter.configure do |config| # Test Account config.consumer_key = "none-of-your-business" config.consumer_secret = "none-of-your-business" config.oauth_token = "none-of-your-business" config.oauth_token_secret = "none-of-your-business" end #user = User.find(session[:user_id]) if #user.twitter_username.present? && #user.twitter_details_present == false #twitter_user = Twitter.user(#user.twitter_username) #user.no_of_followers = #twitter_user[:followers_count] #user.profile_picture_url = #twitter_user[:profile_image_url] #user.following = #twitter_user[:friends_count] #user.twitter_nationality = #twitter_user[:location] #user.no_of_tweets = #twitter_user[:statuses_count] #user.twitter_details_present = true #user.updated_at = Time.now if #user.save #just_updated = "We have just updated your follower details" else #just_updated = "There was a problem with your save1" end end if (Time.now - #user.updated_at) < 10.minute && (#user.updated_at - #user.created_at) > 1.hour Follower.where("owner = #{#user.id}").destroy_all end if #user.twitter_username.present? if (#followers = Follower.where("owner = #{#user.id}")).count > 0 i = Follower.first.id #followers.each do |follower| if (Time.now - Follower.where("owner = #{#user.id}").first.updated_at) > 1.hour if (follower_to_save = Follower.where(follower_id: follower[:id])).present? follower_to_save[0].follower_username = follower[:screen_name].to_s follower_to_save[0].owner = #user.id follower_to_save[0].follower_nationality = follower[:location] follower_to_save[0].no_of_followers = follower[:followers_count] follower_to_save[0].following= follower[:friends_count] follower_to_save[0].no_of_tweets = follower[:statuses_count] follower_to_save[0].profile_picture_url = follower[:profile_image_url] follower_to_save[0].updated_at = Time.now if follower_to_save[0].save #just_updated = "We have just updated your follower details" else #just_updated = "There was a problem with your save1" break; end else follower_to_save = Follower.new follower_to_save.follower_id = follower[:id] follower_to_save.owner = #user.id follower_to_save.follower_username = follower[:screen_name] follower_to_save.follower_nationality = follower[:location] follower_to_save.no_of_followers = follower[:followers_count] follower_to_save.following= follower[:friends_count] follower_to_save.no_of_tweets = follower[:statuses_count] follower_to_save.profile_picture_url = follower[:profile_image_url] follower_to_save.updated_at = Time.now if follower_to_save.save #just_updated = "We have just updated your follower details" else #just_updated = "There was a problem with your save2" break; end end else next end i=i+1 #sleep(1) end else #followers = Twitter.followers(#user.twitter_username) #followers.each do |follower| follower_to_save = Follower.new follower_to_save.follower_id = follower[:id] follower_to_save.owner = #user.id follower_to_save.follower_username = follower[:screen_name] follower_to_save.follower_nationality = follower[:location] follower_to_save.no_of_followers = follower[:followers_count] follower_to_save.following= follower[:friends_count] follower_to_save.no_of_tweets = follower[:statuses_count] follower_to_save.profile_picture_url = follower[:profile_image_url] follower_to_save.updated_at = Time.now if follower_to_save.save #just_updated = "We have just compiled your followers" else #just_updated = "There was a problem with your save3" break; end #sleep(1) end end else #no_twitter_username = "Please go into settings and add your twitter username to start." end #followers = Follower.all.where("owner = #{#user.id}") #follower_count = #followers.count end def logged_in? if session[:user_id].present? true else false end end end The code is pretty inefficient right now, but I'm just trying to get it working.