Writing an Inherited Class

Problem

You want to create a new class that extends or modifies the behavior of an existing class.

Solution

If you're writing a new method that conceptually belongs in the original class, you can reopen the class and append your method to the class definition. You should only do this if your method is generally useful, and you're sure it won't conflict with a method defined by some library you include in the future.

This code adds a scramble method to Ruby's built-in String class (see Recipe 4.10 for a faster way to sort randomly):

class String def scramble split(//).sort_by { rand }.join end end "I once was a normal string.".scramble # => "i arg cn lnws.Ioateosma n r"

If your method isn't generally useful, or you don't want to take the risk of modifying a class after its initial creation, create a subclass of the original class. The subclass can override its parent's methods, or add new ones. This is safer because the original class, and any code that depended on it, is unaffected. This subclass of String adds one new method and overrides one existing one:

class UnpredictableString < String def scramble split (//).sort_by { rand }.join end def inspect scramble.inspect end end str = UnpredictableString.new("It was a dark and stormy night.") # => " hsar gsIo atr tkd naaniwdt.ym" str # => "ts dtnwIktsr oydnhgi .mara aa"

 

Discussion

All of Ruby's classes can be subclassed, though a few of them can't be usefully subclassed (see Recipe 8.18 for information on how to deal with the holdouts).

Ruby programmers use subclassing less frequently than they would in other languages, because it's often acceptable to simply reopen an existing class (even a built-in class) and attach a new method. We do this throughout this book, adding useful new methods to built-in classes rather than defining them in Kernel, or putting them in subclasses or utility classes. Libraries like Rails and Facets Core do the same.

This improves the organization of your code. But the risk is that a library you include (or a library included by one you include) will define the same method in the same built-in class. Either the library will override your method (breaking your code), or you'll override its method (breaking its code, which will break your code). There is no general solution to this problem short of adopting naming conventions, or always subclassing and never modifying preexisting classes.

You should certainly subclass if you're writing a method that isn't generally useful, or that only applies to certain instances of a class. For instance, here's a method Array#sum that adds up the elements of an array:

class Array def sum(start_at=0) inject(start_at) { |sum, x| sum + x } end end

This works for arrays that contain only numbers (or that contain only strings), but it

[79, 14, 2].sum # => 95 ['so', 'fa'].sum('') # => "sofa" [79, 'so'].sum # TypeError: String can't be coerced into Fixnum

Maybe you should signal this by putting it in a subclass called NumericArray or SummableArray:

class NumericArray < Array def sum inject(0) { |sum, x| sum + x } end end

The NumericArray class doesn't actually do type checking to make sure it only contains numeric objects, but since it's a different class, you and other programmers are less likely to use sum where it's not appropriate.[2]

[2] This isn't a hard and fast rule. Array#sort won't work on arrays whose elements can't be mutually compared, but it would be a big inconvenience to put sort in a subclass of Array or leave it out of the Ruby standard library. You might feel the same way about sum; but then, you're not the Ruby standard library.

You should also subclass if you want to override a method's behavior. In the UnpredictableString example, I overrode the inspect method in my subclass. If I'd just modified String#inspect, the rest of my program would have been thrown into confusion. Rarely is it acceptable to override a method in place: one example would be if you've written a drop-in implementation that's more efficient.

See Also

Категории