Home > OS >  How to move logic out of a Rails model into a service module?
How to move logic out of a Rails model into a service module?

Time:11-15

I want to move some functionality out of a Rails model into a service module.

A concern isn't the correct thing for this as it's only for one model, I just want to tidy up some code elsewhere.

I can't seem to get basic calls on the model to work, here's the set up I have:

/app/models/account.rb

class Account < ApplicationRecord
    include SomeService
end

And in a differen't location:

app/services/some_service.rb

module SomeService

  def test_code
    "abc"
  end

  def self.test_code_2
    "xyz"
  end

end

In this case I would then expect Account.first.test_code to output abc and Account.test_code_2 to output zyx.

How do I move functionaity like this out of a model but not into a concern? I feel like I'm very close to this working.

CodePudding user response:

You can define a ClassMethods module inside your module and include it in the base class. This way, the normal module methods will be available as instance methods and the methods defined inside ClassMethods will be available as class methods in the base class.

app/services/some_service.rb

module SomeService

  def self.included(base)
    base.extend ClassMethods
  end

  def test_code
    "abc"
  end

  module ClassMethods
    def test_code_2
      "xyz"
    end
  end
end

CodePudding user response:

This code doesn't actually define a class method:

module SomeService
  def test_code
    "abc"
  end

  def self.test_code_2
    "xyz"
  end
end

It declares a method on the module itself which you can verify by running SomeService.test_code_2. Thats because self is not a "class method keyword" like static in other languages - its just a reference to the current lexical scope. In this case the module itself.

When you declare methods in a class:

class Foo
  def self.bar
    "Hello world"
  end
end

self is whats known as the singleton class - an instance of the Class class. So it defines a method on the Foo class.

When you include a module in a class you're adding the module to the ancestors chain of the class. It can thus call the instance methods of the module as if it where its own. You can contrast this with extend which imports the methods of the module into the class (test_code becomes a class method).

Thus the ClassMethods pattern which extends the class with an inner module when its included:

module SomeService
  def self.included(base)
    base.extend ClassMethods
  end

  def test_code
    "abc"
  end

  module ClassMethods
    def test_code_2
      "xyz"
    end
  end
end

How do I move functionaity like this out of a model but not into a concern?

What you're doing is a concern. The term "concern" in Rails really just vaguely means something like "a module thats mixed into classes". The only real definition is that app/models/concerns and app/controllers/concerns are autoloading roots and ActiveSupport::Concern exists which just simplefies the boilerplate code needed when writing mixins. Like for example you can use its class_methods macro to shorten the above code.

There is no actual definition of what a concern should contain or what its role is in an application nor is there any requirement that it be mixed into multiple classes.

But...

Moving logic out of a model (or any class) and into a module actually accomplishes nothing if you then include it back into the model. You're just obscuring the code by shuffling it into multiple files.

The amount of responsibilites and complexity remains the same.

If you actually want to redestribute the responsibilites you want to create an object that can stand on its own and does a unit of work:

class SomeService
  def initialize(thing)
     @thing = thing
  end

  def perform
    # do something awesome with @thing
  end

  def self.perform(thing)
    new(thing).perform
  end
end

This is commonly known as the service object pattern - service modules are AFAIK not a thing. This has a defined set of responsibilites and offloads the model. ActiveJob is an example of this pattern.

What you are doing is known as method extraction - basically just splitting a god like object into modules because modules are good and big classes are evil. Right? Nope. Its still a god class. This became really popular around the time that Rails introduced the concerns folders.

Another solution that should not be overlooked is to look at if the model is actually doing to much and should be split into multiple models with more clearly defined responsibilites.

  • Related