Use double dispatch. Each original overload first sends a dynamically dispatched message to its argument. The argument's selected body then calls an overloaded receiver method with this, whose static type inside that body is the desired argument software class.
class X {
  def sendTo(m: M): String = m.receive(this)
}
class Y extends X {
  override def sendTo(m: M): String = m.receive(this)
}
class Z extends Y {
  override def sendTo(m: M): String = m.receive(this)
}

class M {
  def f(x: X): String = x.sendTo(this)
  def f(x: Y): String = x.sendTo(this)

  def receive(x: X): String = "f with X at M"
  def receive(x: Y): String = "f with Y at M"
  def receive(x: Z): String = "f with Z at M"
}

class N extends M {
  override def f(x: Y): String = x.sendTo(this)
  def f(x: Z): String = x.sendTo(this)

  override def receive(x: X): String = "f with X at N"
  override def receive(x: Y): String = "f with Y at N"
  override def receive(x: Z): String = "f with Z at N"
}
All three receive overloads must be declared in M, because sendTo receives a parameter with static type M. Declaring the Z version only in N would repeat the original problem.
Whatever original f signature is selected, it invokes sendTo on the actual argument. For a Z object, dynamic dispatch enters Z.sendTo; inside that body, this has static type Z, so method overloading selects receive(Z). The final call dynamically dispatches on the actual m, giving N.receive(Z) when m refers to N. Hence
f with Z at N
f with Z at N
f with Z at N
The result depends on both runtime types:
For the stated X, Y, Z hierarchy, the same construction reports each argument software class correctly for either receiver software class. Further subclasses would need their own dispatch protocol to distinguish additional runtime types.
Replace the overloaded entry family by one forwarding method and introduce the second dispatch explicitly:
class X {
  def dispatch(to: M): String = to.visit(this)
}
class Y extends X {
  override def dispatch(to: M): String = to.visit(this)
}
class Z extends Y {
  override def dispatch(to: M): String = to.visit(this)
}

class M {
  def f(x: X): String = x.dispatch(this)
  def visit(x: X): String = "f with X at M"
  def visit(x: Y): String = "f with Y at M"
  def visit(x: Z): String = "f with Z at M"
}
class N extends M {
  override def visit(x: X): String = "f with X at N"
  override def visit(x: Y): String = "f with Y at N"
  override def visit(x: Z): String = "f with Z at N"
}
The original three calls still type-check because all their arguments are subtypes of X. They all refer to the same runtime Z object already; the change is how the operation discovers that runtime type. The first virtual call selects Z.dispatch, where this has static type Z, so overload resolution selects visit(Z). The second virtual call selects N.visit(Z) from the receiver's runtime type. Thus double dispatch gives f with Z at N on all three lines.
Replace the overloaded entry point by one forwarding method and add a virtual callback on the drink. The callbacks choose a reaction overload using the concrete drink class's statically known this type:
class Drink {
  def serveTo(b: Boy): Unit = b.react(this)
}
class Milk extends Drink {
  override def serveTo(b: Boy): Unit = b.react(this)
}
class Boy {
  def concept(d: Drink): Unit = d.serveTo(this)
  def react(d: Drink): Unit = println("Drink")
  def react(m: Milk): Unit = println("Milk")
}
class Baby extends Boy {
  override def react(d: Drink): Unit = println("Food")
  override def react(m: Milk): Unit = println("White drink")
}
The original b.concept(d) call now dynamically selects Milk.serveTo. Inside that method this has static type Milk, selecting react(Milk); virtual dispatch on b then selects Baby.react(Milk). Thus the output is White drink. This is double dispatch: changing only an overload's parameter name or the reference's runtime object would not change static overload selection.
The classifications and typical uses are
PatternCategoryWhen to use it
Command patternBehavioral design patternRepresent a request as an object, separating its invocation from the receiver. Commands can be queued or logged; undo additionally needs enough information to reverse the particular request.
Strategy patternBehavioral design patternSelect among interchangeable algorithms through a common interface, such as different routing or scoring policies.
Adapter patternStructural design patternWrap an existing component so clients can use it through a different required interface without changing that component.
State patternBehavioral design patternDelegate behavior to an object representing the current state, with transitions selecting another state object instead of scattering conditionals across operations.
Visitor patternBehavioral design patternAdd operations across a relatively stable element-class hierarchy, commonly through double dispatch. Adding a new element type then usually requires updating the visitors.
Visitor pattern 2026-10-31
Separate operations from a relatively stable hierarchy of element classes. An element's accept operation calls the visitor overload appropriate to its own concrete type, combining virtual dispatch with static overload selection to obtain double dispatch. New operations can be added as visitors; adding an element type generally requires extending the visitor interface and existing visitors.