singleton method - Ruby clarity http://rubyclarity.com/ Refactorings of Ruby/Rails projects Tue, 06 Jun 2017 06:12:33 +0000 en-US hourly 1 https://wordpress.org/?v=5.4.7 acts_as_list refactoring part 2 https://rubyclarity.com/2017/01/acts_as_list-refactoring-part-2/?utm_source=rss&utm_medium=rss&utm_campaign=acts_as_list-refactoring-part-2 https://rubyclarity.com/2017/01/acts_as_list-refactoring-part-2/#respond Fri, 27 Jan 2017 15:57:48 +0000 https://rubyclarity.com/?p=196 In this post I'm continuing refactoring of acts_as_list gem I started in part 1. As you might remember, I've split .acts_as_list method into several modules, each module dedicated to an option passed to the method. E.g. ColumnMethodDefiner module defines methods related to the column option (the option defines column name for storing record's list position). This post is dedicated to refactoring of the ColumnMethodDefiner module. Improving ColumnMethodDefiner module So, I've extracted code related to column option of .acts_as_list to ColumnMethodDefiner.

The post acts_as_list refactoring part 2 first appeared on Ruby clarity.

]]>
In this post I'm continuing refactoring of acts_as_list gem I started in part 1.

As you might remember, I've split .acts_as_list method into several modules, each module dedicated to an option passed to the method. E.g. ColumnMethodDefiner module defines methods related to the column option (the option defines column name for storing record's list position).

This post is dedicated to refactoring of the ColumnMethodDefiner module.

Improving ColumnMethodDefiner module

So, I've extracted code related to column option of .acts_as_list to ColumnMethodDefiner. Here's an excerpt:

module ActiveRecord::Acts::List::ColumnMethodDefiner #:nodoc:
def self.call(caller_class, column)
caller_class.class_eval do
attr_reader :position_changed
define_method :position_column do
column
end
define_method :"#{column}=" do |position|
write_attribute(column, position)
@position_changed = true
end
# only add to attr_accessible
# if the class has some mass_assignment_protection
if defined?(accessible_attributes) and !accessible_attributes.blank?
attr_accessible :"#{column}"
end
...

Step 1: what is "column"?

Line 7 (see above ↑) references column, but what column is that? Line 6 hints that we're talking about position column, i.e. column means "name of the column that holds record's position in the list". I.e. position_column_name. Unfortunately, it's too hard to read, so I opted for position_column, which is easier to read:

module ActiveRecord::Acts::List::ColumnMethodDefiner #:nodoc:
def self.call(caller_class, position_column)
caller_class.class_eval do
attr_reader :position_changed
define_method :position_column do
position_column
end
define_method :"#{position_column}=" do |position|
write_attribute(position_column, position)
@position_changed = true
end
# only add to attr_accessible
# if the class has some mass_assignment_protection
if defined?(accessible_attributes) and !accessible_attributes.blank?
attr_accessible :"#{position_column}"
end
...

I like that the method defined on the line 6 (see above ↑) has the same name as #position_column method. Earlier, we had to reason as to why column argument and #position_column method contained the same data, were named differently. But no more! One concept less!

Fed up working on bad code? Here's a way out!

For people that that want to stop suffering from bad code I’ve made a FREE course

Step 2: inconsistent module name

At this point, ColumnMethodDefiner module's mission is to define methods related to position_column, but the module is named as if it works with just Column. It is inconsistent, so I'm going to rename it to PositionColumnMethodDefiner:

def acts_as_list(column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom)
caller_class = self
PositionColumnMethodDefiner.call(caller_class, column)
...

On the line 4 (see above ↑), we still use column argument though, but from the module name, we can infer that we talk about position column.

I would have liked to deprecate the column argument and introduce position_column to replace it, but that would be changing functionality, and refactoring is all about restructuring code and keeping functionality intact.

Step 3: a method that's too long

PositionColumnMethodDefiner.call is 46 lines long and starts with defining some instance methods:

