Referencing class attributes in mixin - ruby

I'm fairly new to Ruby, and I have a code organisation problem.
I have a class, called Movie, which includes a module called IMDBMovieInfo.
class Movie
include IMDBMovieInfo
attr_accessor :name
attr_accessor :year
attr_accessor :movieID
end
IMDBMovieInfo has a method which takes the movieID and uses it to build an IMDB URL:
module IMDBMovieInfo
def imdb_url()
"http://www.imdb.com/title/tt#{self.movieID}/"
end
end
The problem here is I'm not sure I should be referencing something in the movie class, as IMDBMovieInfo doesn't know about that class, and shouldn't. I could add an argument of a movie ID, but then if you don't know about the Movie object, you'd be doing this, which doesn't make sense:
movie = Movie.new("Titanic", "1997", "0120338")
movie.imdb_url(movie.movieID)
What would be the correct way to organise this code?

You would only need to extract this code into a separate module if you were to use it in multiple classes. I can however reenact the wish to split up a large model into several small chunks.
You could for example establish a protocol that the including class must adhere to and leave it up to the including class where the id comes from. In the following example, the including class must implement the method imdb_id which is supposed to return the id that will be used for the url:
module IMDBMovieInfo
def imdb_id
raise NotImplementedError, 'including class needs to override imdb_id'
end
def imdb_url
"http://www.imdb.com/title/tt#{imdb_id}/"
end
end
class Movie
include IMDBMovieInfo
def imdb_id
self.movieID
end
attr_accessor :name
attr_accessor :year
attr_accessor :movieID
end
# In another classs
# (suppose IMDB lists computer games in the future)
class ComputerGame
include IMDBMovieInfo
def imdb_id
self.gameID
end
attr_accessor :name
attr_accessor :year
attr_accessor :gameID
end
I must however say that I find this whole extracting into mixins a bit clumsy. Another way would be to create a utility class that knows how to build an url but not where the id comes from:
class IMDBUtil
def initialize(imdb_id)
#imdb_id = imdb_id
end
def imdb_url
"http://www.imdb.com/title/tt#{#imdb_id}/"
end
end
class Movie
include IMDBMovieInfo
def imdb_url
IMDBUtil.new(self.movieId).imdb_url
end
attr_accessor :name
attr_accessor :year
attr_accessor :movieID
end
To wrap this up, here's a great blog post from CodeClimate on how to refactor fat Rails models.

In respect to your comment I would say, that the IMDBMovieInfo module shouldn't have a function that needs a movieID. You could just move it to your Movie class.

Related

What is the difference between an instance method used to rename an object and a setter method?

