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.

Resources