module ActiveRecord::Acts::List::PositionColumnMethodDefiner #:nodoc:
def self.call(caller_class, position_column)
caller_class.class_eval do
attr_reader :position_changed
define_method :position_column do
position_column
end
define_method :"#{position_column}=" do |position|
write_attribute(position_column, position)
@position_changed = true
end
...

Since the method is too long, I'm going to extract #define_instance_methods:

module ActiveRecord::Acts::List::PositionColumnMethodDefiner #:nodoc:
def self.call(caller_class, position_column)
define_instance_methods(caller_class, position_column)
caller_class.class_eval do
...
end
private
def self.define_instance_methods(caller_class, position_column)
caller_class.class_eval do
attr_reader :position_changed
...

Because in part 1 I've chosen to extract stuff related to position column to a separate module, I can now extract methods from .call method and not be afraid to pollute the namespace (as opposed to a single module for all .acts_as_list options).

A sidenote on what not to do

An interesting thing to note is that line 3 (see above ↑) doesn't need to be inside .class_eval block that starts on line 5. At first, I made a mistake of putting the .define_instance_methods method call inside the block, and it led to a problem. The problem was that inside .class_eval block, self points not to the PositionColumnMethodDefiner module, and I had to do a hack to call .define_instance_methods. It was ugly! Feast your eyes on this:

module ActiveRecord::Acts::List::PositionColumnMethodDefiner #:nodoc:
SELF = self
def self.call(caller_class, position_column)
caller_class.class_eval do
SELF.define_instance_methods(caller_class, position_column)
...
end

Yuck!

Step 3.1: extract class method definitions

Starting at the line 12 (see below ↓), there are several class methods defined via #define_singleton_method:

module ActiveRecord::Acts::List::PositionColumnMethodDefiner #:nodoc:
def self.call(caller_class, position_column)
define_instance_methods(caller_class, position_column)
caller_class.class_eval do
# only add to attr_accessible
# if the class has some mass_assignment_protection
if defined?(accessible_attributes) and !accessible_attributes.blank?
attr_accessible :"#{position_column}"
end
define_singleton_method :quoted_position_column do
@_quoted_position_column ||= connection.quote_column_name(position_column)
end
...

I'm going to extract those class method definitions into a method:

module ActiveRecord::Acts::List::PositionColumnMethodDefiner #:nodoc:
def self.call(caller_class, position_column)
define_class_methods(caller_class, position_column)
define_instance_methods(caller_class, position_column)
...

Sidenote about Object#define_singleton_method

It was my first time encountering #define_singleton_method, and the docs didn't explain it well: "Defines a singleton method in the receiver". WTF is a singleton method? I know the singleton pattern, but that doesn't make any sense here.

It turns out, a singleton method is a method defined on an object instance. A class, for example, Object class, is an instance of class Class, so a class method foo on Object (Object.foo) is a singleton method too. As well as a method defined on a string:

s = "abc"
s.define_singleton_method :foo
s.foo

So, in Ruby def self.foo method is a class method, and at the same time, a singleton method.

If you feel like diving into this a bit more, there's a great article Ways to Define Singleton Methods in Ruby.

Step 4: mass assignment protection

After I've extracted class and instance method definition we're left with adding position_column as an accessible attribute on line 10 (see below ↓). attr_accessible allows to specify a white list of model attributes that can be set via mass-assignment.

module ActiveRecord::Acts::List::PositionColumnMethodDefiner #:nodoc:
def self.call(caller_class, position_column)
define_class_methods(caller_class, position_column)
define_instance_methods(caller_class, position_column)
caller_class.class_eval do
# only add to attr_accessible
# if the class has some mass_assignment_protection
if defined?(accessible_attributes) and !accessible_attributes.blank?
attr_accessible :"#{position_column}"
end
end
end
...

Step 4.1: redundand interpolation

At the line 10 (see above ↑), position_column is interpolated and then converted to a Symbol. We can do away with the interpolation here (see the line 10 below ↓):

