Ben Lakey Ben Lakey - 7 months ago 17
Ruby Question

Ruby constants for defaults in template method parameters?

This is indeed a question about Ruby so don't mind the initial pile of Java.

I come from a Java background and in that language it's common to see default values captured in constants. Here's an example:

public class Foo {

private static final int DEFAULT_POINTS = 10;

private final int points;

public Foo() {
this.points = DEFAULT_POINTS;
}

public Foo(Integer initialPoints) {
this.points = initialPoints;
}

public int getPoints() {
return points;
}

}


My question is about doing similar in Ruby. The above could be translated into ruby as follows:

class Foo

attr_reader :points

def initialize(options={})
@points = options[:points] || default_points
end

def default_points
10
end

end


Ignore for a moment that I could just specify the default in the constructor inline. Let's pretend it's a message-send because I eventually might want to do some sub-classing.

My question is this: In Java I capture the default value in a constant at the top of my class. Is there a similar notion in Ruby, or is it common to leave it as just a return value inside of the method?

Answer

Ruby inherits its design philosophy from perl which basically boils down to having multiple ways to do a thing so you can (in theory) express yourself in the most understandable way.

It comes to no surprise then that there's a couple of ways to do this, depending on your use case and your preferences

Way 1, magic number, this is perfectly OK (even more so, it's preferred) as long as the context is clear and you don't use default points elsewhere in the app (see http://thedailywtf.com/Articles/Soft_Coding.aspx)

def initialize(options)
  points = options[:points] || 10
end

Once you start reusing default points elsewhere in the app it's refactor time and there's several approaches, each with its own upsides and downsides

Way 2, constant, upside is that it's clear and accessible from anywhere (through Foo::DEFAULT_POINTS), downside is that it's not OOP and overriding it in child classes doesn't work as expected

DEFAULT_POINTS = 10

def initialize(options)
  points = options[:points] || DEFAULT_POINTS
end

Ways 3 & 4, instance method vs class method, you did the instance method in your example, class method would work as such:

def self.default_points
  10
end

def initialize(options)
  points = options[:points] || self.class.default_points
end

a refinement of this approach (once self.class self.class self.class self.class spam starts getting to you, rubyists are a bit OCD about these things) is to delegate from instance to class, example in rails:

def self.default_points
  10
end
delegate :default_points, to: 'self.class'

def initialize(options)
  points = options[:points] || default_points
end

There are pros and cons to using class methods vs instance methods to do stuff, best resource to learn what they are is http://blog.codeclimate.com/blog/2012/11/14/why-ruby-class-methods-resist-refactoring/

Way 5, private instance method - when you are immediately certain that default points will be used just in the context of the class and not outside and you want to reuse it through the class and inherit/override in child classes,

def initialize(options)
  points = options[:points] || default_points
end

private

def default_points
  10
end

Note that in Ruby you can still access a private method by calling foo.send(:default_points). Once you start spamming foo.send(:default_points) in your code however it's a smell that something needs refactoring (either to remove the need to call default_points from outside of the class or to make default_points public).

The Ruby way is the way of programming freedom, you can rewrite other people's classes on runtime, you can change values of constants, you can call private methods... Ruby doesn't prevent anything and with great power comes great responsibility.

Basic philosophy is that if you shouldn't do something in ruby, you shouldn't be prevented to do it (warned yes, should=must no) which is in stark contract to the Java way you're probably accustomed to.

Downside to this is that bad people will write bad code, upside is that if you really need to do something extrarodinary that 0.01% of the time when it counts, you will be able to do it (applying a monkey patch to oracle driver until your pull requests get released is a good example).

UPDATE

One thing to have in mind when using ways other then way 2: avoid instantiating an object time and time again (especially a heavy object), see http://gavinmiller.io/2013/basics-of-ruby-memoization/

Also, https://gist.github.com/bbozo/8468940

Comments