Who Owns the Work? A Closer Look at Ruby Object Design

Who Owns the Work? A Closer Look at Ruby Object Design

A Ruby class is often introduced with a short definition, an initializer, and one or two methods. That starting point explains the syntax, but object-oriented programming raises a broader question: what should a class be responsible for?

The answer is not found in the number of lines it contains. It comes from the relationship between information and behavior.

When responsibilities are described carefully, the connections between objects become visible, and a reader can understand why a method belongs in one place rather than another.

Begin with a Concrete Situation

Imagine a reading catalog. Each entry has a title, a category, and a page count. The catalog holds many entries and may need to list items by category.

It would be possible to keep everything in one hash and add operations around that hash. Another approach is to represent each entry as an object and let a separate catalog object coordinate the collection.

Neither structure should be selected merely because it contains more classes. The question is whether the structure reflects the responsibilities in the task.

An Entry class might store the details of one record. Its initializer could receive keyword arguments, such as title:, category:, and pages:.

Methods defined by attr_reader could provide ways to read those values.

A Catalog class might hold an array of entries and define a method named by_category.

This gives the entry responsibility for describing one item and the catalog responsibility for organizing several items.

Think About Conversations Between Objects

Objects cooperate through method calls. A catalog might call entry.category while deciding whether an entry belongs in a requested group.

The catalog does not need to know how the entry stores that category internally; it needs to know the method it can call.

This separation is part of encapsulation, a concept concerned with how an object presents its behavior while keeping internal details distinct.

Method names can make these conversations readable.

Compare catalog.by_category("History") with a series of statements that reach into internal arrays, inspect hashes, and repeat category comparisons across several locations.

The first expression describes the question in terms of the catalog's responsibility.

That does not mean every operation belongs behind another method. Too many layers can also obscure a small task.

A useful exercise is to examine the code and describe the purpose of each method in one plain sentence.

Consider What Callers Can Change

Ruby provides tools such as attr_reader, attr_writer, and attr_accessor, but selecting among them involves design decisions.

A reader allows a value to be obtained through a method; a writer defines an assignment method; an accessor supplies both.

If a catalog exposes a mutable array directly, outside code may change that array without using a catalog method.

In some situations that is acceptable, and in others a separate operation such as add helps communicate how new entries are introduced.

The distinction is not about hiding everything. It is about deciding which interactions belong to the public behavior of an object.

A record that should remain unchanged after creation may need different methods from a record that represents an evolving state.

Small examples with two objects holding different data can make this difference visible.

Choose Relationships with Care

Inheritance allows one Ruby class to derive behavior from another.

Composition describes a different arrangement: an object works with other objects it contains or receives.

For the reading catalog, composition is a natural idea because a catalog contains entries; a catalog is not a specialized kind of entry.

Inheritance becomes relevant when the class relationship genuinely describes a specialized form of the parent concept and the inherited behavior fits that relationship.

Ruby modules introduce another form of organization.

A module can group related methods or provide a namespace for connected definitions.

Modules, composition, and inheritance are not interchangeable decorations. Each describes a different relationship, and each deserves a reason grounded in the behavior of the code.

Review the Design Through Examples

Create two entries and add them to a catalog.

Ask what should happen when you request a category with no matches, when you add another entry, and when two entries share a title.

Write down the expected values, then examine the behavior of the corresponding methods.

Consider whether a method handles more than one responsibility, or whether two classes depend on details they do not need.

These questions offer a practical way to review object design.

Ruby object-oriented study becomes more interesting when the focus moves from class syntax to relationships.

A class tells part of the story, but the arrangement of responsibilities explains how the larger program fits together.

Back to blog