skip to content

A calculation reads mostly another object's data, so the feature-envy smell says move it onto that object. Explain when that advice breaks down for an operation that needs the state of two different objects, and how languages that do not attach methods to a single receiver - Julia, Common Lisp's CLOS, Clojure multimethods - change the answer.

level: principalimportance: nice to knowfreq 22%

answer

  1. Envy relocates when the operation is binary
  2. C++ friend = the non-member symmetric operator
  3. Swift private is file-scoped - same-file function sees both
  4. C#/Kotlin extensions: public access only, naming not encapsulation
  5. CLOS/Julia: method belongs to a generic function, not a class

basics

~20 s

"Move it to the data" is well defined only when one object owns the data. For a genuinely binary operation, single-receiver languages force you to pick an owner and make the other object expose state, so the envy relocates. C++ friend functions, Swift's file-scoped private and CLOS or Julia generic functions let the operation belong to neither type.

solid answer

~50 s

The smell says behaviour sits away from its data; the cure assumes there is one home. - **Java, C#, Python, Ruby** dispatch on one receiver, so a two-operand operation must live on one type, which then reads the other's accessors - the envy has moved, not gone. - **C++ `friend` functions** were designed for exactly this: a free function granted access to both operands, which is why symmetric binary operators are idiomatically non-members. - **Swift**: `private` is file-scoped, so a free function in the same file legitimately reads both types' private state. The identical refactor is impossible with a **C# extension method or a Kotlin extension function** - both compile to static functions with only public access, so they fix naming, not encapsulation. - **CLOS and Julia** select a method using the tuple of argument types, so the operation is owned by a generic function in a module rather than by a class. Cost: no single place lists a type's behaviour, and slots or fields are effectively open.

code

swift · 6 lines
swift
// Pricing.swift
struct Order  { private let weight: Int }
struct Tariff { private let perKg: Int }

// legal: same file, so both private members are visible
func price(_ o: Order, _ t: Tariff) -> Int { o.weight * t.perKg }

go deeper

for a junior

Recognise the smell and apply the simple cure when one object clearly owns the data used by the method.

for a middle

Explain why the cure is ambiguous for a two-operand operation, and know that a Kotlin or C# extension does not grant private access.

for a senior

Resolve the pair case by naming the interaction or narrowing the question asked of the collaborator, and identify which mechanism your language actually offers.

for a principal

Weigh the ecosystem consequences: generic-function designs compose across packages but give up a per-type behaviour listing and rely on social encapsulation - decide whether your codebase can hold that discipline.

