In POODR 2nd edition, page 125, Sandy Metz writes
There are two new messages, default_chain and default_tire_size, sent on lines 6 and 7 below. ... Wrapping the defaults in methods is good practice in general
The code she's referring to is below. Note how the default value for chain and tire_size is set.
class Bicycle
attr_reader :size, :chain, :tire_size
def initialize(**opts)
@size = opts[:size]
@chain = opts[:chain] || default_chain
@tire_size = opts[:tire_size] || default_tire_size
end
def default_chain # <- common default
"11-speed"
end
def default_tire_size # <- common default
"2.1"
end
end
Why is this approach better than the familiar method of simply setting the default value inside initialize(), i.e.
def initialize(chain: "11-speed", tire_size:"2.1", **opts):
This book is about OOP design, so I'm guessing the answer has something to do with good OOP practice but I'm not sure what.
Methods are easier to work with. You can stub them in tests or change the implementation (load values from a config file or something like that), all without touching code that uses them.
But the bigger difference between your code and what Sandi suggests is the handling of falsy values.
In your code, it's possible to pass nils explicitly instead of the default value.
Bicycle.new(chain: nil, tire_size: "2.1", ...)
It might not be a hardcoded nil in your code, but come from somewhere else. Regardless, Bicycle will accept it and then maybe crash at runtime when you try to use the value.
Whereas code from the book does not accept nil values, no matter if it's a default nil or explicitly sent. If chain is falsy, "11-speed" will be used.
Using a method modularizes the code so that subclasses can override the behavior without overriding the initializer:
class MountainBike < Bicycle
def default_tire_size
'18'
end
end
If you used positional arguments initialize(chain="11-speed", tire_size="2.1", **opts) this actually is a major downgrade as you now have to remember the order of the arguments. Positional arguments should only be used when there is an obvious order to the arguments.
You can actually set the defaults for both positional and keyword arguments in the arguments list from a method like this:
class Bicycle
attr_reader :size, :chain, :tire_size
def initialize(tire_size: default_tire_size, chain: default_tire_size, **opts)
end
# ...
end
However its not done very often since it tends to make the method signature very cluttered.