Private service object methods (Ruby + Rails) - ruby-on-rails

I have a PaymentManager service:
/payment_manager/
- online_payment_creator.rb
- manual_payment_creator.rb
- payment_splitter.rb
Code for 3 services:
module PaymentManager
class ManualPaymentCreator < ApplicationService
def call
# do stuff in transaction
PaymentSplitter.call(a, b, c)
end
end
end
module PaymentManager
class OnlinePaymentCreator < ApplicationService
# do stuff in transaction
PaymentSplitter.call(a2, b2, c2)
end
end
module PaymentManager
class PaymentSplitter < ApplicationService
def call(x, y, z)
# do very dangerous stuff
end
end
end
So, the PaymentSplitter essentially splits a payment across multiple invoices, and must be done only inside either of the other 2 services, but should never be called on its own (from a controller or in the console, etc).
I normally would make a private method inside ManualPaymentCreator but because it's needed in BOTH services, I don't know how to do that, as I can't use a simple private instance method without duplication.

Perhaps you are looking for inheritance and protected methods
class Foo < ApplicationService
def public_method
end
protected
#everything after here is private to this class and its descendants
def some_method_to_be_shared_with_descendants
#I am only accessible from this class and from descendants of this class
...
end
private
#Everything after here is private to this class only
def some_private_methods_that_are_only_accessible_from_this_class
end
...
end
Then descendant classes like so
class Bar < Foo
def do_something
# can access protected methods from Foo class here as native methods
some_res = some_method_to_be_shared_here
# do something with some_res
end
end
So you should descend your other 2 classes from PaymentSplitter and set the shared methods as protected

Perhaps a private class contained in a module might work better, eg.
module PaymentSplitting
def self.included(klass)
raise 'Error' unless klass.in?([ManualPaymentCreator, OnlinePaymentCreator])
end
class PaymentSplitter
end
private_constant :PaymentSplitter
end
This should allow you to freely reference PaymentSplitter in any class which includes PaymentSplitting, and you can only include PaymentSplitting in ManualPaymentCreator or OnlinePaymentCreator. Referencing PaymentSplitting::PaymentSplitter will throw a NameError: private constant exception.

Related

How to use the same class name over multiple modules?

How to use the same class name over multiple modules?
I've a main ApplicationService
# /app/services/application_service.rb
class ApplicationService; end
# /app/services/processing/queue/application_service.rb
module Processing
module Queue
class ApplicationService < ApplicationService; end
end
end
# /app/services/processing/callback/application_service.rb
module Processing
module Callback
class ApplicationService < ApplicationService; end
end
end
How to get rails to not be confused and to know to use /app/services/application_service.rb
All my services in /app/services/processing/queue/ have /app/services/processing/queue/application_service.rbas parent class.
Use the FQN (fully-qualified name) ::ApplicationService to refer to your top-level class.
module Processing
module Callback
class ApplicationService < ::ApplicationService; end
end
end

extend self in a module

It seems that this two pieces of codes do the same job, but I would like to understand why I'd rather use one or another
First example:
module MyModule
extend self
def first_method
end
def second_method
end
end
Second example:
module MyModule
def self.first_method
end
def self.second_method
end
end
Your first example defines two instance methods and makes them also available as class (or module) methods via extend:
module MyModule
def first_method; end
def second_method; end
end
MyModule.instance_methods #=> [:second_method, :first_method]
MyModule.methods - Module.methods #=> []
MyModule.extend MyModule
MyModule.instance_methods #=> [:second_method, :first_method]
MyModule.methods - Module.methods #=> [:second_method, :first_method]
Whereas your second example just defines two class (or module) methods and no instance methods:
module MyModule
def self.first_method; end
def self.second_method; end
end
MyModule.instance_methods #=> []
MyModule.methods - Module.methods #=> [:second_method, :first_method]
The first variant can be useful when you want to provide some utility functions that can be called as:
MyModule.first_method
or be included in other modules / classes:
class Foo
include MyModule
def another_method
first_method # <- no explicit receiver needed
end
end
Ruby also provides the helper method module_function to define methods that way:
module MyModule
def first_method
end
module_function :first_method
end
It adds the method as a class methods and makes the instance method private. It's how the methods in Kernel work.

Why can 'self.method' not use a method in the same class?

