1

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.

Anony-mouse
  • 2,041
  • 2
  • 11
  • 23
  • 1
    I corrected it, as not related to OP's question, just a typo. Perhaps a valid answer might point out that Sandi Metz' presentation cannot be applied (I am not sure), and without getting into design opinions, perhaps explain why many dynamic languages will avoid parameter-based overloading of methods. – Neil Slater Jun 18 '15 at 14:41
  • I think the idea behind OCP in your case is that `i` should produce its own results--rather than park() calculating the result based on `i`'s type. – 7stud Jun 18 '15 at 15:58
  • 1
    Apply the "Replace Conditional with Polymorphism Refactoring". It's really not that hard. Smalltalk doesn't even have conditionals (or loops, for that matter), and yet you can express everything you want quite elegantly in it. Object-oriented languages don't need conditionals, runtime polymorphic dynamic message dispatch is more powerful anyway. – Jörg W Mittag Jun 18 '15 at 16:33
  • @JörgWMittag can you explain a little bit more – Anony-mouse Jun 18 '15 at 17:30
  • @Anony-mouse Your other question? http://stackoverflow.com/q/30922788/438992 – Dave Newton Jun 18 '15 at 20:37
  • @Anony-mouse Oh, sorry, didn't see that this question was asked first--never mind! – Dave Newton Jun 18 '15 at 20:39
  • @DaveNewton thank you !! :) – Anony-mouse Jun 19 '15 at 12:27

5 Answers5

1

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]
7stud
  • 46,922
  • 14
  • 101
  • 127
1

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).

Neil Slater
  • 26,512
  • 6
  • 76
  • 94
  • *This does not meet your principles, but is idiomatic Ruby*. Using a case statement with `when` and using `types` for the branches makes your code trickier ruby, but it is no different than a multi-branch if-statement with type checking. I would argue that type checking in any language is NOT considered idiomatic. – 7stud Jun 19 '15 at 03:32
  • @7stud: I agree it is no different logically, I am suggesting that for type-checking code it is easier to read. I would also agree it is worth avoiding the need for type checking, but where I differ is how far to take it. "Clever" dynamic dispatch, or modifying class models to be non-intuitive (adding "park" to String) may be worse code smells than RTTI. I would argue that *idiomatic* means "it is considered a standard solution to the problem". You will find lots of example questions on SO using `case` e.g. http://stackoverflow.com/questions/3908380/ruby-class-types-and-case-statements – Neil Slater Jun 19 '15 at 09:08
  • @NeilSlater what is RTTI ? – Anony-mouse Jun 19 '15 at 10:33
  • RTTI = Run Time Type Identification. Possibly a bad acronym to use in Ruby (where there are no compile time constraints on type), but commonly understood in strongly-typed OO languages such as C++ and Java. – Neil Slater Jun 19 '15 at 11:23
  • @Anony-mouse: RTTI is just my (bad) shorthand for what you are doing, your question is an example. For C++ example, with an answer relevant to the OO designs you are asking about, you could see http://stackoverflow.com/questions/16266339/implementation-of-rtti-using-typeid – Neil Slater Jun 20 '15 at 06:29
