DEV Community

Augusts Bautra
Augusts Bautra

Posted on

What happens when OOP meets Real World™

This has bugged me for a while, so I asked GPT.

Question

What have you heard of design principles in the object orientation space. I generally subscribe to encapuslation and keeping data with the behaior, but with years of experience I see cracks in the wall, specifically regarding the tension between "what should know HOW to do something" (answer clearly being the appropriate object) VS "what should know when and what messages to dispatch to what objects" (answer being less clear if we have public methods on objects). I think record creation stands out as a prominent special case where encapuslation does not quite work. For example, I have a nesting of models Parent -> (many)Child. And I need a versioning/copying functionality for the aggregate. I do not think Child should know anything about duplicating itself, the public API should be Parent.find(123).duplicate! and the method should orchestrate duplication of the nested records.

Answer

You're describing a very mature and nuanced tension in object-oriented design—something many experienced developers confront as they move from theoretical purity (e.g. “Tell, don’t ask”; encapsulation; single responsibility) into the messy domain of real-world complexity.

You're right to highlight encapsulation and “behavior-close-to-data” as core OO ideas—but also right to sense their limits, especially in orchestration-heavy scenarios like object duplication, versioning, or domain-level transactions that span multiple models.

So where do we put orchestration logic that must traverse multiple objects? Some options:

✅ Options for Handling the Tension

1. Orchestrator or Service Object Pattern (Explicit Application Layer)

You create a dedicated class:

class ParentDuplicator def initialize(parent) @parent = parent end def call new_parent = @parent.dup new_parent.children = @parent.children.map { |child| child.dup } new_parent.save! new_parent end end ParentDuplicator.new(Parent.find(123)).call 
Enter fullscreen mode Exit fullscreen mode

🟢 Pros:

  • Keeps orchestration logic out of domain models.
  • Testable, composable.
  • Models remain lean.

🔴 Cons:

  • Moves behavior away from data.
  • Slightly procedural.
  • Some see this as breaking OO purity.

This is very common in DDD (Domain-Driven Design) and “hexagonal”/ports-and-adapters architectures, where orchestration is elevated out of entities and into use cases (often called "application services").

2. Make the Aggregate Root Responsible (Your Example)

Let Parent do the orchestration:

class Parent def duplicate! new_parent = dup new_parent.children = children.map(&:dup) new_parent.save! new_parent end end 
Enter fullscreen mode Exit fullscreen mode

🟢 Pros:

  • Keeps domain logic within the model.
  • Makes the call site simple and intuitive: Parent.find(123).duplicate!

🔴 Cons:

  • Parent accumulates coordination logic; can bloat over time.
  • Harder to reuse logic across different duplication scenarios.

This works best if you're comfortable with your aggregate root knowing about its internals (a DDD-approved idea), and you don’t expect the duplication logic to vary or get more complex.

3. Delegate Duplication to the Nested Object

You'd have:

class Child def duplicate dup end end 
Enter fullscreen mode Exit fullscreen mode

And Parent#duplicate! calls child.duplicate.

🟢 Pros:

  • Encourages polymorphism, reuse.
  • Might be useful if Child has internal relationships or constraints.

🔴 Cons:

  • Child is now coupled to a context-specific behavior (versioning/duplication).
  • Violates SRP if duplication is not core to Child.

This works better when duplication is intrinsic to the object’s identity (e.g. a Task that can be forked) rather than contextual orchestration (like versioning a tree of data).

🧠 Philosophical Take

Encapsulation is valuable, but contextual behaviors like versioning, importing, duplicating, transforming, etc., don’t always belong in the domain objects. These behaviors are:

  • Temporal (used in specific moments)
  • Often cross-object
  • Might require knowledge the object shouldn't have (e.g. associations, persistence, time, request context)

This is where "Application Layer" logic comes into play. And that’s not a betrayal of OO—it’s an acknowledgment that domain entities model state and invariants, not every procedural flow.

My agreement

Spot on! I think "Might require knowledge the object shouldn't have" is the key insight. We're not breaking from OOP, but reframing what is the knowledge context and what is the object that is most appropriate to encapsulate the context.

Top comments (0)