module ActiveRecord::Acts::List::PositionColumnMethodDefiner #:nodoc:
def self.call(caller_class, position_column)
define_class_methods(caller_class, position_column)
define_instance_methods(caller_class, position_column)
caller_class.class_eval do
# only add to attr_accessible
# if the class has some mass_assignment_protection
if defined?(accessible_attributes) and !accessible_attributes.blank?
attr_accessible position_column.to_sym
end
end
end
...

Step 4.2: comments

One of the worst things you can find in code is comments, and I hate them with passion. Sometimes you can't help but have comments, sometimes it's a necessary evil, but not in this case. On the lines 7-8 (see above ↑) the comments explain that we only protect position_column from mass-assignment if the user already uses mass-assignment protection. Can we say the same thing without comments? Absolutely!

module ActiveRecord::Acts::List::PositionColumnMethodDefiner #:nodoc:
def self.call(caller_class, position_column)
define_class_methods(caller_class, position_column)
define_instance_methods(caller_class, position_column)
if mass_assignment_protection_was_used_by_user?(caller_class)
caller_class.class_eval do
attr_accessible position_column.to_sym
end
end
end
...

So, instead of a long conditional, we have a method call .mass_assignment_protection_was_used_by_user?, that is much easier to understand and is at the right level of abstraction.

However, lines 7-9 (see above ↑) are still at the wrong level of abstraction, so I'm going to extract them into a method:

module ActiveRecord::Acts::List::PositionColumnMethodDefiner #:nodoc:
def self.call(caller_class, position_column)
define_class_methods(caller_class, position_column)
define_instance_methods(caller_class, position_column)
if mass_assignment_protection_was_used_by_user?(caller_class)
protect_attributes_from_mass_assignment(caller_class, position_column)
end
end
...

So, I've extracted protecting position_column attribute into .protect_attributes_from_mass_assignment method (see line 7 above ↑).

I feel it reads much better without any comments now.

Step 4.3: .mass_assignment_protection_was_used_by_user?

Let's see whether the code that I've extracted can be improved:

def self.mass_assignment_protection_was_used_by_user?(caller_class)
caller_class.class_eval do
defined?(accessible_attributes) and !accessible_attributes.blank?
end
end

On the line 3 (see above ↑) we check whether accessible_attributes is defined. But what is accessible_attributes? It seems that it's an undocumented Rails method.

In Rails 2.3.8 accessible_attributes used to reference attr_accessible attribute (used to store those attributes that allow mass assignment). In Rails 4, attr_accessible was removed in favour of strong parameters and thus, would no longer be defined.

This explains why accessible_attributes may not be defined, and I will not dive deeper into undocumented Rails stuff.

Step 4.3.1: gratuitous use of defined?

defined?(accessible_attributes) returns a truthful value if . accessible_attributes is defined. However, it would also return a truthful value if a variable named accessible_attributes was defined. It's not very likely that such variable would be defined, but for somebody reading it thoroughly, it makes code harder to understand. "Did the author really mean that accessible_attributes variable counts as mass protection defined?". Thus, it's better to replace defined? with #respond_to?:

def self.mass_assignment_protection_was_used_by_user?(caller_class)
caller_class.class_eval do
respond_to?(:accessible_attributes) and !accessible_attributes.blank?
end
end

In this way, it's clear that we're looking for a method .accessible_attributes, and there are no further questions.

Step 4.3.2: gratuitous negation

But we're not done with the .mass_assignment_protection_was_used_by_user? method yet. On the line 3 (see above ↑) we check whether accessible_attributes is not #blank?. It's probably always better to avoid using negation. In this case, we can use #present?:

def self.mass_assignment_protection_was_used_by_user?(caller_class)
caller_class.class_eval do
respond_to?(:accessible_attributes) and accessible_attributes.present?
end
end

Now I'm happy with the method.

Step 5: too much of passing caller_class around

To remind you what the state of .call method is:

module ActiveRecord::Acts::List::PositionColumnMethodDefiner #:nodoc:
def self.call(caller_class, position_column)
define_class_methods(caller_class, position_column)
define_instance_methods(caller_class, position_column)
if mass_assignment_protection_was_used_by_user?(caller_class)
protect_attributes_from_mass_assignment(caller_class, position_column)
end
end
...

