0
\$\begingroup\$

I defined the class Rectangle:

class Rectangle
  attr_reader :b, :h
  def initialize(b, h)
    @b = b
    @h = h
  end
  def area
    @b*@h
  end
  def to_s
    "Rectangle #{@b}x{@h}"
  end
end

and its subclass Square:

class Square < Rectangle

  attr_reader :s
  def initialize(s)
    @s = s
    super(@s, @s)
  end
  def to_s
    "Square of side #{@s}"
  end
end

Now, let's say I defined a Rectangle object r = Rectangle.new(5, 5), but actually it should be a Square because I'd like r.to_s to return "Square of side 5".

I can define a to_square method in the Rectangle class that returns a Square object equivalent to r, but, is it possible to write a to_square! method that would actually change the r class to Square without returning another object, so that r.class would now return Square instead of Rectangle?

And, what if I'd like:

Square(r)

to return the Square equivalent object of r, just like:

Integer("3")

which returns the Integer 3?

Is that possible? And, if so, how?

\$\endgroup\$
  • 1
    \$\begingroup\$ Don't close questions if you can migrate them to SO! \$\endgroup\$ – user13328 May 22 '14 at 0:19
  • 1
    \$\begingroup\$ the hall monitors strike again. i wish you guys wouldn't do this. i got here from google, it was exactly what i needed and rather then find help, i find know it alls telling the submitter and me how stupid we are. \$\endgroup\$ – user1130176 Oct 11 '18 at 16:08
1
\$\begingroup\$

First off, why bother with a separate Square class?

Personally, I'd make a Rectangle class' to_square! method simply set both width and height to Math.sqrt(area). The result is still a Rectangle, just a square one.

The Rectangle's initializer method could also be written with an optional second argument; If there's only one argument, use that for both width and height, and you have a square.

Furthermore, I'd add a square? method. Checking obj.square? is, in my view, preferable to checking obj.class == Square. You could also use square? in to_s to return one string or the other.

To answer your question, though, I don't know of any way to just change an instance's class. You could however experiment with including different modules depending on whether the rectangle in question is a square or not. But that all seems pretty complex.

Finally, if you want a Square method, simply define one:

def Square(side)
  Rectangle.new(side, side)
end

Even if you have a Square class already, you can still have a method of the same name:

class Square
  # ...
end

def Square(side) # no naming conflict
  Square.new(side)
end
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Because I'm studying inheritance and subclassing :-) \$\endgroup\$ – DuArme Oct 7 '12 at 13:07
  • 1
    \$\begingroup\$ @DuccioArmenise Ah, well that explains it :) In that case, I'd definitely go with tokland's suggestion of a factory method on the class, or a Square() factory outside the class (like you see above), and/or have Rectangle#to_square (no bang) return a new Square instance. Don't start changing the class of an instantiated object, even if you can (and again, I doubt you can - it'd get confusing in a hurry). \$\endgroup\$ – Flambino Oct 7 '12 at 13:27
  • \$\begingroup\$ Ok, but what about if r, instance of Rectangle, can't be converted in a Square because it isn't a square. Conventionally speaking, it would be right to assume that r.to_square should return false while r.to_square! should raise an exception? \$\endgroup\$ – DuArme Oct 7 '12 at 13:40
  • \$\begingroup\$ @DuccioArmenise Depends on what you want to enforce. I imagine a to_square that returns a square with the same area as r, regardless of whether r is already a square. Same as calling to_i on a float returns an int even if the float can't be perfectly expressed as an int; to_i doesn't raise. Alternatively, you could have a to_square that raises an exception for a non-square r, while a to_square! forcibly returns a "squared square". However, in this context, I'd say that's a bad use of the bang-suffix. \$\endgroup\$ – Flambino Oct 7 '12 at 14:14
  • \$\begingroup\$ Just found this statement of Matz's about it: "The bang (!) does not mean "destructive" nor lack of it mean non destructive either. The bang sign means "the bang version is more dangerous than its non bang counterpart; handle with care" (wobblini.net/bang.txt) And, from the Pickaxe: "Methods that are dangerous, or that modify their receiver, may be named with a trailing exclamation mark" (ruby-doc.org/docs/ProgrammingRuby/html/tut_methods.html). :-) \$\endgroup\$ – DuArme Oct 7 '12 at 14:31
1
\$\begingroup\$

If you make the expression ClassName.new return something different than an instance of ClassName you will be breaking the programmer's expectations. The most idiomatic, and simple, way is writing a factory method:

class Rectangle
  # same code you have

  def self.with_sides(b, h)
    b == h ? Square.new(b) : Rectangle.new(b, h)
  end
end

Rectangle.with_sides(1, 2).to_s # Rectangle 1x2
Rectangle.with_sides(2, 2).to_s # Square of side 2

Of course, you can always do black magic with Ruby's new, but I'd advise against it.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Really interesting answer and article, thank you. But I didn't ask for ClassName.new to return something different from an instance of ClassName. Instead I asked if it's possible to change the class of an existing instance in Ruby (I guess it's not possible, as in Java, and for good reasons, but I don't know for sure yet). And I agree with @Flambino when he sais that even if it would be possible it would be a bad idea. \$\endgroup\$ – DuArme Oct 7 '12 at 13:35

Not the answer you're looking for? Browse other questions tagged or ask your own question.