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.
1 2 3 4 5 6 7 8 9 10 11 12 13
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 –
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…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
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
@per_lb even though it probably only uses one of them.
Can you create two different classes –
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.