We are passing caller_class to each method call here. We could define a class instance variable and reference it in class methods later:

module ActiveRecord::Acts::List::PositionColumnMethodDefiner #:nodoc:
def self.call(caller_class, position_column)
@caller_class = caller_class
define_class_methods(position_column)
define_instance_methods(position_column)
if mass_assignment_protection_was_used_by_user?
protect_attributes_from_mass_assignment(position_column)
end
end
...
def self.mass_assignment_protection_was_used_by_user?(caller_class)
@caller_class.class_eval do
respond_to?(:accessible_attributes) and accessible_attributes.present?
end
end
...

Voila! Reads much better!

Step 6: but it's not thread safe!

But alas, using a class instance variable is not thread safe :(

I have two choices here:

  1. Use a service object.
  2. Use a thread variable.

Step 6.1: using a service object

Long story short, I've refactored to this:

module ActiveRecord::Acts::List::PositionColumnMethodDefiner #:nodoc:
def self.call(caller_class, position_column)
Definer.new(caller_class, position_column).call
end
class Definer
def initialize(caller_class, position_column)
@caller_class, @position_column = caller_class, position_column
end
def call
define_class_methods(@position_column)
define_instance_methods(@position_column)
if mass_assignment_protection_was_used_by_user?
protect_attributes_from_mass_assignment(@position_column)
end
end
...

And, I can't stand it. The cure is worse than the disease. In the #call method (see the lines 12-17 above ↑) I'm passing an instance variable @position_column as a method argument. It's awful, but it's that or I have to say something like position_column = @ position_column for the variable to be picked up by a #class_eval block. Neither of the options are good. So, it's a no-go.

Step 6.2: using a thread variable

So, I've refactored to use a thread variable:

module ActiveRecord::Acts::List::PositionColumnMethodDefiner #:nodoc:
def self.call(caller_class, position_column)
self.caller_class = caller_class
define_class_methods(position_column)
define_instance_methods(position_column)
if mass_assignment_protection_was_used_by_user?
protect_attributes_from_mass_assignment(position_column)
end
end
private
def self.caller_class=(value)
Thread.current.thread_variable_set :acts_as_list_caller_class, value
end
def self.caller_class
Thread.current.thread_variable_get :acts_as_list_caller_class
end
def self.define_class_methods(position_column)
caller_class.class_eval do
define_singleton_method :quoted_position_column do
@_quoted_position_column ||= connection.quote_column_name(position_column)
end
...

Much better than service object, but the cognitive load is there. It's just far from being standard to say self.caller_class = caller_class. And thread variable instead of just another method argument? That takes much more thinking. "Why a thread variable?", "What does self.caller_class = caller_class assignment mean?". It's a no-go either.

Step 7: back to the functional solution

So, in the end I was unable to improve on this:

module ActiveRecord::Acts::List::PositionColumnMethodDefiner #:nodoc:
def self.call(caller_class, position_column)
define_class_methods(caller_class, position_column)
define_instance_methods(caller_class, position_column)
if mass_assignment_protection_was_used_by_user?(caller_class)
protect_attributes_from_mass_assignment(caller_class, position_column)
end
end
...

Can you think of a way to improve it?

What to expect from part 3?

In part 3 I'll dive into methods defined with #define_singleton_method in .define_class_methods. Some of them use class instance variables, so they may not be thread safe. I'm looking forward to finding out.

That's all for today, and, happy hacking!

P.S. my PR was accepted by acts_as_list project!

The post acts_as_list refactoring part 2 first appeared on Ruby clarity.

]]>
https://rubyclarity.com/2017/01/acts_as_list-refactoring-part-2/feed/ 0
acts_as_list refactoring part 1 https://rubyclarity.com/2016/11/acts_as_list-refactoring-part-1/?utm_source=rss&utm_medium=rss&utm_campaign=acts_as_list-refactoring-part-1 https://rubyclarity.com/2016/11/acts_as_list-refactoring-part-1/#comments Mon, 28 Nov 2016 17:20:27 +0000 https://rubyclarity.com/?p=184 Today I'm going to refactor acts_as_list Rails library. It allows to treat Rails model records as part of an ordered list and offers methods like #move_to_bottom and #move_higher. Step 1: .acts_as_list introduction .acts_as_list is available as a class method in ActiveRecord::Base when acts_as_list gem is loaded. Here's an excerpt from .acts_as_list definition: Using ClassMethods module is customary in Rails, but it's not a requirement to be familiar with it to read this article. All you need to know is that