Why when I do self.method from a class, I get an undefined method `my_method' for MyModule::MyOtherModule::MyClass:Class
module MyModule
module OtherModule
class MyClass < Base
def my_method
end
def self.my_self_method
my_method
end
end
end
end
I call my_self_method with send from an herited [sic] class:
class Base
class << self
my_method(method_name)
send("my_self_#{method_name}")
end
end
end
I don't understand it.
In your code, you're defining one instance method (my_method), and one class method (my_self_method).
This means you can call:
MyClass.my_self_method
or
MyClass.new.my_method
If you want my_method to be callable from my_self_method, you could define it as:
def self.my_method
...
end
Then the following would be available:
def self.my_self_method
my_method
end
Here's another alternative. There's a comment that suggests it's bad practice to call new.my_method from within a class method, but I've seen a pattern that applies this that I find quite idiomatic, for example:
class MyClass
def self.run(the_variables)
new(the_variables).process
end
def initialize(the_variables)
# setup the_variables
end
def process
# do whatever's needed
end
end
This allows a simple entry point of MyClass.run(the_variables). If your use case seems suitable, a similar pattern for you would be:
module MyModule
module OtherModule
class MyClass < Base
def my_method
end
def self.my_self_method
new.my_method
end
end
end
end
I'm sure there's scope to disagree with this pattern, and would be interested to hear others' opinions in the comments.
Hope this helps clear a few things up #N.Safi.

Is it okay to call a private method of a parent class's subclass from a module which is included in the parent class in rails?

Is it okay to call a private method of a parent class's subclass from a module which is included in the parent class especially when it concerns ApplicationController, Controllers and lib modules in Rails?
Consider if required to change the controller name the method name to reflect the model name(to Article) change.
I feel this is really bad coding and wanted to know what community thinks about this
Example from a Rails Application:
/lib/some_module.rb
module SomeModule
include SomeModuleResource
def filtering_method
calling_method
end
def calling_method
fetch_object
end
end
/lib/some_module_resource.rb
module SomeModuleResource
def fetch_object
note
end
end
/app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include SomeModule
before_action :filtering_method
end
/app/controllers/notes_controller.rb
class NotesController < ApplicationController
def show
end
private
def note
#note ||= Note.find(param[:id]))
end
end
I'm of the opinion that this is not necessary bad, although when you expect a certain interface (methods, variables, etc.) from the class that includes the module I would add the following:
module SomeModuleResource
def fetch_object
note
end
private
def note
raise NotImplementedError
end
end
This way, when #note is called without implementing it (because you forgot it was needed or whatever) a NotImplementedError is raised.
Another option is to work around it and create a more general solution. For example, if all controllers behave the same way you described above you can do the following:
module SomeModuleResource
def fetch_object
note
end
private
def note
klass = params[:controller].classify.constantize
instance = klass.find(params[:id])
var_name = "##{klass.underscore}"
instance_variable_set(var_name, instance) unless instance_variable_get(var_name)
end
end
You could also create a class helper method like before_action so that you can pass your own implementation.
module SomeModule
include SomeModuleResource
def self.included(base)
base.extend(ClassMethods)
end
def filtering_method
calling_method
end
def calling_method
fetch_object
end
module ClassMethods
def custom_before_action(&block)
define_method(:note, &block)
private :note
before_action :filtering_method
end
end
end
Now you can use custom_before_filter { #note ||= Note.find(params[:id]) } in every controller (after including).
The above is just to present you with ideas. I'm sure you could find better solution to the problem, but this hopefully points you in the right direction.
See: Alternatives to abstract classes in Ruby?. Or search for abstract classes in Ruby and you'll find more on this subject.

Trying to move model code to shared concerns

I have a model with the following two methods which are required in another model so I thought I'd try sharing them via a concern instead of duplicating the code.
class Region < ActiveRecord::Base
def ancestors
Region.where("lft < ? AND ? < rgt", lft, rgt)
end
def parent
self.ancestors.order("lft").last
end
end
I have created a file in app/models/concerns/sets.rb and my new model reads:
class Region < ActiveRecord::Base
include Sets
end
sets.rb is:
module Sets
extend ActiveSupport::Concern
def ancestors
Region.where("lft < ? AND ? < rgt", lft, rgt)
end
def parent
self.ancestors.order("lft").last
end
module ClassMethods
end
end
Question:
How do I share a method between models when the method references the model such as "Region.where..."
Either by referencing the class of the including model (but you need to wrap the instance methods in an included block):
included do
def ancestors
self.class.where(...) # "self" refers to the including instance
end
end
or (better IMO) by just declaring the method as a class method, in which case you can leave the class itself out altogether:
module ClassMethods
def ancestors
where(...)
end
end

Resources