skip to content

What is a "hybrid" class in the objects-versus-data-structures sense, and how do Tell-Don't-Ask and feature envy relate to it?

level: middleimportance: should knowfreq 40%

answer

  1. Hybrid = exposed data + real behaviour = worst of both
  2. Feature envy = uses another object's fields more than its own
  3. Tell, Don't Ask: account.withdraw(x), not get/compute/set
  4. Move Method to the data owner
  5. Split: dumb persistence record vs rich domain object

basics

~20 s

A hybrid is a class that both exposes all its data through getters/setters and carries important business behaviour. It gets the downsides of both styles. Feature envy is code that uses another object's data more than its own; Tell-Don't-Ask fixes it by moving the behaviour to the data.

solid answer

~60 s

A **hybrid** is half object, half data structure: public accessors expose the entire representation *and* the class holds significant business logic. Consequences: callers can bypass the behaviour and reimplement rules on the raw fields (so invariants leak and duplicate), yet the class is too opinionated to be treated as a dumb serializable record. Hybrids are usually the residue of an incomplete refactoring, or of an ORM/framework requiring no-arg constructors and setters. The diagnostic smell is **feature envy**: a method that reads several fields of *another* object to make a decision is more interested in that object than its own, and it belongs over there. **Tell, Don't Ask** is the corrective principle - do not ask an object for state and then decide on its behalf; tell it to do the thing (`account.withdraw(x)` instead of `if (account.getBalance() >= x) account.setBalance(...)`). Practical remedies: move the method to the data owner, replace getter chains with intention-revealing operations, keep framework-required setters package-private or use dedicated persistence/DTO types so the domain object stays sealed.

code

pseudocode · 7 lines
pseudocode
// Ask (feature envy, rule duplicated at every caller)
if (account.getBalance() >= amount) {
  account.setBalance(account.getBalance() - amount)
}

// Tell (rule lives with the data it protects)
account.withdraw(amount)   // throws or returns a result if funds are short

go deeper

for a junior

Define hybrid, and give the withdraw example contrasting get/compute/set with telling the object to do it.

for a middle

Name feature envy as the detector and Move Method as the fix; mention that invariants leak through setters.

for a senior

Discuss splitting persistence/DTO models from domain objects, legitimate exceptions (mappers, reports, query methods), and how ORM constraints should be contained.

for a principal

Treat it as a boundary policy: which layers may hold behaviour, how invariants are guarded across aggregates, and how conventions plus review or static rules stop hybrids from accreting.

## Hybrid defined Two coherent designs exist: **objects** (hide data, expose behaviour) and **data structures** (expose data, no behaviour). A **hybrid** sits in the middle: it exposes its data *and* contains meaningful behaviour. Symptoms: - A full set of public getters and setters, plus methods enforcing business rules. - Callers writing rules from the accessors: `if (order.getStatus() == NEW && order.getLines().size() > 0 && order.getCustomer().isActive())`. - Documentation or reviews arguing about "should this validation live in the service or the entity?" - and both answers being partly true in the codebase. Why it is the worst of both worlds: - **Invariants leak.** A setter can put the object into a state its own methods swore was impossible. - **Duplication.** The same rule gets re-derived at every caller that reaches into the fields. - **Change is expensive on both axes.** Adding a type is not cheap (callers switch on fields) and adding an operation is not cheap (behaviour is already scattered). - **Serialization/versioning friction.** Behaviour and lifecycle assumptions ride along with data you wanted to be dumb. ## Feature envy A method suffers **feature envy** when it uses another class's data more than its own - typically pulling three or four getters from the same object and computing something. That is a location error: the computation wants to live in the class it is envying, next to the data it needs. Moving it there removes accessors from the public surface, which is what actually reduces coupling. Not all feature envy is bad. Deliberate cases: a report or export layer that must read many objects, a mapper that translates domain to DTO, and cases where the envied class must remain a pure data structure (an event payload). The judgment: is the logic a *rule of the envied concept*, or a rule of the *caller's* concept that merely reads data? ## Tell, Don't Ask The corrective: **do not ask an object for its state and then decide for it - tell it what to do.** - Ask style: `if (account.getBalance() >= amount) { account.setBalance(account.getBalance() - amount); }` - the rule lives in the caller, repeated everywhere, and the setter can violate it. - Tell style: `account.withdraw(amount)` - the rule lives once, with the data it protects; an overdraft can be rejected by throwing or returning a result type. This is also the deep reason behind the Law of Demeter: train wrecks are usually asking, at depth. Queries are not banned. `account.canCover(amount)` is a legitimate ask if the *decision* it feeds is not the account's business (e.g. UI enabling a button). The line: asking for a *judgment* the object is qualified to make is fine; asking for *raw internals* so you can make its judgment yourself is not. ## Practical remedies 1. **Move Method** to the class whose data is being envied (the standard refactoring). 2. **Replace accessor chains with intention-revealing operations** (`order.shipTo(address)`). 3. **Split the hybrid**: a dumb persistence/DTO record at the edge, plus a behaviour-rich domain object inside; map between them. 4. **Constrain framework-driven setters**: constructor injection, package-private or private setters, or a separate persistence model, so an ORM's requirements do not dictate the domain API. 5. **Delete unused accessors.** Getters exist because someone needed them; if nobody does now, removing them shrinks the surface that hybrids grow on. ## Counter-pressure Tell-Don't-Ask taken to an extreme produces objects that try to do everything, including presentation and persistence - violating single responsibility and making the domain depend on infrastructure. The balanced position: rules about a concept live with that concept; orchestration, I/O, and cross-aggregate coordination live in services that *tell* those objects what to do.

  • An ORM requires a no-arg constructor and setters. Does that force your entities to be hybrids?
    No. Keep setters non-public or use a separate persistence model mapped to a behaviour-rich domain object. The framework's instantiation needs should not become the domain type's public API.
  • Is every getter a violation of Tell-Don't-Ask?
    No. Asking an object for a judgment it is qualified to make (canCover, isExpired) or for data the presentation layer must display is legitimate. The violation is pulling raw internals to apply the object's own rules on its behalf.

Asking a doctor for your raw lab numbers so you can diagnose yourself is 'ask'. Telling the doctor 'treat me' is 'tell'. A hybrid is a doctor who both diagnoses you and hands out the raw chart to anyone who walks in.

saying these in an interview costs you the question

  • Believing setters are fine because the fields are technically private
  • Reimplementing the same business rule at each caller from getters, then calling it 'the service layer'
  • Treating all feature envy as a defect, including mappers and reporting code
  • Pushing Tell-Don't-Ask so far that domain objects render UI or perform persistence
  • Assuming a framework requirement (ORM setters, serialization) justifies exposing the whole representation as public API

context