Ruby object initialization: params, hash, vanilla? - ruby

I ran into some confusion over when/why or if it is just a matter of preference when initializing a method with a hash type structure.
class Person1
attr_reader :name, :age
def initialize(params)
#name = params[:name]
#age = params[:age]
end
end
me = Person1.new(name: 'John Doe', age: 27)
puts me.name
puts me.age
#----------------------------------------------
class Person2
attr_reader :name, :age
def initialize(name, age)
#name = name
#age = age
end
end
me = Person2.new('John Doe', 27)
puts me.name
puts me.age
#----------------------------------------------
class Person3
attr_reader :person
def initialize(name, age)
#person = { name: name,
age: age }
end
end
me = Person3.new('John Doe', 27)
puts me.person[:name]
puts me.person[:age]
If it is a matter of preference I like just passing the hash but I could see this being an issue if you need different attr reader, writer within the hash itself. Is there a rule of thumb? I see a lot of rails articles using params.

Your third way will most likely never appear in the wild - you are already constructing a person object, why does it have an accessor for person? The attributes should be on the class itself. The difference between 1 and 2 is mostly preference, but the second one can be advantageous when you regulary want to set only specific attributes.
Concering rails' usage of params: params is the hash that contains the request parameters of the particular request you are handling. As you deal with requests a lot in rails, it will appear in every controller and can also appear in some views.

Related

Ruby - initialize inheritance, super with only certain arguments?

I've been playing around with Ruby as of late and I can't seem to find the answer to my question.
I have a class and a subclass. Class has some initialize method, and subclass has its own initialize method that is supposed to inherit some (but not all) variables from it and additionally add its own variables to the subclass objects.
My Person has #name, #age and #occupation.
My Viking is supposed to have a #name and #age which it inherits from Person, and additionally a #weapon which Person doesn't have. A Viking obviously doesn't need any #occupation, and shouldn't have one.
# doesn't work
class Person
def initialize(name, age, occupation)
#name = name
#age = age
#occupation = occupation
end
end
class Viking < Person
def initialize(name, age, weapon)
super(name, age) # this seems to cause error
#weapon = weapon
end
end
eric = Viking.new("Eric", 24, 'broadsword')
# ArgError: wrong number of arguments (2 for 3)
You can make it work in the following ways, but neither solution appeals to me
class Person
def initialize(name, age, occupation = 'bug hunter')
#name = name
#age = age
#occupation = occupation
end
end
class Viking < Person
def initialize(name, age, weapon)
super(name, age)
#weapon = weapon
end
end
eric = Viking.new("Eric", 24, 'broadsword')
# Eric now has an additional #occupation var from superclass initialize
class Person
def initialize(name, age, occupation)
#name = name
#age = age
#occupation = occupation
end
end
class Viking < Person
def initialize(name, age, occupation, weapon)
super(name, age, occupation)
#weapon = weapon
end
end
eric = Viking.new("Eric", 24, 'pillager', 'broadsword')
# eric is now a pillager, but I don't want a Viking to have any #occupation
The question is either
is it by design and I want to commit some Cardinal Sin against OOP principles?
how do I get it to work the way I want to (preferably without any crazy complicated metaprogramming techniques etc)?
How super handles arguments
Regarding argument handling, the super keyword can behave in three ways:
When called with no arguments, super automatically passes any arguments received by the method from which it's called (at the subclass) to the corresponding method in the superclass.
class A
def some_method(*args)
puts "Received arguments: #{args}"
end
end
class B < A
def some_method(*args)
super
end
end
b = B.new
b.some_method("foo", "bar") # Output: Received arguments: ["foo", "bar"]
If called with empty parentheses (empty argument list), no arguments are passed to the corresponding method in the superclass, regardless of whether the method from which super was called (on the subclass) has received any arguments.
class A
def some_method(*args)
puts "Received arguments: #{args}"
end
end
class B < A
def some_method(*args)
super() # Notice the empty parentheses here
end
end
b = B.new
b.some_method("foo", "bar") # Output: Received arguments: [ ]
When called with an explicit argument list, it sends those arguments to the corresponding method in the superclass, regardless of whether the method from which super was called (on the subclass) has received any arguments.
class A
def some_method(*args)
puts "Received arguments: #{args}"
end
end
class B < A
def some_method(*args)
super("baz", "qux") # Notice that specific arguments were passed here
end
end
b = B.new
b.some_method("foo", "bar") # Output: Received arguments: ["baz", "qux"]
Yes, you are committing a Cardinal Sin (obviously, you are aware of it, since you are asking about it). :)
You are breaking Liskov substitution principle (and probably some other named or unnamed rules).
You should probably extract another class as a common superclass, which does not contain occupation. That will make everything much clearer and cleaner.
You can avoid setting a default value to your occupation parameter in Person Class by simply passing the nil argument for occupation in super(). This allows
you to call Viking.new with your 3 arguments (name, age, weapon) without
having to take extra consideration for occupation.
class Person
def initialize(name, age, occupation)
#name = name
#age = age
#occupation = occupation
end
end
class Viking < Person
def initialize(name, age, weapon)
super(name, age, nil)
#weapon = weapon
end
end
eric = Viking.new("Eric", 24, 'broadsword')
p eric
output Viking:0x00007f8e0a119f78 #name="Eric", #age=24, #occupation=nil, #weapon="broadsword"
Really this is just 2 ways - auto include all attributes (just the word super) and super where you pick which arguments get passed up. Doing super() is picking no arguments to hand to the parent class.
Edit: This was meant as a comment on the above comment but they don't allow comments when someone is new... oh well.