The post acts_as_list refactoring part 1 first appeared on Ruby clarity.

]]>
Today I'm going to refactor acts_as_list Rails library. It allows to treat Rails model records as part of an ordered list and offers methods like #move_to_bottom and #move_higher.

Step 1: .acts_as_list introduction

.acts_as_list is available as a class method in ActiveRecord::Base when acts_as_list gem is loaded. Here's an excerpt from .acts_as_list definition:

module ClassMethods
def acts_as_list(options = {})
configuration = { column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom}
configuration.update(options) if options.is_a?(Hash)
...

Using ClassMethods module is customary in Rails, but it's not a requirement to be familiar with it to read this article. All you need to know is that .acts_as_list is a class method when used in a Rails model.

As you can see on the line 3 above ↑, there are 4 options that can be passed to .acts_as_list:

  • column: db column to store position in the list.
  • scope: restricts what is to be considered a list. For example, enabled = true SQL could be used as scope, to limit list items to those that are enabled.
  • top_of_list: a number the first element of the list will have as position.
  • add_new_at: specifies whether new items get added to the :top or :bottom of the list.

Fed up working on bad code? Here's a way out!

For people that that want to stop suffering from bad code I’ve made a FREE course

Step 2: the problem with passing options

Options are passed as options argument, and a hash is expected (see the line 2 above ↑). Then, the default configuration hash is updated with the passed options on the line 4, thus overriding defaults with the passed options.

The problem here is that the caller can make mistakes:

  • By passing not a Hash, but something else:

acts_as_list :column
acts_as_list 1

  • By passing a wrongly spelled option (:columm instead of :column):

acts_as_list columm: "order"

In both cases, .acts_as_list will fail silently, leaving the user to figure out what went wrong by themselves.

Using Ruby 2 keyword arguments solves both described problems:

def acts_as_list(column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom)
configuration = { column: column, scope: scope, top_of_list: top_of_list, add_new_at: add_new_at }

Step 3: configuration variable

def acts_as_list(column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom)
configuration = { column: column, scope: scope, top_of_list: top_of_list, add_new_at: add_new_at }
if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
configuration[:scope] = :"#{configuration[:scope]}_id"
end

Using configuration variable after using keyword arguments does look confusing, and it's so much shorter without it:

def acts_as_list(column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom)
if scope.is_a?(Symbol) && scope.to_s !~ /_id$/
scope = :"#{scope}_id"
end

