Ruby instance variable access - ruby

Just curious what the best practice is for accessing an instance variable from within a class assuming attr_accessor is set.
class Test
attr_accessor :user
def initializer(user)
#user = user
end
def foo
#user
end
end
or
class Test
attr_accessor :user
def initializer(user)
#user = user
end
def foo
self.user
end
end
So by instance variable (#user) or getter method (Test#user)?

Getter method, because it's easier to refactor. Say you want to update a time stamp at the point of access.
class Test
def user
#user.last_read = Time.now
#user
end
end
And all your references to user are updated with the new logic. Not so easy if your references are to #user.

Related

Attr_accessor for Subclass inside Parent Class

I'm trying to figure out a way to dynamically generate subclasses based on a parent class. In my specific case I'd want to have attr_accessor for every instance variable, initialized in my Parent class and inherited on the SubClasses.
My classes are three different models representing three different tables in a DB.
"Record" is my parent class where I want to store and write all of my code.
"Post" and "User" are the Subclasses inheriting.
My code
class Record
attr_reader :id
# attr_accessor
def initialize(**params)
#id = params[:id]
instance_variable_set("##{params.keys[0]}", params.values[0])
instance_variable_set("##{params.keys[1]}", params.values[1])
instance_variable_set(:#votes, params["votes"] || 0) if instance_of?(Post)
# p self.title
end
Want I want to achieve is setting attr_accessor as for example in my Subclass "Post" I want to call
post = Post.new(title: "New post", url: "some url")
puts post.title
I can access the title instance variable without raising a NoMethodError
Could someone guide me, or give me some hint?
Thanks
You're going about it backwards. A parent class should not have to know about or implement specific logic for its subclasses.
class Record
attr_reader :id
def initialize(**attributes)
attributes.each do |key, value|
send("#{key}=", value)
end
end
end
class Post < Record
attr_accessor :title
attr_accessor :votes
end
irb(main):066:0> Post.new(id: 1, votes: 10, title: "Hello World").title
=> "Hello World"
attr_accessor is just a metaprogramming convenience for defining methods so your accessor methods are inherited anyways. But if you're writing something like an Object Relational Manager you'll want to define your own macro method for defining attributes that lets you keep track of the attributes of a class:
module Attributes
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
#attributes ||= {}
end
end
# assigns the passed attributes to the instance
def initialize(**attributes)
attributes.each do |key, value|
send "#{key}=", value
end
end
# gets all the attributes of an instance
def attributes
self.class.attributes.keys.each_with_object(Hash.new) do |key, hash|
hash[key] = send(key)
end
end
module ClassMethods
# Inherits the attributes of the parent class
def inherited(subclass)
attributes.tap do |parent_attributes|
subclass.class_eval do
#attributes ||= {}.merge(parent_attributes)
end
end
end
# defines an attribute that is inherited
def attribute(name, type = nil, **kwargs)
#attributes[name] = { type: type }.merge(kwargs)
attr_accessor name
end
def attributes
#attributes
end
end
end
class Record
include Attributes
attribute :id, Integer
end
class Post < Record
attribute :title, String
attribute :votes, Integer
end
irb(main):101:0> Post.new(votes: 10, title: "Hello World").attributes
=> {:id=>nil, :title=>"Hello World", :votes=>10}
This stores the attribute definitions in a class instance variable which lets you attach "metadata" which opens up for features that you will want later such as typecasting, serialization and dirty tracking.

Ruby object initialization using instance_eval

Consider the following class:
class Person
attr_accessor :first_name
def initialize(&block)
instance_eval(&block) if block_given?
end
end
When I create an instance of Person as follows:
person = Person.new do
first_name = "Adam"
end
I expected the following:
puts person.first_name
to output "Adam". Instead, it outputs only a blank line: the first_name attribute has ended up with a value of nil.
When I create a person likes this, though:
person = Person.new do
#first_name = "Adam"
end
The first_name attribute is set to the expected value.
The problem is that I want to use the attr_accessor in the initialization block, and not the attributes directly. Can this be done?
Ruby setters cannot be called without an explicit receiver since local variables take a precedence over method calls.
You don’t need to experiment with such an overcomplicated example, the below won’t work as well:
class Person
attr_accessor :name
def set_name(new_name)
name = new_name
end
end
only this will:
class Person
attr_accessor :name
def set_name(new_name)
# name = new_name does not call `#name=`
self.name = new_name
end
end
For your example, you must explicitly call the method on a receiver:
person = Person.new do
self.first_name = "Adam"
end
If the code is run with warnings enabled (that is ruby -w yourprogram.rb)
it responds with : "warning: assigned but unused variable - first_name", with a line-number pointing to first_name = "Adam". So Ruby interprets first_name as a variable, not as a method. As others have said, use an explicit reciever: self.first_name.
Try this:
person = Person.new do |obj|
obj.first_name = "Adam"
end
puts person.first_name
I want to use the attr_accessor in the initialization block, and not the attributes directly
instance_eval undermines encapsulation. It gives the block access to instance variables and private methods.
Consider passing the person instance into the block instead:
class Person
attr_accessor :first_name
def initialize
yield(self) if block_given?
end
end
Usage:
adam = Person.new do |p|
p.first_name = 'Adam'
end
#=> #<Person:0x00007fb46d093bb0 #first_name="Adam">

How to call an instance method on an instance of a class from a different class

So I have the following setup
class foo
:attr_accessor :textBoxInFoo
#textBoxInFoo
def appendText
//appends text to textBoxInFoo
end
def getLastPartOfText
//gets the last line of text in textBoxInFoo
end
end
class bar
def UseFoo
//Declares instance of textBoxInFoo
end
end
class snafu
def runInBackground
//Needs to read and write from instance of textBoxInFoo in bar class
end
end
What I'm failing to grasp is how to do what I need to do in runInBackground to get it to read and write to textBoxInFoo, I've been reading through various explanations of instance and class methods, and none of them have really clicked with me yet, so I'm wondering if anyone knows what I'm messing up.
This is a small example of how you can create a user object and send it to student object as an argument. run() method calls user's run method.
class User
attr_accessor :name
def initialize(name) # it is similar to constructor
#name = name
end
#run method
def run
puts "I am running"
end
#getter for name
def get_name
#name
end
#setter for name
def set_name=(name)
#name = name
end
end
class Student
attr_accessor :obj
def initialize(obj)
#obj = obj
end
def run
obj.run
puts "inside of Student"
end
end
user = User.new("John")
stud = Student.new(user)
stud.run # shows I am running
# inside of student

Instance variables and attr_accessor

When I define #foo=3 in the initialize method, I expect to access my variable like this.
class Object
def initialize(v)
#foo = v
end
Object.new.foo
That doesn't happen though. I need to write attr_accessor :foo. Why do I need to do this even though # already does that for me?
One instance variable in Ruby is not public by default. And access should be granted based on accessors.
For read only attr_reader
For write only attr_writer
For read write attr_accessor
It is not accessible by default
# does not automatically do that for you. That's why. attr_accessor creates getters and setters for your instance variables ("#-variables").
The instance variables are private. You need accessors and mutators to access them. A common accessor/mutator pair looks like:
def foo
#foo
end
def foo=(value)
#foo=value
end
This creates an abstraction which you can now use as:
Classname.new.foo
Classname.new.foo="OOP"
Since this is such a common need and also reults in lot of boilerplate(read: unnecessary) code, ruby provides a dynamic method which literally defines these two methods for you.
attr_accessor :foo
If you want only one of accessor or mutator method, then use the corresponding from following:
attr_reader :foo
attr_writer :foo
This will save a lot of copy/paste. I hope I was clear.
All Ruby attributes are "private" and are invisible outside the class's methods. You need accessor methods to read and write an attribute. So, in your example, you need
class MyClass
def initialize(v)
#foo = v
end
def foo
#foo
end
def foo=(v)
#foo = v
end
end
Then MyClass.new(4).foo will work, and return 4.
You can also add the accessor methods using the convenience methods
attr_reader :foo
attr_writer :foo
or
attr_accessor :foo
An instance variable starts with an # character. All instance variables are private, which means you can't read them and you can't change their value. So what to do?
class Dog
def initialize(name)
#name = name
end
def name #getter
#name
end
def name=(str) #setter
#name = str
end
end
Well, that gets to be a pain to type out, so ruby provides a shortcut:
class Dog
attr_accessor :name
def initialize(name)
#name = name
end
end

How can a class method (inside a module) update an instance variable?

How can a class method (inside a module) update an instance variable? Consider the code bellow:
module Test
def self.included(klass)
klass.extend ClassMethods
end
module ClassMethods
def update_instance_variable
#temp = "It won't work, bc we are calling this on the class, not on the instance."
puts "How can I update the instance variable from here??"
end
end
end
class MyClass
include Test
attr_accessor :temp
update_instance_variable
end
m = MyClass.new # => How can I update the instance variable from here??
puts m.temp # => nil
You'd have to pass your object instance to the class method as a parameter, and then return the updated object from the method.
That does nto quite make sense.
You use the initialize method to set default values.
class MyClass
attr_accessor :temp
def initialize
#temp = "initial value"
end
end
The initialize method is automatically run for you when you create a new object.
When your class declaration is run, there are no, and cannot be any, instances of the class yet.
If you want to be able to change the default values later you can do something like this:
class MyClass
attr_accessor :temp
##default_temp = "initial value"
def initialize
#temp = ##default_temp
end
def self.update_temp_default value
##default_temp = value
end
end
a = MyClass.new
puts a.temp
MyClass.update_temp_default "hej"
b = MyClass.new
puts b.temp
prints
initial value
hej
If you also want that to change already created instances' variables you need additional magic. Please explain exactly what you wish to accomplish. You are probably doing it wrong :)

Resources