Handy Rails Tips

The Self-Frying Burger

A while ago, I gave a talk about Object-Oriented Programming at a Ruby conference. Afterwards, I was chatting about my talk with a friend who had a sceptical view of “Real OOP”.

“Yeah, but then you just end up with nonsense like burgers that fry themselves!” he joked (his background is in food tech).

He was being facetious, but the core argument is a relatively common one: if you follow the principles of OOP to their logical extreme, modelling the world as classes and objects, you end up writing nonsensical things like “burgers that fry themselves”. @burger.fry!.

The broader discussion was that a mixture of procedural code and objects is a more intuitive way to code than a pure OOP approach, because sometimes processes (cooking) live outside the objects they act upon (burgers).

To his mind, it makes more sense that a process should be contained in a single, discrete object, and that the items involved in the process are modified through external means. ‘The burger is fried’, rather than ‘the burger fries itself.’

This interaction echoes a conversation I have regularly with various engineers. I think it is ultimately a misunderstanding of the intention behind OOP.

Model the problem, not reality

One source of this misunderstanding is the mistaken belief that OOP intends to mirror reality in code. This suggests that a software system for managing a shoe shop should have classes called Shoe and Lace, and a system for managing a kitchen should have classes called Burger and FryingPan. But this is not the intention behind Object-Oriented Programming.

When writing OOP, the point is not to rebuild the real world in code, but to create a suitable model of a solution to a problem. The effectiveness of the solution is not how well it resembles the real world, but how elegantly and simply it can abstract a problem to its essential components. This means that, in practice, real world concepts such as shoe, lace, burger, and frying pan are usually represented as simpler, more general concepts—or excluded completely as irrelevant.

Imagining what sort of system cares about frying burgers, you might think of a stock management system for a kitchen. Such a system would be far too complicated if it had to represent every item in a kitchen literally in code, because a professional kitchen is likely to have hundreds of food ingredients. Ingredients are likely to change regularly, and would differ from kitchen to kitchen.

Modelling a burger as a @burger = Burger.new would be unmanageable. Instead, we should decide what the underlying abstraction is that the burger represents. This should be something relatively timeless and general to a broad set of current and future use cases. Ingredient might be a sensible name to use in this case, but that might not cover things like cleaning products or consumable items such as cocktail sticks and napkins. Maybe Consumable is a better abstraction for stock management?

(This is more art than science, and knowing how to choose the right abstractions for a concept is the art of good software design.)

Maybe there are different types of consumables, and managing stock is easier when they are classified by how they are consumed? Something like:

class Consumable
  def initialize(name:)
    @name = name
    @consumed = false
  end
  
  def consume!
    @consumed = true
  end
end

# Defines a consumable product that is purchased as volumes (e.g. 1l of Ketchup)
class VolumeConsumable < Consumable
  def initialize(name:, initial_volume:)
    super(name: name)
    @volume = initial_volume # in ml 
  end
  
  ##
  # Consume a given volume or the entire amount
  def consume_volume!(vol = @volume)
    # The most we can consume is the current volume
    consumed_volume = [vol, @volume].min
    @volume -= consumed_volume
    # mark as consumed when the volume reaches zero
    consume! if @volume.zero?
    @volume
  end
end

# Consume 500ml of ketchup
consumable = VolumeConsumable.new(name: "Ketchup", initial_volume: 1_000)
consumable.consume_volume!(500)

Taking this approach, “burger” would likely never be mentioned in the code of a kitchen stock management system at all, because it’s an irrelevant detail to the model.

The self-consumable consumable?

With the above code model, instead of telling a burger to go fry itself, we tell a consumable to mark itself as consumed. This doesn’t sound as ridiculous as a self-frying burger, but it is essentially the same thing.

Grouping data and behaviour

But renaming the self-frying burger doesn’t remove the underlying question behind the joke, we’ve simply found a better abstraction for the same principle. We’ve just found a better abstraction for the same principle. To understand how to think in OOP terms, we need to better understand encapsulation, one of OOP’s foundational concepts.

Encapsulation is often described as simply “bundling data with behaviour”. I often feel that this description hand-waves an important idea away as a mere code organisation technique. In reality, encapsulation represents a deeper theory about system design: that data and the logic that governs it should not be separate. They should exist as one indivisible thing: the object.

The object doesn’t simply hold its data. It owns its data completely as an integral part of itself, and is solely responsible for managing and interpreting it.

We see this principle at work constantly in Ruby. Consider how we work with time:

time = Time.now
time.sunday? # => true or false

In other paradigms, it’s normal for this sort of interrogation to happen externally:

func isSunday(timestamp) {
  return dayOfWeek(timestamp) === 0; 
}

The Ruby Time object doesn’t just store a timestamp. It contains all the intelligence needed to understand and answer questions about itself. No external code has to pull apart the internal representation and apply its own calendar logic. If we instantiated that time object just before midnight on a Saturday and asked it the same question a few minutes later, we might get a different answer. The behaviour belongs to the data.

This creates a profound shift from other programming approaches. Instead of passive data waiting for external procedures to act upon it, we have active objects that participate in the system. They maintain their own state according to their own rules.

Return to our kitchen example. When we call:

consumable.consume_volume!(500)

We’re not reaching in and manipulating the volume directly. We’re asking the object to update its own state according to rules it alone understands. It can validate the request, handle partial consumption, decide when it should be marked as fully consumed, and preserve consistency—all within clear boundaries. If this logic lived outside the object—in service classes, utility functions, or scattered throughout the codebase—we would create countless opportunities for inconsistency. Every new piece of code that touched a consumable would need perfect knowledge of how consumption works. That knowledge would inevitably drift and duplicate.

By keeping the rules inside the object, we create a reliable boundary. The rest of the system can treat consumables as black boxes that simply do things when asked. This is what allows object-oriented systems to scale.

The self-frying burger only sounds absurd when we interpret the metaphor too literally. The real goal isn’t to simulate reality. It’s to give our abstractions ownership over their own state and behaviour. Once you internalise this idea—that behaviour should be encapsulated in the same place as the data it operates on—what once seemed ridiculous becomes one of the most powerful tools we have for managing complexity in software systems.