## What feature envy actually claims The smell describes a method that spends its time reading another object's state - it "envies" the features of a class other than its own. The standard cure is to move the method to the class whose data it uses, so that behaviour and the data it needs sit together and the accessors it was using can disappear. When one object really owns the data, this works perfectly and is one of the highest-value refactorings there is. ## Where the cure stops being well defined Consider an operation that needs state from two objects roughly equally: pricing an order against a tariff, deciding whether a shipment satisfies a customs rule, comparing two measurements in different units. Move it to the first and it reads the second's accessors; move it to the second and it reads the first's. Whichever you choose, one of the two types must publish state it would rather keep, and the envy has been relocated rather than removed. In a single-receiver language this is a real, unavoidable tension, not a failure of skill: the language provides exactly one place for a method to live, and the operation belongs to a *pair*. Three honest resolutions exist inside such languages: 1. **Promote the interaction to a concept.** If pricing an order against a tariff is a real domain idea, then a `Quotation` type that receives both and owns the rule has genuine cohesion, and the two operands can each expose a narrow, intention-revealing surface to it instead of full accessors. This is the answer that scales. 2. **Push a question, not data.** Instead of reading three fields from the second object, ask it a question that returns the decision: `tariff.rateFor(category)` rather than `tariff.getBands()`. The operation still lives on one side, but the collaboration is a request, not an inspection. 3. **Accept the asymmetry deliberately** and document which side owns the rule, so it does not drift back and forth in review. ## The mechanisms other languages offer **C++ friend functions.** C++ anticipated the problem: a class may declare a free function (or another class) a friend, granting it access to private members. This is why symmetric binary operators in C++ are idiomatically written as non-member functions - a member operator privileges its left operand and breaks symmetry, including for implicit conversions. So C++ has a first-class way to say "this operation belongs to neither type" while keeping both encapsulated. The price is that the access grant is written into both class definitions, so the classes still name the operation. **Swift's file-scoped access.** Swift's `private` is scoped to the enclosing declaration and file, and `fileprivate` to the file. A free function declared in the same file as two types can therefore read both types' private members legitimately, with no access-control widening. The refactor "lift the operation out of both types into a function beside them" is legal in Swift. Contrast that sharply with **C# extension methods and Kotlin extension functions**, which are frequently offered as the answer and are not. Both compile to static functions dispatched on the static type, and both can only touch the public surface of the receiver - a Kotlin extension outside the class cannot see its private members, and neither can a C# extension. They improve naming and call-site readability; they do nothing for encapsulation, and an operation moved into an extension is still reading accessors. That distinction - Swift yes, C#/Kotlin no - is the concrete test of whether a language actually supports the refactor or only appears to. **Generic functions: CLOS, Julia, Clojure multimethods.** These languages do not attach methods to a receiver at all. A method is selected using the tuple of argument types, and it belongs to a generic function that lives in a package or namespace. "Which class should own this?" simply does not arise: the operation over a pair is defined for that pair, symmetrically, in a module chosen for cohesion. Julia's whole ecosystem is built on this, which is why unrelated packages compose so well - a new method for a new type pair can be added by a third party. The costs are real and are cohesion costs, not performance ones. There is no single place that enumerates what a type can do, so answering "what is this object's behaviour?" needs tooling (`methodswith` in Julia, the metaobject protocol in CLOS). Encapsulation is weak - Julia struct fields are conventionally accessible, CLOS slots are reachable through `slot-value` - so the discipline that keeps state private is social. And in Clojure, protocol extension has no coherence check, so two libraries extending the same protocol to the same type resolve by load order. ## How to use this in review When someone raises feature envy, ask first whether the data has one owner. If yes, move the method and delete the accessors - the smell is doing its job. If the operation is genuinely binary, do not let the review turn into a ping-pong of moving the method back and forth. Name the interaction, give it a type or a module-level home, and reduce what each operand exposes to the narrowest question the operation needs to ask. Then check what your language actually offers: a C++ friend, a Swift same-file function, or a generic function in CLOS or Julia is a real home for the operation, while a C# or Kotlin extension is only a nicer name for the same envy.

  • Why are symmetric binary operators in C++ idiomatically non-member functions?
    A member operator treats its left operand specially: the left side must already be of the class type, so implicit conversions apply only to the right operand and the operation stops being symmetric. A free function takes both operands on equal terms, and `friend` grants it the private access it needs. The design says outright that some operations belong to a pair rather than to a type.
  • Someone proposes moving an envious method into a Kotlin extension function on the envied class. Has the smell been addressed?
    Only cosmetically. A Kotlin extension compiles to a static function resolved on the static type and can only use the receiver's public members, so it still reads the same accessors from outside. It improves the call site's readability, which is worth something, but the state is still being pulled out of the object to make a decision elsewhere.

saying these in an interview costs you the question

  • Applying 'move it to the data' mechanically to an operation that needs two objects, then moving it back next review
  • Claiming C# or Kotlin extension functions give access to private members
  • Treating multiple dispatch as free - it removes the ownership question and removes the single place that lists a type's behaviour
  • Solving the pair problem by widening one type's accessors and calling that cohesion
  • Forgetting the option of asking the collaborator a question instead of reading its fields

context