1

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
engineersmnky
  • 25,495
  • 2
  • 36
  • 52
  • @parker ||= Object.const_get("#{obj.class}Park").new(obj) can you explain this line . – Anony-mouse Jun 18 '15 at 17:50
  • 1
    @Anony-mouse this is lazy loading it means that if `@parker` is not set then set it otherwise proceed with the current `@parker` it is evaluated as `@parker = @parker || Object.const_get("#{obj.class}Park").new(obj)`. So if `@parker` is nil then evaluates the or part. `Object.const_get` will return the constant referenced by the string so in the case where `obj` is a `String` this will be `Object.const_get("StringPark")` then it calls new with the `obj` so if `obj == "string"` it is evaluated as `@parker = @parker || Object.const_get("StringPark").new("string")`. Hope this helps – engineersmnky Jun 18 '15 at 19:46
  • 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)` – Anony-mouse Jun 18 '15 at 19:49
  • @Anony-mouse I thought I went over it pretty clearly above but let me try to add a little detail assuming `obj` is a `String` then `Object.const_get("StringPark")` will return the class `StringPark` we then call `StringPark.new(obj)` this sets this will then set the `@parker` instance variable in `StringPark` to the value of `obj`. This will allow you to add additional methods to `StringPark` that apply to `String` objects without impacting `FixnumPark`. Does that help? – engineersmnky Jun 18 '15 at 20:02
0

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
Andrew Kozin
  • 1,012
  • 9
  • 17
  • 1
    This is still using a conditional, but it's been collapsed into a ternary statement. – kevinthompson Jun 18 '15 at 14:33
  • But it is still a ternary operator.I want to avoid them completely through object oriented principles like function overloading.But Ruby do not support it and i am looking for an alternate which doesnot result in codesmells.I am looking at a solution where in if i decide to add one more check it will not result in a change in the main class.,but only result in addition of methods maybe. – Anony-mouse Jun 18 '15 at 14:35
  • IMHO you cannot exclude the decision entirely, just because you're expecting different behaviour depending on some condition. So the right question is who is responcible for that selection. You could define various classes with various `park`-s, but then you should add a factory method somewhere to select what object to instantiate in every case. – Andrew Kozin Jun 18 '15 at 14:43
  • 1
    The particular way you are doing what you are doing does not matter--it violates the OCP principle because as you add types, you have to change the if-statement/ternary operator/whatever. – 7stud Jun 18 '15 at 14:43
  • 2
    While I am a firm supporter of duck typing in its entirety (one of may favorite parts about ruby and true OO Programming) `String` does `respond_to?(:+)` so this does not meet the requested conditions – engineersmnky Jun 18 '15 at 15:05
  • It still works on the conditionals.According to Metz a code should not contain conditionals. – Anony-mouse Jun 18 '15 at 17:33
  • Okay so your update has a few issues. First neither `pluck_string` or `pluck_fixnum` are defined. Second it could be simplified to `def pluck(i);send("__pluck_#{i.class.downcase}__",i);end` (no conditional). Third if it is not a `String` or `Fixnum` then the response will be `nil`. Fourth the OP is looking to avoid code smell and generally early returns in ruby are considered code smell – engineersmnky Jun 18 '15 at 19:51
  • 1) Thanks. Corrected 2) No, i think this is a bad idea, because it tightly couples the code to the naming of the method 3) Yes, this is by intention (look at the second example in the post) 4) No, it doesn't. Guard clause is not a code smell. It is a sort of style https://github.com/bbatsov/rubocop/blob/4240ae468edbd1e1589ed3501cc16cbba3ab8513/lib/rubocop/cop/style/guard_clause.rb – Andrew Kozin Jun 18 '15 at 20:59
-1

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?

7stud
  • 46,922
  • 14
  • 101
  • 127
  • This does not answer the question. It appears to be an answer to a *different* question, raised in the comments of your other answer. It would be more useful on SO if the question and answer stood separately, and linked from here. – Neil Slater Jun 19 '15 at 09:11
  • @NeilSlater, Unlike you and your ilk, I actually care about helping the person who asked the question. – 7stud Jun 19 '15 at 16:00
  • It would both help the asker, and work with the site, if this answer were associated with the question in the usual way. Answering a different question helps the OP maybe a little, but then makes this page a mess and difficult for anyone else to figure out. Result is that you've helped the OP (one person) and obstructed some other potential users who have to figure out what is going on. – Neil Slater Jun 19 '15 at 16:07
  • @Neil Slater, *but then makes this page a mess and difficult for anyone else to figure out.* -- **lol** – 7stud Jun 19 '15 at 16:43