In Rails we can call "Person.find_by_attribute" where 'attribute' is dynamically placed by attributes in the 'Person' migration. I want to replicate this logic n my Person class without rails and call 'find_by_attribute' for the attributes. What I have so far:
class Person
def attributes
[
{ id: 1, nice_attribute: "something" },
{ id: 2, nice_attribute: "another thing" },
]
end
def find_by_id(id)
attributes.select { |d| d[:id] == id }
end
But defining a find_by like this could bloat the class quickly. How can I achieve dynamic def setting?
I'm reasonably sure this is not how Rails does this, but here is some implementation that might assist you:
What you have defined there is a Singleton class and you should be aware of how to dynamically define attribute records in such a class before committing to that design. It might be a good choice, but you should be feeding that class an access point to search through records defined elsewhere. Regardless, the below will get you started:
class Person
##attributes = { id: Integer, nice_attribute: String }
# getter method
def self.attribute_types
##attributes
end
attribute_types.keys.each do |attr|
define_singleton_method :"find_by_#{attr}" do |value|
# logic
end
end
end
Then you will be able to call Person.find_by_id or Person.find_by_nice_attribute
When you implement it may be better for you to have a PersonRecord module that collects records from a database and the Person class might be an instance like so:
class Person
def initialise(opts ={})
define_find_by_methods
end
def attribute_keys
# stub method, but should be collecting this information dynamically
[ :id, :nice_attribute ]
end
def define_find_by_methods
attribute_keys.each do |attr|
# no longer using define_singleton_method here and define_method is a class method you will have to send it to self.class
self.class.send(:define_method, :"find_by_#{attr}") do |val|
# logic
end
end
end
end
Related
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.
I have a module FDParser that reads a csv file and returns a nice array of hashes that each look like this:
{
:name_of_investment => "Zenith Birla",
:type => "half-yearly interest",
:folio_no => "52357",
:principal_amount => "150000",
:date_of_commencement => "14/05/2010",
:period => "3 years",
:rate_of_interest => "11.25"
}
Now I have an Investment class that accepts the above hash as input and transforms each attribute according to what I need.
class Investment
attr_reader :name_of_investment, :type, :folio_no,
:principal_amount, :date_of_commencement,
:period, :rate_of_interest
def initialize(hash_data)
#name = hash_data[:name_of_investment]
#type = hash_data[:type]
#folio_no = hash_data[:folio_no]
#initial_deposit = hash_data[:principal_amount]
#started_on =hash_data[:date_of_commencement]
#term = hash_data[:period]
#rate_of_interest = hash_data[:rate_of_interest]
end
def type
#-- custom transformation here
end
end
I also have a Porfolio class with which I wish to manage a collection of investment objects. Here is what the Portfolio class looks like:
class Portfolio
include Enumerable
attr_reader :investments
def initialize(investments)
#investments = investments
end
def each &block
#investments.each do |investment|
if block_given?
block.call investment
else
yield investment
end
end
end
end
Now what I want is to loop over the investment_data yielded by the module and dynamically create instances of the investment class and then send those instances as input to the Portfolio class.
So far I tried:
FDParser.investment_data.each_with_index do |data, index|
"inv#{index+1}" = Investment.new(data)
end
But obviously this doesn't work because I get a string instead of an object instance. What is the right way to send a collection of instances to a enumerable collection class that can then manage them?
I'm not sure what "send as input to the Portfolio class" means; classes themselves don't accept "input". But if you're just trying to add Investment objects to the #investments instance variable inside an instance of Portfolio, try this:
portfolio = Portfolio.new([])
FDParser.investment_data.each do |data|
portfolio.investments << Investment.new(data)
end
Note that the array literal [] and the return value of portfolio.investments point to the self-same Array object here. This means you could equivalently do this, which arguably is a little clearer:
investments = []
FDParser.investment_data.each do |data|
investments << Investment.new(data)
end
Portfolio.new(investments)
And if you want to play a little code golf, it shrinks further if you use map.
investments = FDParser.investment_data.map {|data| Investment.new(data) }
Portfolio.new(investments)
I think this is a little harder to read than the previous option, though.
I'm using Ruby's metaprogramming methods creating a bunch of methods within a class. Within the class OmekaItem there are a bunch of methods of this form dc_title and dc_subject, and there are a bunch of methods of this form itm_field1 and itm_field2. I'd like to group those methods better. Ideally, given an instance of the class named item, I'd like call the methods this way:
item.dublin_core.title
item.item_type_metadata.field
and so on. Is there a way to do this?
This question has the code I'm working with.
Would something like the following work for you?
class OmekaItem
class DublinCore
def initialize(omeka_item)
#omeka_item = omeka_item
end
def title
#omeka_item.dc_title
end
def subject
#omeka_item.dc_subject
end
end
class ItemTypeMetadata
def initialize(omeka_item)
#omeka_item = omeka_item
end
def field1
#omeka_item.itm_field1
end
def field2
#omeka_item.itm_field2
end
end
def dublin_core
#dublin_core ||= DublinCore.new(self)
end
def item_type_metadata
#item_type_metadata ||= ItemTypeMetadata.new(self)
end
end
The methods on DublinCore and ItemTypeMetadata could be dynamically generated using define_method as appropriate.
I want something like the following but would like it to be reusable for different classes.
How do I refactor this code, so with minimal effort it can be included in a class and that class will automatically be collecting instances whenever new is called?
I've tried all sorts of things like overriding new or initialize but just can't get the magic to happen.
class Person
##people_instances = []
def initialize
##people_instances << self
end
def self.instances
##people_instances
end
end
People.new
People.new
Poople.instances
=> [#<Person:0x000001071a7e28>, #<Person:0x000001071a3828>]
After some feedback below, I don't think the answer is to put the instances in a class variable as it will stay in memory forever. Rails cache is also not so appropriate as I don't need the instances to persist.
The following code uses class instance variables instead of class variables.
http://www.dzone.com/snippets/class-variables-vs-class
class Employee
class << self; attr_accessor :instances; end
def store
self.class.instances ||= []
self.class.instances << self
end
def initialize name
#name = name
end
end
class Overhead < Employee; end
class Programmer < Employee; end
Overhead.new('Martin').store
Overhead.new('Roy').store
Programmer.new('Erik').store
puts Overhead.instances.size # => 2
puts Programmer.instances.size # => 1
Will these instance variables be unique to every rails request or will they persist?
UPDATED ANSWER
If you want to keep it available during the request alone, none of the previous answers can do it. The solution for keeping it available only during the request-response cycle is to use a thread-local that is assigned in a controller method, example:
class YourController < ApplicationController
around_filter :cache_objects
protected
def cache_objects
Thread.current[:my_objects] = ['my-object', 'my-other-object']
yield
ensure
Thread.current[:my_objects]
end
end
Then, at the code that needs it, you just do Thread.current[:my_objects] and do whatever you would like to do with them. You need to use an around_filter because your web framework or server structure could try to reuse threads and the only real solution is to clean them up once the request is done to avoid memory leaks.
OLD ANSWER
Not sure what you're trying to do, but you can easily pick every single instance of a class using ObjectSpace:
ObjectSpace.each_object(String) { |s| puts(s) }
If what you need is as a database cache just use the Rails cache, load these objects once and then keep them in the cache. When using the Rails cache all you need to do is send your objects to the cache:
Rails.cache.write( "my_cached_objects", [ 'first-object', 'second-object' ] )
And then get them somewhere else:
Rails.cache.fetch("my_cached_objects") do
# generate your objects here if there was a cache miss
[ 'first-object', 'second-object' ]
end
As you can see, you don't even have to call cache.write, you can just use fetch and whenever there is a cache miss the block given will be called and your objects will be created.
You can read more about rails caching here and you can see all supported methods of the ActiveSupport::Cache::Store here.
Another method without using ObjectSpace but still with an ugly solution, now using alias_method:
module Counter
def self.included( base )
base.extend(ClassMethods)
base.class_eval do
alias_method :initialize_without_counter, :initialize
alias_method :initialize, :initialize_with_counter
end
end
def count_class_variable_name
:"###{self.class.name.downcase}_instances"
end
def initialize_with_counter( *args )
unless self.class.class_variable_defined?(count_class_variable_name)
self.class.class_variable_set(count_class_variable_name, [])
end
self.class.class_variable_get(count_class_variable_name) << self
initialize_without_counter(*args)
end
module ClassMethods
def all_instances
class_variable_get(:"###{name.downcase}_instances")
end
end
end
class Person
def initialize
puts 'new person'
end
include Counter
end
p1 = Person.new
p2 = Person.new
p3 = Person.new
puts Person.all_instances.size
lib/keeper.rb
def initialize
instance_eval "###{self.class.to_s.downcase}_instances ||= []"
instance_eval "###{self.class.to_s.downcase}_instances << self"
end
def self.instances
return class_eval "###{self.to_s.downcase}_instances"
end
person.rb
class Person
eval File.open('./lib/keeper.rb','rb').read
end
Then this works:
Person.new
Person.new
Person.instances
I saw a RubyGem with the following use:
f = Foo.new("joe")
f.say.hello #=> "Hello joe"
In my Gem, I'm trying to have the same syntax. I have many classes within the Module Dance, but once I create a new instance of Dance::Client, I can't access the other Classes. For example:
d = Dance::Client.new("key")
d::Genres.all # => errors out
The results I would like is:
d = Dance::Client.new("key")
d.genres.all
There's probably dozens of ways you could do this, so here's a few examples:
Client instance method returns Genre class
module Dance
class Client
def genres
Genre
end
end
class Genre
def self.all
# return all genres
end
end
end
Client instance method returns Genre collection class
module Dance
class Client
def genres
GenreCollection.new
end
end
class GenreCollection
def all
Genre.all
end
end
class Genre
def self.all
# return all genres
end
end
end
In the example you show, "say" is an instance method, returning something with a "hello" method.
Similarly, "genres" would be an instance property of a Dance module's Client instance.
If you're just trying to instantiate a client class you'd use Module:: Class notation, whereas your error example attempts to do that with an instance.