Is this deadly sin killing your Ruby code?
Nothing kills clean, object-oriented code faster than feature envy. What’s feature envy? It’s a code smell that looks harmless, but quickly spreads throughout your code, making it impossible to decouple components from one another.
Identify and eliminate feature envy, and you’ll end up with a cleanly decoupled system that’s easy to change. Let it slip under the radar, and your code will become brittle and break any time you try to change it.
Fortunately, you can spot feature envy easily: any time a caller asks for data from another object, there’s chance it has feature envy. If the caller asks for two pieces of data, it almost certainly does. And if it asks for three pieces of data, it’s in full-on envy mode.
Let’s look at an example. Our system will calculate shipping based on weight – or sometimes use flat shipping.
class Seller
def initialize(shipper)
@shipper = shipper
end
def shipping_cost(weight)
if @shipper.flat_rate
@shipper.flat_rate
else
weight * @shipper.per_lb
end
end
end
Seller#shipping_cost
asks the @seller
for two pieces of data – flat_rate
and per_lb
. It’s not the worst thing in the world… but what if we want to provide free shipping for orders under 50 lbs? It quickly gets messy.
Besides, why should a seller know how to calculate shipping cost? Isn’t that something a shipper would know?
We can make the code easier to understand and change by moving the calculation into the shipper
object. Check it out…
class Seller
def initialize(shipper)
@shipper = shipper
end
def shipping_cost(weight)
@shipper.cost weight
end
end
class Shipper
def initialize(flat_rate: nil, per_lb: nil)
@flat_rate = flat_rate
@per_lb = per_lb
end
def cost(weight)
@flat_rate ? @flat_rate : (weight * @per_lb)
end
end
Now any object that implements #cost(weight)
can calculate the shipping cost. It’s a perfect example of a domain method.
Challenge: Replace conditional with polymorphism
Shipper#cost
is definitely an improvement over what we had before… but it has some unnecessary complexity – it has @flat_rate
and @per_lb
even though it probably only uses one of them.
Can you create two different classes – FlatRateShipper
and WeightBasedShipper
– to separate those two pieces of behavior? What changes do you need to make to Seller
to make that work?
Eradicate feature envy with East-Oriented Code
There’s one sure-fire way to eradicate feature envy, and that’s to use the East-Oriented principle that my friend James taught me. I’ve got a whole chapter on it in RubySteps.
Do you want to write better Ruby code? Do you recognize words like “encapsulate”, “decouple”, and “polymorphism” but have no idea how to use them in practice? Get RubySteps and you’ll learn all about how to use object-oriented programming, refactoring, and testing to create high-quality, maintainable Ruby applications.