Rafal Rafal - 2 months ago 16
Ruby Question

How to refactor method to lower RuboCop's ABCsize

On my journey to learn ruby & rails I went on and installed Rubocop. So far it's been a great help in refactoring my code the ruby-way, but now I think I've hit the wall with this helpless case. Given the following method for creating a new entity, I'm looking for a way to refactor it to make Rubocop stop yelling at me about:


  1. Line length

  2. Assignment Branch Condition size (currently 26.02/15)



the only thing that I can think of for the moment, except of disabling those cops ofc, is actually splitting up the model into two smaller ones (say basic info and financial) and set them up accordingly, but I get the impression that this would move the complexity out of the creation method and put it elsewhere, as I would need to remember to create both related entities etc. Any hints are more than welcome.

def create_store_information(store, meta)
user = @datasource.user
user.store_informations.create!(
name: store['name'],
description: store['description'],
status: 1,
url: store['URL'].downcase,
store_version: store['version'],
api_version: store['wc_version'],
timezone: meta['timezone'],
currency: meta['currency'],
currency_format: meta['currency_format'],
currency_position: meta['currency_position'],
thousand_separator: meta['thousand_separator'],
decimal_separator: meta['decimal_separator'],
price_num_decimals: meta['price_num_decimals'],
tax_included: cast_to_bool(meta['tax_included']),
weight_unit: meta['weight_unit'],
dimension_unit: meta['dimension_unit'],
ssl_enabled: cast_to_bool(meta['ssl_enabled']),
permalinks_enabled: cast_to_bool(meta['permalinks_enabled']),
generate_password: cast_to_bool(meta['generate_password']),
user: user
)
end


Edit:
As per request, I'm attaching a second sample of creating store_information from a different class.

def create_store_information(store, meta)
user = @datasource.user
user.store_informations.create!(
name: store['id'],
description: store['name'],
status: 1,
url: store['domain'].downcase,
store_version: '1.0',
api_version: '1.0',
timezone: meta['timezone'],
currency: meta['currency'],
currency_format: meta['money_format'],
currency_position: '', # not applicable
thousand_separator: '', # not applicable, take from user's locale
decimal_separator: '', # not applicable, take from user's locale
price_num_decimals: '', # not applicable, take from user's locale
tax_included: cast_to_bool(meta['taxes_included']),
weight_unit: nil, # not applicable
dimension_unit: nil, # not applicable
ssl_enabled: cast_to_bool(meta['force_ssl']),
permalinks_enabled: true,
generate_password: false,
user: user
)
end

Answer

This is just 1 suggestion out of many possibilities.

You can use Ruby's meta programming abilities to dynamically send methods. The meta object's fields are easy to assign the user.store_informations because the fields match 1 for 1. It is also possible for the store object but it wouldn't be as straightforward.

You can move the fields to an array inside your class definition:

CAST_TO_BOOL = %w(
  tax_included
  ssl_enabled
  permalinks_enabled
  generate_password
).freeze

META_FIELDS = %w(
  timezone
  currency
  currency_format
  currency_position
  thousand_separator
  decimal_separator
  price_num_decimals
  tax_included
  weight_unit
  dimension_unit
  ssl_enabled
  permalinks_enabled
  generate_password
).freeze

then you could define a private method which dynamically sets the meta fields of the user.store_informations

private

def set_meta_fields_to_store_information(user)
  META_FIELDS.each do |field|
    if CAST_TO_BOOL.include? field
      user.store_informations.__send__ "#{f}=" { cast_to_bool( meta[field] ) }
      next
    end
    user.store_informations.__send__ "#{f}=" { meta[field] }
  end
end

then you could call that method instead:

def create_store_information(store, meta)
  user = @datasource.user
  user.store_informations.new(
    name: store['name'],
    description: store['description'],
    status: 1,
    url: store['URL'].downcase,
    store_version: store['version'],
    api_version: store['wc_version'],
    user: user
  )
  set_meta_fields_to_store_information(user)
  user.save!
end

Edit#2

Regarding populating the fields with objects of different classes;

One way to go about it would be to define a method which assign's the fields for you depending on the class of the store. But then again, if you have thousands of different stores, this probably wouldn't be optimal.

class StoreA; end
class StoreB; end
class StoreC; end

then:

# you could also use dynamic method dispatching here instead:

def set_store_information_to_user(store, user)
  case store
  when StoreA
    assign_store_a_method(store, user)
  when StoreB
    assign_store_b_method(store, user)
  when StoreC
    assign_store_c_method(store, user)
  end
end

private
def assign_store_a_method(store, user); end
def assign_store_b_method(store, user); end
def assign_store_c_method(store, user); end
Comments