I am implementing 'service objects' as per a workshop I've been studying, I'm building a reddit API application. I need the object to return something, so I can't just execute everything in the initializer. I have these two options:
Option1: Class needs instantiating
class SubListFromUser
def user_subscribed_subs(client)
#client = client
#subreddits = sort_subs_by_name(user_subs_from_reddit)
end
private
def sort_subs_by_name(subreddits)
subreddits.sort_by { |sr| sr[:name].downcase }
end
def user_subs_from_reddit
#client.subscribed_subreddits :limit => 100
end
end
Called with:
#subreddits = SubListFromUser.new(#client).user_subscribed_subs
Or Option2 is having it as a class method:
class SubListFromUser
def self.user_subscribed_subs(client)
sort_subs_by_name(client, user_subs_from_reddit)
end
private
def self.sort_subs_by_name(subreddits)
subreddits.sort_by { |sr| sr[:name].downcase }
end
def self.user_subs_from_reddit(client)
client.subscribed_subreddits :limit => 100
end
end
Called with:
#subreddits = SubListFromUser.user_subscribed_subs(#client)
What is considered 'best practice' in this situation? Is there a reason I shouldn't be using object.new(args).method? I think it gives a cleaner service class but I'm not sure of the technicalities of this approach and if it has disadvantages.
Edit: Or option3 - I'm going about this all wrong and there is a better approach :)
In many cases you'll need to keep a state for the process lifecycle, such as the client. Instead of having it "travel" through all methods you need it, as an argument, it makes more sense to keep it as a class variable. But for the sake of cleaner syntax, I recommend to combine the two approaches:
class SubListFromUser
def initialize(client)
#client = client
end
private_class_method :new # only this class can create instances of itself
def user_subscribed_subs
#subreddits = sort_subs_by_name(user_subs_from_reddit)
end
private
def sort_subs_by_name(subreddits)
subreddits.sort_by { |sr| sr[:name].downcase }
end
def user_subs_from_reddit
#client.subscribed_subreddits :limit => 100
end
class << self
def user_subscribed_subs(client)
new(client).user_subscribed_subs # create instance of this class and run a process
end
end
end
Call as a class method:
#subreddits = SubListFromUser.user_subscribed_subs(#client)
In Ruby, I don't find that there's much of a difference.
I find the use of class variables in your "static" version a bit disturbing.
I think the class version might lead to more-creative re-use through subclassing, but that brings its own set of headaches unless things are designed as correctly as possible.
Related
I have method in my rails model which returns anonymous class:
def today_earnings
Class.new do
def initialize(user)
#user = user
end
def all
#user.store_credits.where(created_at: Time.current.beginning_of_day..Time.current.end_of_day)
end
def unused
all.map { |el| el.amount - el.amount_used }.instance_eval do
def to_f
reduce(:+)
end
end
end
def used
all.map(&:amount_used).instance_eval do
def to_f
reduce(:+)
end
end
end
end.new(self)
end
I want to achieve possibility to chain result in that way user.today_earning.unused.to_f and I have some problems with instance eval because when I call to_f on result it's undefined method, I guess it is due to ruby copying returned value so the instance gets changed, is it true? And if I'm correct how can I change the code to make it work. Also I'm wondering if making new model can be better solution than anomyous class thus I need advice if anonymous class is elegant in that case and if so how can I add to_f method to returned values
Yes, Anonymous class makes the code much complex. I would suggest a seperate class. It will solve 2 problems here.
defining some anonymous class again and again when we call the today_earnings method.
Readability of the code.
Now coming to actual question, you can try something similar to hash_with_indifferent_access. The code looks as follows.
class NumericArray < Array
def to_f
reduce(:+)
end
end
Array.class_eval do
def with_numeric_operations
NumericArray.new(self)
end
end
Usage will be:
Class Earnings
def initialize(user)
#user = user
end
def all
#user.store_credits.where(created_at: Time.current.beginning_of_day..Time.current.end_of_day)
end
def unused
all.map { |el| el.amount - el.amount_used }.with_numeric_operations
end
def used
all.map(&:amount_used).with_numeric_operations
end
end
This looks like a "clever" but ridiculously over-complicated way to do something that can be simply and efficiently done in the database.
User.joins(:store_credits)
.select(
'users.*',
'SUM(store_credits.amount_used) AS amount_used',
'SUM(store_credits.amount) - amount_used AS unused',
)
.where(store_credits: { created_at: Time.current.beginning_of_day..Time.current.end_of_day })
.group(:id)
So I got introduced to using PORO instead of AR object for abstraction and size reduction.
But I have so many AR tables didn't make sense to put so much time to build a PORO class for each and every. Would take like a hour or two!! So instead I spent many hours thinking about how can I make this simpler.
And this is what I ended up making:
class BasePORO
def initialize(obj, immutable = true)
self.class::ATTRIBUTES.each do |attr|
instance_variable_set("##{attr}".to_sym, obj.attributes[attr.to_s])
instance_eval("undef #{attr}=") if immutable
end
end
end
class UserPORO < BasePORO
# or plug your own attributes
ATTRIBUTES = User.new.attributes.keys.map(&:to_sym).freeze
attr_accessor(*ATTRIBUTES)
end
But I can't somehow move the attr_accessor into the Base class or even ATTRIBUTES when not given explicitly. Not sure if its possible even.
Can I somehow move attr_accessor and default ATTRIBUTES into the main BasePORO class?
Any pointers or feedback is welcome.
As suggested in the comments, OpenStruct can do most of the heavy lifting for you. One thing to note is that if you don't freeze it, then after it's initialization you'll be able to add more attributes to it throughout its lifetime, e.g.:
struct = OpenStruct.new(name: "Joe", age: 20)
struct.email = "joe#example.com" # this works
p struct.email # => "joe#example.com"
(so essentially it works like a Hash with object-like interface)
This behavior may be undesired. And if you do freeze the struct, it won't allow any more attributes definition, but then you'd also lose the ability to override existing values (which I think you want to do in cases when someone sets immutable to false).
For the immutable flag to work as I understand you to expect it, I'd create a class that uses OpenStruct under its hood, for example like this:
class BasePORO
def initialize(obj, immutable = true)
#immutable = immutable
#data = OpenStruct.new(obj.attributes)
obj.attributes.keys.each do |attr|
self.class.define_method(attr.to_sym) do
#data.send(attr.to_sym)
end
self.class.define_method("#{attr}=".to_sym) do |new_value|
if #immutable
raise StandardError.new("#{self} is immutable")
else
#data.send("#{attr}=".to_sym, new_value)
end
end
end
end
end
class UserPORO < BasePORO
end
BTW, if you insisted on having a solution similar to the one shown in the question, then you could achieve this with something like that:
class BasePORO
def initialize(obj, immutable = true)
#immutable = immutable
attributes.each do |attr|
instance_variable_set("##{attr}".to_sym, obj.attributes[attr.to_s])
self.class.define_method(attr.to_sym) do
instance_variable_get("##{attr}".to_sym)
end
self.class.define_method("#{attr}=".to_sym) do |new_value|
if #immutable
raise StandardError.new("#{self} is immutable")
else
instance_variable_set("##{attr}".to_sym, new_value)
end
end
end
end
private
# default attributes
def attributes
[:id]
end
end
class UserPORO < BasePORO
private
# overriding default attributes from BasePORO
def attributes
User.new.attributes.keys.map(&:to_sym).freeze
end
end
So this is what actually ended up with:
class BaseStruct < OpenStruct
def initialize(model, immutable: true, only: [], includes: [])
if only.empty?
hash = model.attributes
else
hash = model.attributes.slice(*only.map!(&:to_s))
end
includes.each do |i|
relation = model.public_send(i)
if relation.respond_to?(:each)
hash[i.to_s] = relation.map{|r| OpenStruct.new(r.attributes).freeze}
else
hash[i.to_s] = OpenStruct.new(relation.attributes).freeze
end
end
super(hash)
self.freeze if immutable
end
end
Feel free to critique or suggest improvements.
How can I refactor this ruby code using the Open/Closed principle or Strategy pattern ?
I know that the main thought is 'software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification' but how can I use this in practice?
class PaymentService
def initialize(payment, payment_type)
#payment = payment
#payment_type = payment_type
end
def process
result = case payment_type
when 'first'
process_first
when 'second'
process_second
end
payment.save(result)
end
def process_first
'process_first'
end
def process_second
'process_second'
end
end
In this example, instead of passing a payment_type you can build an object with a class that processes a payment:
class FirstPayment
def process
'process_first'
end
end
class SecondPayment
def process
'process_second'
end
end
class PaymentService
def initialize(payment, payment_strategy)
#payment = payment
#payment_strategy = payment_strategy
end
def process
result = #payment_stategy.process
payment.save(result)
end
end
PaymentService.new(payment, FirstPayment.new)
As a result, PaymentService behaviour can be extended by passing a new strategy (for example, ThirdPayment), but the class doesn't need to be modified, if the logic of processing the first or the second payments is changed.
I am rather new to Rails, and would greatly appreciate any bit of help. I have created the following method:
def name_fix
name = self.split
mod_name = []
name.each do |n|
n.split("")
if n[0]
n.upcase
else
n.downcase
end
mod_name.push(n)
end
mod_name.join
end
I would like to use this method in my Controller as such:
def create
#patient = Patient.new(params[:patient])
#patient.name = params[:params][:name].name_fix
if #patient.save
redirect_to patients_path
else
render :new
end
end
How can I go about accomplishing this? Will this method reside within my Model or Controller? Previously, I've run into an undefined method error.
Note: I'm sure that there is a way to better write my code. I am grateful for help with that as well.
#app/models/patient.rb
class Patient < ActiveRecord::Base
protected
def name=(value)
mod_name = []
value.split.each do |n|
n.split("")
type = n[0] ? "up" : "down"
n.send("#{type}case")
mod_name.push(n)
end
#name = mod_name.join
end
end
#app/controllers/patients_controller.rb
class PatientsController < ApplicationController
def create
#patient = Patient.new patient_params
#patient.save ? redirect_to(patients_path) : render(:new)
end
private
def patient_params
params.require(:patient).permit(:name)
end
end
What you're doing is trying to override the setter method, which can be done using the above code. Much more efficient and out of the way.
I have created the following method
Since you're new, let me explain something else.
It is important to note where you're using this method.
You've currently put it in the model, which means you'll have to call it to manipulate some attribute / functionality of any object created with said model.
--
Models - in Rails - build the objects which populate your app. Ruby is an object orientated language, which means that every element of your program should revolve around data objects in some degree.
As you can see above, the method of building objects in your system is really about invoking classes. These classes contain methods which can be called, either at class level (IE invoking the class through the method), or at instance level (IE calling a method on an already invoked object).
This is where you get "class" methods (Model.method) and "instance" methods (#model.method) from:
#app/models/patient.rb
class Patient < ActiveRecord::Base
def explode
#this is an instance method
puts "Instance Explode"
end
def self.explode
#this is a class method
puts "Exploded"
end
end
Thus you can call the following:
#patient = Patient.find params[:id]
#patient.explode #-> "Instance explode"
Patient.explode #-> "Exploded"
--
This is important because it gives you a strict framework of where you should, and shouldn't use methods in your models.
It explains why you have controllers & helpers, and allows you to formulate the best way to structure your application as to get the most out of the least code.
For example...
Your use of #patient.name = params[:params][:name].name_fix is incorrect.
It's wrong because you're calling the instance method .name_fix on a piece of data totally unrelated to your model. If you wanted to use .name_fix in a general sense like this, you'd probably use a helper:
#app/helpers/patients_helper.rb
class PatientsHelper
def name_fix value
# stuff here
end
end
#app/controllers/patients_controller.rb
class PatientsController < ApplicationController
def create
#patient.name = name_fix params[:patient][:name]
end
end
Since you're using the method to populate the .name attribute of your model, it makes sense to override the name= setter. This will not only provide added functionality, but is much smoother and efficient than any other way.
Methods that are called directly are best put in the Controller (or in ApplicationController if you think more than one controller might want to use it).
These are methods like
# app/controllers/my_controller.rb
def foo(bar)
# do something here
end
def create
id = params[:id]
value = foo(id)
end
If you want a chained method that acts as a property method of whatever you're calling it on. Those are characteristic of how Models work - you have your main model and you call attributes or methods on the instance of that model.
# app/models/my_model.rb
def full_name
first_name + " " + last_name
end
# app/controller/my_controller.rb
def create
id = params[:id]
model = MyModel.find(id)
full_name = model.full_name
end
In your case, you want to call name_fix ON whatever is returned by params[:params][:name], which is (I'm guessing) a String.
You have two options
Modify the String class to define a method named name_fix. I highly recommend against this. It's call "monkeypatching" and shouldn't be done without good reason. Just letting you know you can do it in some cases.
Use a direct method in your controller or ApplicationController like the first example above.
#patient.name = name_fix(params[:params][:name])
Edit: As for your request about a better way to write your code... that's difficult to teach or convey in one answer. I'd say read some open source projects out there to see how people write Ruby and some common idioms used to clean up the code. To get you started, here's how I'd re-write your code
def create
#patient = Patient.new(params[:patient])
# 1. Be descriptive with your method names. `name_fix` is vague
# 2. Why is `:name` nested under another `[:params]` hash?
#patient.name = capitalize_name(params[:name])
if #patient.save
# 1. I think `patient_path` has to be singular
# 2. It needs a `Patient` object to know how to construct the URL
# e.g. `/patients/:id`
redirect_to patient_path(#patient)
else
render :new
end
end
def capitalize_name(full_name)
# Example: julio jones
#
# 1. `split` produces an array => ["julio", "jones"]
# 2. `map` applies a function (`capitalize`) to each element
# => ["Julio", "Jones"]
# 3. `join(" ")` rejoins it => "Julio Jones"
full_name.split.map(&:capitalize).join(" ")
end
Assuming your goal with the name_fix method is just to capitalize the first letter of each name, you could just pass name as an argument and store it as a private method on the Controller:
# app/controllers/patient_controller.rb
private
def name_fix(name)
name.split.map(&:capitalize).join(" ")
end
Then you could do
#patient.name = name_fix(params[:params][:name])
in the create method.
OR, you could store this method in the model:
# app/models/patient.rb
def self.name_fix(name)
name.split.map(&:capitalize).join(" ")
end
Then you could do this instead, in the controller:
#patient.name = Patient.name_fix(params[:params][:name])
I would also suggest renaming your name_fix method to something like capitalize_name.
update your create method as below
def create
#patient = Patient.new(params[:patient])
#patient.name = params[:params][:name]
#patient = #patient.name_fix
if #patient.save
redirect_to patients_path
else
render :new
end
end
It should work.
I have a situation where i need to call something like this :
class Office
attr_accessor :workers, :id
def initialize
#workers = []
end
def workers<<(worker)
type = worker.type
resp = Organiation::Worker.post("/office/#{#id}/workers.json", :worker => {:type => type})
end
end
this is where i need to call
office = Office.new()
new_worker = Worker.new()
office.workers << new_worker
how should i modify the above workers method in order to implement above code.
New answer for this (based on updated question):
class WorkersClient
attr_accessor :office_id
def <<(worker)
type = worker.type
resp = Organiation::Worker.post("/office/#{#office_id}/workers.json", :worker => {:type => type})
end
end
class Office
attr_accessor :workers, :id
def initialize
#workers = WorkersClient.new
#workers.office_id = #id
end
end
I'm assuming that the Worker class is defined somewhere, something like:
def Worker
attr_accessor :type
...
end
The WorkersClient class is just a proxy to handle the collection (like ActiveRecord 3 does with associations). You can develop it further to store a local cache of workers, and so on.
I would recommend looking at how Rails' ActiveResource is implemented, as it does something VERY similar.
try this office.build_worker
If those objects are actually ActiveRecord objects (which it sort of sounds like), you're probably looking at
office.workers << new_worker
Note the plural form.
If those objects are your own creations, you probably want Office#workers to return an Array'ish object, so something like
class Office
def workers
#workers ||= []
end
end
Add sanity checks and whatnot as you see fit.
There's not much to add to what's already been said, but one thing to think about is hiding the implementation of workers. Sure, it starts out with an array, but that may change. By implementing your own << method you can hide implementation details from the user.
class Office
attr_accessor :workers
def initialize
#workers = []
end
def <<(other)
self.workers << other
end
end
I tend to use getter/setters inside my classes as that's something I learned from Smalltalk books, but of course you could just do #workers << other.