I realise that it puts cognitive load on us, to figure out that scope is part of configuration, but if the method is short (and currently it's not short), it'll be ok. Meanwhile, I'll enjoy shorter names :)

Step 4: the problem with ad-hoc solution

As you can see on the line 3 above ↑, _id suffix is added to scope. The problem with this line is twofold:

  1. It tells the story of what it does, but doesn't tell why.
  2. It's an ad-hoc solution, and is harder to read than a standard solution.

I thought of extracting that into a method (thus, solving the 1st problem), but fortunately, I guessed that there must be a method out there doing that already. And indeed, there is: ActiveSupport::Inflector.foreign_key. So, I'm going to use it:

module ClassMethods
include ActiveSupport::Inflector
def acts_as_list(column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom)
if scope.is_a?(Symbol) && scope.to_s !~ /_id$/
scope = foreign_key(scope).to_sym
end

The #foreign_key method fits perfectly here, because, scope is described in the comments as Given a symbol, it'll attach _id and use that as the foreign key restriction. Not only it's a standard solution, the story it tells, fits well into what .acts_as_list does.

As you can see on the line 2 above ↑, I've chosen to include ActiveSupport::Inflector into ClassMethods, thus polluting all classes ClassMethods will be extending. But this is temporary, and I'll figure out later, how to fix that.

Step 4.1: a hairy conditional

On the lines 5-7 (see above ↑), we add _id suffix to scope if it's a Symbol and doesn't end with _id already. This code is ripe for extracting a method:

def acts_as_list(column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom)
scope = idify(scope) if scope.is_a?(Symbol)
....
end
def idify(name)
return name.to_sym if name.to_s =~ /_id$/
foreign_key(name).to_sym
end

On the line 2 (see above ↑) you can see that I haven't extracted the check of whether scope is a Symbol. I believe, it would be less readable to have just scope = idify(scope) as it'd look like we add _id suffix always. But this is not the case, the suffix is added only for symbols (strings are left untouched).

However, there's one problem with this setup. Having #idify in the module ClassMethods pollutes namespace of ActiveRecord::Base.

Step 5: split .acts_as_list into smaller pieces

At this stage, .acts_as_list method is 118 lines long. Here's a short snippet:

def acts_as_list(column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom)
scope = idify(scope) if scope.is_a?(Symbol)
caller_class = self
class_eval do
define_singleton_method :acts_as_list_top do
top_of_list.to_i
end
define_method :acts_as_list_top do
top_of_list.to_i
end
define_method :acts_as_list_class do
caller_class
end
...

The code in .acts_as_list defines methods and Rails callbacks, related to column, scope, top_of_list, add_new_at arguments. It seems like a good idea to group code by those arguments, putting scope-related stuff into one place and column-related, into some other place.

Step 5.1: ways to split .acts_as_list

I see 3 approaches to split .acts_as_list, and I'm going to describe them below.

Approach 1: a module with methods

To avoid polluting ClassMethods namespace, add a module AuxMethods and split .acts_as_list into multiple methods. It'd look something like this:

def acts_as_list(column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom)
caller_class = self
AuxMethods.define_column_methods(caller_class, column)
AuxMethods.define_scope_methods(caller_class, scope)
AuxMethods.define_top_of_list_methods(caller_class, top_of_list)
AuxMethods.define_add_new_at_methods(caller_class, att_new_at)
...

The problem with this approach is that methods names aren't very readable. Also, since we can't include AuxMethods to ClassMethods, we can't get rid of AuxMethods. prefix. And it doesn't read that well too.

Approach 2: a service object

A service object could look like this:

def acts_as_list(column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom)
caller_class = self
definer = MethodDefiner.new(caller_class, column, scope, top_of_list, add_new_at)
definer.define_column_methods
definer.define_scope_methods
definer.define_top_of_list_methods
definer.define_add_new_at_methods
...

I think, it's even worse than the approach 1. It looks like the methods that are defined when #define_column_methods is called, are defined on the definer object. And, it's one line longer.

Approach 3: multiple modules

def acts_as_list(column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom)
caller_class = self
ColumnMethodDefiner.call(caller_class, column)
ScopeMethodDefiner.call(caller_class, scope)
TopOfListMethodDefiner.call(caller_class, top_of_list)
AddNewAtMethodDefiner.call(caller_class, add_new_at)
...

This is my favourite of the three, because:

  • The first thing you read is what argument the defined methods belong to, e.g. ColumnMethodDefiner.
  • The modules' .call methods only take the arguments the modules need (better than the approach 2).

Step 5.2: the result of splitting into multiple modules

After the module extraction (I chose the approach 3), .acts_as_list looks like this:

def acts_as_list(column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom)
caller_class = self
ColumnMethodDefiner.call(caller_class, column)
ScopeMethodDefiner.call(caller_class, scope)
TopOfListMethodDefiner.call(caller_class, top_of_list)
AddNewAtMethodDefiner.call(caller_class, add_new_at)
class_eval do
define_method :acts_as_list_class do
caller_class
end
end
before_validation :check_top_position
before_destroy :lock!
after_destroy :decrement_positions_on_lower_items
before_update :check_scope
after_update :update_positions
after_commit :clear_scope_changed
if add_new_at.present?
before_create "add_to_list_#{add_new_at}".to_sym
end
include ::ActiveRecord::Acts::List::InstanceMethods
end

So, instead of 118 lines, .acts_as_list is 30 lines now, and fits into a page.

Step 5.3: redundant class_eval

Exactly because I have reduced the number of lines, I can now pay more attention to what's left. And, on the line 9 (see above ↑) there's a redundant .class_eval call. This call changes execution context from self to, well, self. That's why it's redundant. After removal, we get (see the lines 9-11 below ↓):

def acts_as_list(column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom)
caller_class = self
ColumnMethodDefiner.call(caller_class, column)
ScopeMethodDefiner.call(caller_class, scope)
TopOfListMethodDefiner.call(caller_class, top_of_list)
AddNewAtMethodDefiner.call(caller_class, add_new_at)
define_method :acts_as_list_class do
caller_class
end
before_validation :check_top_position
before_destroy :lock!
after_destroy :decrement_positions_on_lower_items
before_update :check_scope
after_update :update_positions
after_commit :clear_scope_changed
if add_new_at.present?
before_create "add_to_list_#{add_new_at}".to_sym
end
include ::ActiveRecord::Acts::List::InstanceMethods
end

Step 5.4: Rails callbacks

On the lines 13-25 (see above ↑), there are lots of Rails callbacks created. I've already added ColumnMethodDefiner.call, etc, so having callback code here breaks Single Level of Abstraction. I've extracted the Rails callbacks into a separate module (see the line 13 below ↓):

def acts_as_list(column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom)
caller_class = self
ColumnMethodDefiner.call(caller_class, column)
ScopeMethodDefiner.call(caller_class, scope)
TopOfListMethodDefiner.call(caller_class, top_of_list)
AddNewAtMethodDefiner.call(caller_class, add_new_at)
define_method :acts_as_list_class do
caller_class
end
CallbackDefiner.call(caller_class, add_new_at)
include ::ActiveRecord::Acts::List::InstanceMethods
end

Step 5.5: #acts_as_list_class method

If Rails callbacks break Single Level of Abstraction, doesn't code on the lines 9-11 (see above ↑) break it too? It does. Because it's so small, it seems that there's no harm in having it there as it is, but I don't really care to read that #acts_as_list_class is added, I'd rather read a high-leveled description of what kind of functionality it provides.

So, I've looked up the rest of the code and, #acts_as_list_class is just used internally by the gem. So, it's an auxiliary method. I've extracted it into its own module (see the line 9 below ↓):

def acts_as_list(column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom)
caller_class = self
ColumnMethodDefiner.call(caller_class, column)
ScopeMethodDefiner.call(caller_class, scope)
TopOfListMethodDefiner.call(caller_class, top_of_list)
AddNewAtMethodDefiner.call(caller_class, add_new_at)
AuxMethodDefiner.call(caller_class)
CallbackDefiner.call(caller_class, add_new_at)
include ::ActiveRecord::Acts::List::InstanceMethods
end

Is this the best I can do?

I could possibly treat definers as plugins and load them with:

def acts_as_list(column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom)
load_definer_plugins dir: "definers"
include ::ActiveRecord::Acts::List::InstanceMethods
end

But I think it'd be an overkill. My main argument against that is that these modules aren't really plugins. If there was a standard way to add plugins in Ruby, that might have been plausible, but adding an ad-hoc plugin system would only make things more complicated. And instead of reading a number of .calls, reader would have to figure out the plugin system. A no-go.

So, that's the best I can do with this method (as per step 4.5).

What to expect from part 2?

In part 2 I'll reap the consequences of choosing the approach 3 to split .acts_as_list into modules, and will refactor one of those modules. I've already started on that, so I can say that it's interesting to see how the choice to use a separate module allowed to further improve the code by extracting methods. Single Responsibility Principle isn't there for nothing after all :)

If you want to know when the part 2 is out, sign up for my email list.

Happy hacking!

The post acts_as_list refactoring part 1 first appeared on Ruby clarity.

]]>
https://rubyclarity.com/2016/11/acts_as_list-refactoring-part-1/feed/ 3