the power of yield and super
The two keywords yield
and super
allow you to pass control back and forth between parent and child methods, to weave power between a more general method (in the parent class) and a more specific method (in the child class) with ease and logic. Using yield
and super
effectively can help you maintain the DRY (Don’t repeat yourself) principle, keeping your code easier to maintain.
Say you have three classes for image creation: a parent class, Image
, and two children: Raster
and Vector
. Now for your raster images, you’re going to be using the GD2 library for Ruby, and for your vector images, you’re going to be creating SVG images. Some of the functionality used to create any given image will be different, naturally, since with the raster images, you’re creating an image object, and with the vector images, you’re concatenating strings of XML SVG code. However, a lot of the code will be the same, and should thus be shared between the two children to avoid duplication. For example, the logic used to determine what color some text should be, or what shape you should draw, can be shared between the two child classes. Any shared code should go in the one common class between the two children: the parent class, Image
.
Accessing Shared Code
That is, accessing code in a parent class method from a child class method of the same name. The trick is with super
.
write_text()
in Raster
1 2 3 4 5 6 7 8 9 10 11 12 |
def write_text( text, color ) super() do |width, height, left, top| image = Image.new( width, height ) image.draw do |canvas| canvas.color = color canvas.font = Font::TrueType['/usr/share/fonts/times.ttf', 20] canvas.move_to( left, top ) canvas.text( text ) end end image end |
write_text()
in Vector
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
def write_text( text, color ) image = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>' image << '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/SVG/DTD/svg10.dtd">' super() do |width, height, left, top| image << '<svg viewBox="0 0 ' + width.to_s + ' ' + height.to_s + '" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">' image << '<text font-family="Times" x="' + left.to_s + '" y="' + top.to_s + '" fill="' + color.to_s + '">' end image << text.to_s image << '</text>' image << '</svg>' image end |
While these two methods accomplish the same thing, they do it differently. However, each of them has a call to super
. That will access a method of the same name in the parent class:
write_text()
in Image
All that Image
’s write_text()
is doing is passing values back to the child method that called it. Each child method’s super
call defines variables in the block: width, height, left, and top in the line that says super() do |width, height, left, top|
. The yield
call in Image
defines those values to be width = 300, height = 100, left = 5, and top = 5. To Image
’s write_text()
, however, it’s just throwing some numbers at the child method: it doesn’t know that the child method is going to use those numbers to define the dimensions of the image and the starting position of its text.
The definition of write_text()
in Image
is very simple, but it doesn’t have to be that way. There could be a great deal of calculation involved in determining what values the child methods should use for whichever variables are being passed to it. The parent class’s shared method is a place to store shared logic and shared data; let the child classes do the specific stuff with that general data.
Shared Variables Sans yield
Values can also be reached in the child class methods without using yield
if instance variables are defined in the parent method:
write_text()
in Image
Then write_text()
in both Raster
and Vector
would be able to access Image
’s @width
, @height
, @left
, and @top
, as defined in its write_text()
.
Multiple Calls to super
Sometimes it may be useful to let the parent class’s method do some work, then the child class’s, then the parent’s again. In this case, you can call super
and yield
as many times as necessary:
Method in Child Class
1 2 3 4 5 6 7 8 9 |
def my_method return1 = super( 'a string' ) do |my_var| # Do some stuff with my_var end return2 = super( ['array', 'of', 'strings'] ) do |my_other_var| # Do some stuff with my_other_var end end |
Method in Parent Class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def my_method( value ) if value.class == String yield( 'this is my_var' ) elsif value.class == Array yield( 'this is my_other_var' ) end # Checks to see if value can work with the << # method, which both Array and String objects # have, and if so, adds another string to it; # this value is returned since it's the last # line of the method value << ' appended to value' if value.respond_to?( :<< ) end |
yield
Returns Data to super
The child class method can also throw data back to the parent class method by returning a value from the super
block:
Method in Child Class
1 2 3 4 5 |
def my_method value_from_parent = super do |my_var| 'this gets thrown back to the parent class method' end end |
Method in Parent Class
1 2 3 4 |
def my_method value_from_child = yield( 'becomes my_var' ) 'this gets thrown back to the child class method' end |
More Information
- Programming Ruby: Classes, Objects, and Variables - Examples of
super
andyield
use. - Orthogonality and the DRY Principle - “DRY says that every piece of system knowledge should have one authoritative, unambiguous representation.”