Access variables in methods that are in a class, outside of that class

I have the following code that creates a viking game character and gives them random stats such as health, age and strength.
class Viking
def initialize(name, health, age, strength)
#name = name
#health = health
#age = age
#strength = strength
end
def self.create_warrior(name)
age = rand * 20 + 15
health = [age * 5, 120].min
strength = [age/2, 10].min
Viking.new(name, health, age, strength)
end
end
brad = Viking.create_warrior("Brad")
puts "New Warrior Created!"
The create_warrior function returns all those values, but how do I access them so I could see the stats.
For example this doesn't work but I would like to see the age or health of the new Viking brad (i.e brad.age even though that wouldn't work because it's not a method).
So how do I access those variables (without making them global).
Use attr_accessor :name, :health, :age, :strength if you would like the variables to be both readable and writable or attr_reader :name, :health, :age, :strength if you would like them to be read only.
After this you can access with brad.varname e.g. brad.name etc...
[Documentation]
Use attr_reader :age, so you can simply use brad.age, same goes for the other variables
If you really don't want them global, try instance_variable_get method http://apidock.com/ruby/Object/instance_variable_get
puts brad.instance_variable_get(:#age)
Use the attr_reader method, it creates a attribute method to point to the variable in the initialize method which is private. Its read-only
You can use attr_writer to write only
And attr_accessor to both read and write
class Viking
attr_reader :age, :name, :health, :strength
def initialize(name, health, age, strength)
#name = name
#health = health
#age = age
#strength = strength
end
def self.create_warrior(name)
age = rand * 20 + 15
health = [age * 5, 120].min
strength = [age/2, 10].min
Viking.new(name, health, age, strength)
end
end
brad = Viking.create_warrior("Brad")
puts "New Warrior Created!"

What is the most elegant way to get the value in an array, minimizing a certain class attribute?

Say I have the following class:
class Person
def initialize(name, age)
#name = name
#age = age
end
def get_age
return #age
end
end
And I have an array of Person objects. Is there a concise, Ruby-like way to get the person with the minimum (or maximum) age? What about sorting them by it?
This will do:
people_array.min_by(&:get_age)
people_array.max_by(&:get_age)
people_array.sort_by(&:get_age)
Your question has been answered properly but, since you are interested in doing things in a Ruby-like way,
I am going to show you a better Ruby-like way to define your Person class.
If do you do not have behaviour (methods, etc.) in your class, the simplest way is using a Struct:
Person = Struct.new(:name, :age)
# example of use
person = Person.new("My name", 21)
Otherwise create a custom class like this, using attr_reader and a hash of arguments:
class Person
attr_reader :name, :age
def initialize(args = {})
#name = args[:name]
#age = args[:age]
end
end
# example of use
person = Person.new(:name => "My name", :age => 21)
Assuming you got an array named array having 5 different instances of Person.
To get the older person
for person in array
olderperson = person if maxperson.get_age < person.get_age
end
And for sorting them
array.sort! { |a,b| a.get_age <=> b.get_age }
Something like below:
youngest_person = people.select { |p| p.get_age == people.map(&:get_age).min }

class methosd (ruby)

Can you tell me why this isn't working?
The code below outputs student1 & student2 properly, but I cannot get the class method to work on student3.
All I am trying to do is assign a class method .create_with_species , with the species attribute = to "Human".
However, when I run the program, I get an error that "local variable or method sex is undefined". I'm a newbie and I can't figure out what I've done wrong!
It was my understanding that since I had identified "sex" in the Initialize method I should be able to use it within a class method such as create_with_species. I tried explicitly defining sex in the class method, as student.sex = sex, but still got the same error.
class Students
attr_accessor :sex, :age, :species
def self.create_with_species(species)
student = Students.new(sex,age)
student.species = species
return student
end
def initialize(sex, age)
#sex = sex
#age = age
puts "----A new student has been added----"
end
end
student1 = Students.new("Male", "21")
puts student1.sex
puts student1.age
puts
student2 = Students.new("Female", "19")
puts student2.sex
puts student2.age
student3 = Students.create_with_species("human")
sex and age are not defined. Do you mean to use a class variable? Using #age or #sex will result in nils, as no class attributes are set and the instance is not yet created, hence no instance variables set either. You don't need to state Students.new in create_with_species. new will suffice because you are scoped in Students. I think you would be better off passing a hash object to initialize, and just set the species variable there. It will be nil if not assigned explicitly, or you can have a default value.
Try this:
class Students
attr_accessor :sex, :age, :species
def initialize(options)
#sex = options[:sex]
#age = options[:age]
#species = options[:species]
puts "----A new student has been added----"
end
end
student1 = Students.new(:sex => "Male", :age => "21")
puts student1.sex
puts student1.age
puts student1.species
puts
student2 = Students.new(:sex => "Female", :age => "19")
puts student2.sex
puts student2.age
student3 = Students.new(
:sex => "Hermi", :age => "2", :species => "alien"
)
#sex and #age are instance attributes. You can't use them (what would they mean?) inside a class method. You've written just sex and age inside create_with_species, but again: what are these supposed to refer to? (In an instance method they'd be calls to the accessors, but at this point there's no instance to call the accessors on.)
Consider: when you say
student3 = Students.create_with_species("human")
what sex and age should this new student object get? You haven't specified any.
As far as the syntax goes, you have to use #sex and #age instead of sex and age. Otherwise they won't be interpreted as instance attributes.
But you do realize that the logic of the code is broken? I understand you want to use the static factory method pattern but this is not how you should do it since #sexand #age are nil when you call that method and those variables are instance attributes not class attributes.

Creating a class dynamically

I'm trying to create a new class, without knowing the name of the class until it's supposed to be created.
Something like this;
variable = "ValidClassName"
class variable
end
Test = ValidClassName.new
If possible, i'd also appreciate som hints on how to dynamically add attributes (and methods) to this new class.
I'll be retreiving 'settings' for the class, and they will look something like this:
title :Person
attribute :name, String
attribute :age, Fixnum
But should not be designed to accept only that explicit file, the attributes might differ in number end type.
Which in the end will generate a class that should look something like:
class Person
def initialize(name, age)
#name_out = name
#age_out = age
end
end
Help?
A class gains its name when it is assigned to a constant. So It's easy to do in a generic fashion with const_set.
For example, let's say you want to use Struct to build a class with some attributes, you can:
name = "Person"
attributes = [:name, :age]
klass = Object.const_set name, Struct.new(*attributes)
# Now use klass or Person or const_get(name) to refer to your class:
Person.new("John Doe", 42) # => #<struct Person name="John Doe", age=42>
To inherit from another class, replace the Struct.new by Class.new(MyBaseClass), say:
class MyBaseClass; end
klass = Class.new(MyBaseClass) do
ATTRIBUTES = attributes
attr_accessor *ATTRIBUTES
def initialize(*args)
raise ArgumentError, "Too many arguments" if args.size > ATTRIBUTES.size
ATTRIBUTES.zip(args) do |attr, val|
send "#{attr}=", val
end
end
end
Object.const_set name, klass
Person.new("John Doe", 42) # => #<Person:0x007f934a975830 #name="John Doe", #age=42>
Your code would look something akin to this:
variable = "SomeClassName"
klass = Class.new(ParentClass)
# ...maybe evaluate some code in the context of the new, anonymous class
klass.class_eval { }
# ...or define some methods
klass.send(:title, :Person)
klass.send(:attribute, :name, String)
# Finally, name that class!
ParentClass.send(:const_set, variable, klass)
...or you could just use eval:
eval <<DYNAMIC
class #{name}
title :Person
attribute :name, String
# ...or substitute other stuff in here.
end
DYNAMIC

Resources