If I want to rename my jedi object below, why would I create an instance method named rename that uses the setter method name=? Why not just use the setter method `name=' directly?
Why do this:
class Skywalker
attr_accessor :name
def initialize(name)
#name = name
end
def rename(new_name)
self.name = new_name
end
end
jedi = Skywalker.new('Anakin')
puts jedi.name
jedi.rename('Luke')
puts jedi.name
When you could just do this:
class Skywalker
attr_accessor :name
def initialize(name)
#name = name
end
end
jedi = Skywalker.new('Anakin')
puts jedi.name
jedi.name = 'Luke'
puts jedi.name
Both code snippets above do the same thing, so I'm wondering if there is a situation where it would be useful to have the instance method rename in addition to the setter method name=. Because to me it looks like they are redundant.
#rename hides the implementation details. You expose a clean and explicit interface - an object can be renamed, but the caller doesn't have to care how it's done. I would recommend to use attr_reader :name instead of attr_accessor :name to avoid exposing the setter.
If you expose just #name= you let the caller to change object internals. It may cause the future changes harder (e.g. if you move name to a separate object).

How do I stub a Class object in Rspec?

I am trying hard to enforce encapsulation (but am probably not doing very well), and want to test the code in Rspec. The Customer class will take a class Object (as klass) when it is instantiated in a factory class. Through an as yet non-existent UI, the Customer will create an Order.
My current test is as follows. I just want to confirm that the order is the Order class.
describe 'Customer' do
let(:customer){Customer.new}
let(:customer_with_instantiation){Customer.new(:klass=>order, :name=>'Oscar Wilde', :number=>'0234567')}
let(:order){double :order, :name=>:order}
it 'klass object to be the order class when customer is instantiated with a klass attribute' do
expect(customer_with_instantiation.klass).to be_a(order)
end
end
Class code as follows:
class Customer
attr_accessor :name, :number, :klass
DEFAULT_CUSTOMER_ORDER = {:order_detail => [{ :dish=>"",
:item_count=>0 }],
:order_total_cost=>0 }
def initialize(options={})
#name=options.fetch(:name, "")
#number=options.fetch(:number, "")
#klass=options.fetch(:klass, Object)
#customer_order=DEFAULT_CUSTOMER_ORDER
end
def place_order(menu)
#requires user input
customer_order=klass.new({:order_detail => [{:dish => :pizza, :item_count => 3},
{:dish => :burger, :item_count => 3}],
:order_total_cost => 210})
klass.test_customer_order(customer_order, self)
end
end
class Order
attr_reader :order_detail, :order_total_cost
attr_accessor :total_check
def initialize(options={})
#order_detail=options.fetch(:order_detail, Object)
#order_total_cost=options.fetch(:order_total_cost, Object)
end
def self.test_customer_order(customer_order, customer, menu, assistant)
customer_order.total_check = 0
customer_order.order_detail.each do |order_item|
menu.dishes.each do |dish|
if order_item[:dish]==dish.name
customer_order.total_check += dish.price*order_item[:item_count]
end
end
end
assistant.take_order(customer_order, customer, customer_order.total_check)
end
end
Any help gratefully appreciated!
By using be_a, you're testing that klass is an instance of klass, which is probably not what you want.
It seems to me that when testing the initialize method and the getter for klass (which is what you're doing, in effect), you should only be interested in confirming that whatever you send into Customer.new can be read afterwards.
So maybe something like this:
class Foo
attr_reader :klass
def initialize(args)
#klass = args.fetch(:klass)
end
end
describe Foo do
describe "#initialize" do
let(:klass) { double }
let(:instance) { Foo.new(klass: klass)}
it "sets klass" do
expect(instance.klass).to eq(klass)
end
end
end
Some general points:
If you want to test whether the order is an instance of klass, you should probably rewrite your code to make that easier to test
klass isn't a very useful name in this case. It isn't clear why a Customer would need a klass.
You want to decouple the order from the customer, but the customer is clearly making some assumptions about the interface of the order. Did you really achieve anything?
I'd recommend not putting test methods in the classes themselves, but rather in the test files.
Using Object as a default in fetch is probably not what you want. To begin with, you probably want them to be instances of some class, not class objects.
Is it really the job of an instance of the Customer class to create orders? If the point is to make sure that any kind of abstract order can be instantiated based on user input, maybe a separate OrderCreator class would be more appropriate? This class could accept user data and an order class and the affected customer.

using attributes of different objects in different objects methods

what i need is basically to use variable from one file, in the method. let me explain
lets say we have
class Game
attr_accessor :number, :object
end
where number is just some number and object is object of some other class defined by me, lets name it Player class. now we make another file, which requires class Game, and which goes like this:
require './Game.rb'
require './Player.rb'
myGame = Game.new
myGame.number = 1
myGame.object = Player.new
and now the big moment. in method defined in Player class, i would like to use myGame.number attribute. eg like this
class Player
attr_accessor :some_var
def method
#some_var = myGame.number
end
end
How can i achieve this?
Your player should have a reference to the game is playing. For instance
class Game
attr_accessor :number
attr_reader :my_player
def my_player=(player)
player.my_game = self
#my_player = player
end
end
class Player
attr_accessor :some_var, :my_game
def method
#some_var = #my_game.number if #my_game
end
end
myGame = Game.new
myGame.number = 1
myGame.my_player = Player.new()
myGame.my_player.method
puts myGame.my_player.some_var
Alternatively to toch's answer, you can keep a reference to the game object when you set the player accessor. Instead of using automatic accessors you can use the get_ set_ accessor syntax to have custom code in the accessor, which would set the reference on the rvalue.

Extending all my Mongoid documents with an instance method

I want to extend all my Mongoid::Document's with an instance method. Instead of making a module and including it to each Mongoid::Document I want to extend, there should be another way.
For example, for the ruby class Array I would simply reopen this class and add the methods I want:
class Array
def my_new_method
#....
end
end
But how do I that for Mongoid::Document?
I would do it this way
module Mongoid::Document
def self.validate
...
end
end
However, I would refrain from opening an external module (even thou this seems) to be a common thing to do in the ruby community. What is to say against to include your own module explicitly?
If you're going to open a class as you did with Array, better to do it like this:
module MyNewMethodable
def my_new_method( *args )
fail ArgumentError, "not the right number of arguments"
#....
rescue => error
if MyNewMethodable::Error
puts "because then users of your module will know where to look for the fault"
else
raise error
end
end
class Error < StandardError; end
class ArgumentError < Error; end
end
class Array
include MyNewMethodable
end
To do this for Mongoid::Document
class Mongoid::Document
include MyNewMethodable
end
But, it says here
Documents are the core objects in Mongoid and any object that is to be persisted to the database must include Mongoid::Document.
So it's already being included into classes that you've defined. Therefore I would suggest you include your module into your classes, not into Mongoid::Document. e.g.
class MyClass
include Mongoid::Document
include MyNewMethodable
end

Rails Model without a table

I want to create a select list for lets say colors, but dont want to create a table for the colors. I have seen it anywhere, but can't find it on google.
My question is: How can I put the colors in a model without a database table?
Or is there a better rails way for doing that?
I have seen someone putting an array or a hash directly in the model, but now I couldn't find it.
class Model
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
attr_accessor :whatever
validates :whatever, :presence => true
def initialize(attributes = {})
attributes.each do |name, value|
send("#{name}=", value)
end
end
def persisted?
false
end
end
attr_accessor will create your attributes and you will create the object with initialize() and set attributes.
The method persisted will tell there is no link with the database. You can find examples like this one:
http://railscasts.com/episodes/219-active-model?language=en&view=asciicast
Which will explain you the logic.
The answers are fine for 2013 but since Rails 4 all the database independent features of ActiveRecord are extracted into ActiveModel. Also, there's an awesome official guide for it.
You can include as many of the modules as you want, or as little.
As an example, you just need to include ActiveModel::Model and you can forgo such an initialize method:
def initialize(attributes = {})
attributes.each do |name, value|
send("#{name}=", value)
end
end
Just use:
attr_accessor :name, :age
The easiest answer is simply to not subclass from ActiveRecord::Base. Then you can just write your object code.
What worked for me in Rails 6:
class MyClass
include ActiveModel::Model
attr_accessor :my_property
end
If the reason you need a model without an associated table is to create an abstract class real models inherit from - ActiveRecord supports that:
class ModelBase < ActiveRecord::Base
self.abstract_class = true
end
If you want to have a select list (which does not evolve) you can define a method in your ApplicationHelper that returns a list, for example:
def my_color_list
[
"red",
"green",
"blue"
]
end

Resources