Sandi Metz says in SOLID OOPS concepts from GORUCO that presence of if..else blocks in Ruby can be considered to be a deviation from Open-Close Principle. What all methods can be used to avoid not-urgent if..else conditions? I tried the following code:
class Fun
def park(s=String.new)
puts s
end
def park(i=Fixnum.new)
i=i+2
end
end
and found out that function overloading does not work in Ruby. What are other methods through which the code can be made to obey OCP?
I could have simply gone for:
class Fun
def park(i)
i=i+2 if i.class==1.class
puts i if i.class=="asd".class
end
end
but this is in violation to OCP.
With your current example, and wanting to avoid type detection, I would use Ruby's capability to re-open classes to add functionality you need to Integer and String:
class Integer
def park
puts self + 2
end
end
class String
def park
puts self
end
end
This would work more cleanly when altering your own classes. But maybe it doesn't fit your conceptual model (it depends what Fun represents, and why it can take those two different classes in a single method).
An equivalent but keeping your Fun class might be:
class Fun
def park_fixnum i
puts i + 2
end
def park_string s
puts s
end
def park param
send("park_#{param.class.to_s.downcase}", param)
end
end
As an opinion, I am not sure you will gain much writing Ruby in this way. The principles you are learning may be good ones (I don't know), but applying them forcefully "against the grain" of the language may create less readable code, regardless of whether it meets a well-intentioned design.
So what I would probably do in practice is this:
class Fun
def park param
case param
when Integer
puts param + 2
when String
puts param
end
end
end
This does not meet your principles, but is idiomatic Ruby and slightly easier to read and maintain than an if block (where the conditions could be far more complex so take longer for a human to parse).
You could just create handled classes for Fun like so
class Fun
def park(obj)
#parker ||= Object.const_get("#{obj.class}Park").new(obj)
#parker.park
rescue NameError => e
raise ArgumentError, "expected String or Fixnum but recieved #{obj.class.name}"
end
end
class Park
def initialize(p)
#park = p
end
def park
#park
end
end
class FixnumPark < Park
def park
#park += 2
end
end
class StringPark < Park
end
Then things like this will work
f = Fun.new
f.park("string")
#=> "string"
f.instance_variable_get("#parker")
#=> #<StringPark:0x1e04b48 #park="string">
f = Fun.new
f.park(2)
#=> 4
f.instance_variable_get("#parker")
#=> #<FixnumPark:0x1e04b48 #park=4>
f.park(22)
#=> 6 because the instance is already loaded and 4 + 2 = 6
Fun.new.park(12.3)
#=> ArgumentError: expected String or Fixnum but received Float
You could do something like this:
class Parent
attr_reader :s
def initialize(s='')
#s = s
end
def park
puts s
end
end
class Child1 < Parent
attr_reader :x
def initialize(s, x)
super(s)
#x = x
end
def park
puts x
end
end
class Child2 < Parent
attr_reader :y
def initialize(s, y)
super(s)
#y = y
end
def park
puts y
end
end
objects = [
Parent.new('hello'),
Child1.new('goodbye', 1),
Child2.new('adios', 2),
]
objects.each do |obj|
obj.park
end
--output:--
hello
1
2
Or, maybe I overlooked one of your twists:
class Parent
attr_reader :x
def initialize(s='')
#x = s
end
def park
puts x
end
end
class Child1 < Parent
def initialize(x)
super
end
def park
x + 2
end
end
class Child2 < Parent
def initialize(x)
super
end
def park
x * 2
end
end
objects = [
Parent.new('hello'),
Child1.new(2),
Child2.new(100),
]
results = objects.map do |obj|
obj.park
end
p results
--output:--
hello
[nil, 4, 200]
And another example using blocks, which are like anonymous functions. You can pass in the desired behavior to park() as a function:
class Function
attr_reader :block
def initialize(&park)
#block = park
end
def park
raise "Not implemented"
end
end
class StringFunction < Function
def initialize(&park)
super
end
def park
block.call
end
end
class AdditionFunction < Function
def initialize(&park)
super
end
def park
block.call 1
end
end
class DogFunction < Function
class Dog
def bark
puts 'woof, woof'
end
end
def initialize(&park)
super
end
def park
block.call Dog.new
end
end
objects = [
StringFunction.new {puts 'hello'},
AdditionFunction.new {|i| i+2},
DogFunction.new {|dog| dog.bark},
]
results = objects.map do |obj|
obj.park
end
p results
--output:--
hello
woof, woof
[nil, 3, nil]
Look at the is_a? method
def park(i)
i.is_a?(Fixnum) ? (i + 2) : i
end
But even better not to check a type, but use duck typing:
def park(i)
i.respond_to?(:+) ? (i + 2) : i
end
UPD: After reading comments. Yes, both examples above don't solve the OCP problem. That is how I would do it:
class Fun
# The method doesn't know how to pluck data. But it knows a guy
# who knows the trick
def pluck(i)
return __pluck_string__(i) if i.is_a? String
__pluck_fixnum__(i) if i.is_a? Fixnum
end
private
# Every method is responsible for plucking data in some special way
# Only one cause of possible changes for each of them
def __pluck_string__(i)
puts i
end
def __pluck_fixnum__(i)
i + 2
end
end
I understand or equal to operation in ruby but can you explain what
you have done with:
Object.const_get("#{obj.class}Park").new(obj)
In ruby, something that starts with a capital letter is a constant. Here is a simpler example of how const_get() works:
class Dog
def bark
puts 'woof'
end
end
dog_class = Object.const_get("Dog")
dog_class.new.bark
--output:--
woof
Of course, you can also pass arguments to dog_class.new:
class Dog
attr_reader :name
def initialize(name)
#name = name
end
def bark
puts "#{name} says woof!"
end
end
dog_class = Object.const_get("Dog")
dog_class.new('Ralph').bark
--output:--
Ralph says woof!
And the following line is just a variation of the above:
Object.const_get("#{obj.class}Park").new(obj)
If obj = 'hello', the first portion:
Object.const_get("#{obj.class}Park")
is equivalent to:
Object.const_get("#{String}Park")
And when the String class object is interpolated into a string, it is simply converted to the string "String", giving you:
Object.const_get("StringPark")
And that line retrieves the StringPark class, giving you:
Object.const_get("StringPark")
|
V
StringPark
Then, adding the second portion of the original line gives you:
StringPark.new(obj)
And because obj = 'hello', that is equivalent to:
StringPark.new('hello')
Capice?
Related
I would like to be able to insert some code at the beginning and at the end of methods in my class. I would like to avoid repetition as well.
I found this answer helpful, however it doesn't help with the repetition.
class MyClass
def initialize
[:a, :b].each{ |method| add_code(method) }
end
def a
sleep 1
"returning from a"
end
def b
sleep 1
"returning from b"
end
private
def elapsed
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
block_value = yield
finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)
puts "elapsed: #{finish - start} seconds, block_value: #{block_value}."
block_value
end
def add_code(meth)
meth = meth.to_sym
self.singleton_class.send(:alias_method, "old_#{meth}".to_sym, meth)
self.singleton_class.send(:define_method, meth) do
elapsed do
send("old_#{meth}".to_sym)
end
end
end
end
The above does work, but what would be a more elegant solution? I would love to be able to, for example, put attr_add_code at the beginning of the class definition and list the methods I want the code added to, or perhaps even specify that I want it added to all public methods.
Note: The self.singleton_class is just a workaround since I am adding code during the initialisation.
If by repetition you mean the listing of methods you want to instrument, then you can do something like:
module Measure
def self.prepended(base)
method_names = base.instance_methods(false)
base.instance_eval do
method_names.each do |method_name|
alias_method "__#{method_name}_without_timing", method_name
define_method(method_name) do
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
public_send("__#{method_name}_without_timing")
t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
puts "Method #{method_name} took #{t2 - t1}"
end
end
end
end
end
class Foo
def a
puts "a"
sleep(1)
end
def b
puts "b"
sleep(2)
end
end
Foo.prepend(Measure)
foo = Foo.new
foo.a
foo.b
# => a
# => Method a took 1.0052679998334497
# => b
# => Method b took 2.0026899999938905
Main change is that i use prepend and inside the prepended callback you can find the list of methods defined on the class with instance_methods(false), the falseparameter indicating that ancestors should not be considered.
Instead of using method aliasing, which in my opinion is something of the past since the introduction of Module#prepend, we can prepend an anonymous module that has a method for each instance method of the class to be measured. This will cause calling MyClass#a to invoke the method in this anonymous module, which measures the time and simply resorts to super to invoke the actual MyClass#a implementation.
def measure(klass)
mod = Module.new do
klass.instance_methods(false).each do |method|
define_method(method) do |*args, &blk|
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
value = super(*args, &blk)
finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)
puts "elapsed: #{finish - start} seconds, value: #{value}."
value
end
end
end
klass.prepend(mod)
end
Alternatively, you can use class_eval, which is also faster and allows you to just call super without specifying any arguments to forward all arguments from the method call, which isn't possible with define_method.
def measure(klass)
mod = Module.new do
klass.instance_methods(false).each do |method|
class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{method}(*)
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
value = super
finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)
puts "elapsed: \#{finish - start} seconds, value: \#{value}."
value
end
CODE
end
end
klass.prepend(mod)
end
To use this, simply do:
measure(MyClass)
It looks like you're trying to do some benchmarking. Have you checked out the benchmark library? It's in the standard library.
require 'benchmark'
puts Benchmark.measure { MyClass.new.a }
puts Benchmark.measure { MyClass.new.b }
Another possibility would be to create a wrapper class like so:
class Measure < BasicObject
def initialize(target)
#target = target
end
def method_missing(name, *args)
t1 = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
target.public_send(name, *args)
t2 = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
::Kernel.puts "Method #{name} took #{t2 - t1}"
end
def respond_to_missing?(*args)
target.respond_to?(*args)
end
private
attr_reader :target
end
foo = Measure.new(Foo.new)
foo.a
foo.b
Always working with Rails but now that I am coding completely without framework I am missing a lot of methods that I am not able to write myself. So I need a little hint here.
So I've got this code
class Foo
initialize(k)
(1..k).each do |b|
Boo.new(b)
end
end
class Boo
initialize(a)
#a = a
end
end
Foo.new(10)
#and now I want to access one of the Boo's with a certain a (in Rails Boo.find_by_a(2) e.g.)
How do I write a method that uses a certain value "a" to get the specific Boo back with #a = a?
Update:
Okay I added this methot to Boo
def self.all
ObjectSpace.each_object(self).to_a
end
And now I get the right object by:
boo = Boo.all.select{ |b| b.a == 2 }.first
But if I want this to be a method, where would i put it?
DO NOT RELY ON ObjectSpace for this functionality when the GC collects you will lose these objects because Ruby sees them as unneeded.
This is the general concept of what you are intending to do
class Foo
attr_reader :boos
def initialize(k)
#boos = (1..k).map {|b| Boo.new(b) }
end
def find_boo_with(x)
#boos.find {|b| b.a == x }
end
end
class Boo
attr_reader :a
def initialize(a)
#a = a
end
end
Then you can do this
f = Foo.new(10)
f.boos
#=> [#<Boo:0x256f428 #a=1>,
#<Boo:0x256f3b0 #a=2>,
#<Boo:0x256f368 #a=3>,
#<Boo:0x256f338 #a=4>,
#<Boo:0x256f2f0 #a=5>,
#<Boo:0x256f2d8 #a=6>,
#<Boo:0x256f2a8 #a=7>,
#<Boo:0x256f290 #a=8>,
#<Boo:0x256f278 #a=9>,
#<Boo:0x256f260 #a=10>]
f.find_boo_with(9)
#=> #<Boo:0x256f278 #a=9>
I hope this helps but you really should look into something like RubyMonk, or TryRuby to get a better understanding of how PORO (Plain Old Ruby Objects) work.
You could also do it this way if you really wanted your find methodology just know that no Boo in this case will ever be collected as they will all be stored in a class instance variable called #all_boos which could casue memory issues depending on what a Boo is and how many of them you are creating.
class Boo
class << self
def all_boos
#all_boos ||= []
end
def find(x)
#all_boos.find {|boo| boo.a == x }
end
alias_method :all, :all_boos
end
attr_reader :a
def initialize(a)
#a = a
self.class.all_boos << self
end
end
Then you can do
10.times {|i| Boo.new(i) }
Boo.all
#=> [#<Boo:0x256f428 #a=1>,
#<Boo:0x256f3b0 #a=2>,
#<Boo:0x256f368 #a=3>,
#<Boo:0x256f338 #a=4>,
#<Boo:0x256f2f0 #a=5>,
#<Boo:0x256f2d8 #a=6>,
#<Boo:0x256f2a8 #a=7>,
#<Boo:0x256f290 #a=8>,
#<Boo:0x256f278 #a=9>,
#<Boo:0x256f260 #a=10>]
Boo.find(9)
#=> #<Boo:0x256f278 #a=9>
If I did understand what you need then I think that you can use ObjectSpace to achieve this. This is the sample code that illustrates the use:
class Foo
def initialize(k)
(1..k).each do |b|
Boo.new(b)
end
end
end
class Boo
attr_reader :a
def initialize(a)
#a = a
end
def self.find(value)
ObjectSpace.each_object(Boo).select { |b| b.a == value }.first
end
end
Foo.new(10)
Boo.find(2).a
Let me know if this is the solution you were looking for.
Not sure I 100% understand your question but using attr_reader in Boo will allow you to access a as if it were a function.
class Foo
def initialize(k)
(1..k).each do |b|
Boo.new(b)
end
end
end
class Boo
attr_reader :a
def initialize(a)
#a = a
end
end
Foo.new(10)
As for the other part of your question, here is a good article explaining how dynamic finders work
I have several methods which have similar conditional logic and I think it's reasonable to DRY them.
class A
def parent1(val)
puts "parent1 A #{val}"
end
def parent2(val)
puts "parent2 A #{val}"
end
end
class B < A
def parent1(val)
if val
puts "foo"
else
super
end
end
def parent2(val)
if val
puts "bar"
else
super
end
end
end
Is it possible to create a #puts_or_super which would be able to call super of its caller? (B#parent1 or B#parent2) So the code will look like this:
def parent1(val)
puts_or_super(val, "foo")
end
def parent2(val)
puts_or_super(val, "bar")
end
Edit: this works but looks insane
def puts_or_super(val, text)
if val
puts text
else
self.class.superclass.instance_method(caller[0][/`.*'/][1..-2].to_sym).bind(self).call
end
end
Any better solutions?
Suppose your definitions are in class A, and the relevant super methods are defined in class B so that A inherits from B.
I think you should go the other way around. Make B a module, and prepend B to A. Then, the definitions where you have the "foo", "bar", etc. would not be the super methods, and the conditions will be in the prepended module.
module B
def parent1(val)
return super if val
... # your original logic in super class
end
def parent2(val)
return super if val
... # your original logic in super class
end
end
class A
prepend B
def parent1(_); puts "foo" end
def parent2(_); puts "bar" end
end
Just for fun, again, but is it possible to take a block that contains method definitions and add those to an object, somehow? The following doesn't work (I never expected it to), but just so you get the idea of what I'm playing around with.
I do know that I can reopen a class with class << existing_object and add methods that way, but is there a way for code to pass that information in a block?
I guess I'm trying to borrow a little Java thinking here.
def new(cls)
obj = cls.new
class << obj
yield
end
obj
end
class Cat
def meow
puts "Meow"
end
end
cat = new(Cat) {
def purr
puts "Prrrr..."
end
}
cat.meow
# => Meow
# Not working
cat.purr
# => Prrrr...
EDIT | Here's the working version of the above, based on edgerunner's answer:
def new(cls, &block)
obj = cls.new
obj.instance_eval(&block)
obj
end
class Cat
def meow
puts "Meow"
end
end
cat = new(Cat) {
def purr
puts "Prrrr..."
end
}
cat.meow
# => Meow
cat.purr
# => Prrrr...
You can use class_eval(also aliased as module_eval) or instance_eval to evaluate a block in the context of a class/module or an object instance respectively.
class Cat
def meow
puts "Meow"
end
end
Cat.module_eval do
def purr
puts "Purr"
end
end
kitty = Cat.new
kitty.meow #=> Meow
kitty.purr #=> Purr
kitty.instance_eval do
def purr
puts "Purrrrrrrrrr!"
end
end
kitty.purr #=> Purrrrrrrrrr!
Yes
I suspect you thought of this and were looking for some other way, but just in case...
class A
def initialize
yield self
end
end
o = A.new do |o|
class << o
def purr
puts 'purr...'
end
end
end
o.purr
=> purr...
For the record, this isn't the usual way to dynamically add a method. Typically, a dynamic method starts life as a block itself, see, for example, *Module#define_method*.
Let's go to the code directly:
#!/usr/bin/ruby
require 'tk'
class Epg
def initialize
#var = "bad"
#cvs = nil
#items_demo = TkRoot.new() {title "EPG"}
TkFrame.new(#items_demo) {|cf|
#var = "good"
#cvs = TkCanvas.new(cf) {|c|}
puts "#cvs 1 is #{#cvs}"
puts "#var 1 is #{#var}"
}.pack('side'=>'top', 'fill'=>'both', 'expand'=>'yes')
puts "#cvs 2 is #{#cvs}"
puts "#var 2 is #{#var}"
end #initialize
def test
#var = "bad"
puts " #var 3 :#{#var}"
(1..3).each {|x| #var="good"}
puts " #var 4 :#{#var}"
end
end
e= Epg.new
e.test
Here is the output:
#cvs 1 is #<Tk::Canvas:0xb7cecb08>
#var 1 is good
#cvs 2 is
#var 2 is bad ##var has NOT been changed by the code in the block
#var 3 :bad
#var 4 :good ##var has been changed by the code in the block
Why we see different behavior here?
You can think of blocks as closing over both the set of local variables and the current self.
In Ruby, you will always have access to local variables, no matter what. The self encapsulates instance methods on the current object as well as instance variables.
Consider the following code:
class Table
def initialize(legs)
#legs = legs
end
def with_legs
yield #legs
end
end
And then:
def some_calling_method
name = "Yehuda"
Table.new(4) {|legs| puts "#{name} gnaws off one of the #{legs} legs" }
end
By Ruby's block semantics, you can be assured that name will be available inside the block, even without looking at the method you're calling.
However, consider the following:
class Person
def initialize(name)
#name = name
end
def gnaw
Table.new(4).with_legs do |legs|
puts "#{#name} gnaws off one of the #{legs} legs"
end
end
end
Person.new("Yehuda").gnaw
In this case, we are accessing the #name instance variable from inside the block. It works great in this case, but is not guaranteed. What if we implemented table a bit differently:
class Table
def initialize(legs)
#legs = legs
end
def with_legs(&block)
self.instance_eval(&block)
end
end
Effectively, what we're saying is "evaluate the block in the context of a different self." In this case, we are evaluating the block in the context of the table. Why would you do that?
class Leg
attr_accessor :number
def initialize(number)
#number = number
end
end
class Table
def initialize(legs)
#legs = legs
end
def with_leg(&block)
Leg.new(rand(#legs).instance_eval(&block)
end
end
Now, you could do:
class Person
def initialize(name)
#name = name
end
def gnaw
Table.new(4).with_leg do
puts "I'm gnawing off one of leg #{number}"
end
end
end
If you wanted access to the person object inside of the block, you'd have to do:
class Person
def initialize(name)
#name = name
end
def gnaw
my_name = name
Table.new(4).with_leg do
puts "#{my_name} is gnawing off one of leg #{number}"
end
end
end
As you can see, the use of instance_eval can make it simpler and less bulky to access methods of a far-off object inside a block, but comes at the cost of making the self unaccessible. The technique is usually used in DSLs, where a number of methods are injected into the block, but the self doesn't matter that much.
This is what's happening with Tk; they're using instance_eval to inject their own self into the block, which is wiping your self clean.
The explanation is that TkFrame.new uses instance_eval and thus the assignment #var = "good" changes the instance variable of TkFrame. Try this out:
class A
def initialize(&b)
instance_eval(&b)
end
end
class B
def initialize
#x = 10
#a = A.new do
#x = 20
end
end
end
p B.new
This is what you'll see:
#<B:0x10141314 #x=10, #a=#<A:0x10141300 #x=20>>