7

Behavior-Oriented Design with Duck Typing and Mixins

Ruby source files are converted directly from the English source tree.

en/topic_07_duck_typing_polymorphism

Overview

Why this topic matters ๐Ÿ’ก

This topic is one of the clearest places to show what feels distinctively Ruby. Instead of centering class hierarchies or formal interfaces, students design around shared behavior and simple collaborator expectations.

Learning outcomes ๐ŸŽฏ

By the end of this topic, students should be able to:

  • explain duck typing as "objects are accepted for what they can do";
  • identify a small implicit contract such as export(rows);
  • extend a service with new collaborator types without modifying the service;
  • use a module mixin for shared behavior;
  • discuss the strengths and risks of implicit interfaces.

Assessment focus โœ…

Students should be able to justify why the service depends on behavior rather than a concrete exporter class.

Short Note

Duck typing is one of Ruby's most influential design ideas. Instead of asking "what class is this object?" Ruby often asks "can this object do what I need?"

For Java developers, this can feel both freeing and risky. The educational target is not to reject interfaces blindly. It is to see when a tiny behavior contract is clear enough without extra structure.

Ruby beauty in this topic:

  • a service can stay open to new collaborator types;
  • the contract can be very small and practical;
  • mixins let shared behavior travel without forcing hierarchy.

Ruby caution in this topic:

  • implicit contracts can become invisible if naming and tests are weak;
  • inheritance should not be the default extension tool.

Reflection prompt:

  • What makes an implicit contract understandable enough to be safe?

Worked Examples

Example 1: Export service ๐Ÿ’ก

Exporting the same report to CSV, JSON, XML-like markup, or an external API payload is a realistic case for duck typing.

ReportService.new(exporter: CsvExporter.new).call(rows)
ReportService.new(exporter: JsonExporter.new).call(rows)
worked_examples.md
ruby

The service does not care which exporter class it receives. It cares that the object responds to export(rows).

Example 2: Shared logging behavior ๐Ÿ’ก

Mixins are useful when multiple service objects need a small piece of shared behavior but inheritance would say the wrong thing about the domain.

Why this is useful:

  • the service remains focused;
  • extension happens through small collaborators and modules;
  • the code demonstrates Ruby's preference for composable behavior.

Cheatsheet

Duck-typed collaborator

class ReportService
  def initialize(exporter:)
    @exporter = exporter
  end

  def call(rows)
    @exporter.export(rows)
  end
end
cheatsheet.md
ruby

Mixin

module Loggable
  def log(message)
    "LOG: #{message}"
  end
end
cheatsheet.md
ruby

Design reminders โš–๏ธ

  • Depend on the method you need, not the class name you expect.
  • Tests are part of documenting the implicit contract.

Study Guide

Topic purpose ๐ŸŽฏ

Learn one of Ruby's most characteristic design moves: depending on behavior instead of concrete type. This topic should help students appreciate why Ruby can feel both powerful and elegant in extension scenarios.

Study sequence ๐Ÿชœ

  1. Read overview.md.
  2. Read shortnote.md carefully; this is a key Java-to-Ruby mindset topic.
  3. Study worked_examples.md.
  4. Use cheatsheet.md while reading example.rb.
  5. Complete the basic exporter exercise.
  6. Complete the advanced logging exercise and explain the design choice.

What to notice ๐Ÿ”Ž

  • The service depends on export, not on a class hierarchy.
  • New exporters can be introduced without modifying the service.
  • Mixins share behavior without implying "is-a" relationships.

Reflection questions ๐Ÿค”

  • Why is export(rows) a good implicit contract here?
  • What documentation makes duck typing safe enough for a team?
  • When would a formal interface or stricter boundary still be appropriate?

Source Files and Tests

Ruby source files are converted directly from the English source tree.

# EXAMPLE CODE
# Topic: topic_07_duck_typing_polymorphism
#
# Purpose:
# - This file demonstrates reference implementation for the concept.
# - It should pass tests from the beginning.
# - Read and understand it before solving exercises.

require "json"

class CsvExporter
  def export(rows)
    rows.map { |r| r.join(",") }.join("\n")
  end
end

class JsonExporter
  def export(rows)
    rows.to_json
  end
end

class ReportService
  def initialize(exporter:)
    @exporter = exporter
  end

  def call(rows)
    @exporter.export(rows)
  end
end
en/topic_07_duck_typing_polymorphism/example.rb
Ruby course source
# STUDENT TASK (BASIC)
# Topic: topic_07_duck_typing_polymorphism
#
# What to do:
# - Implement or improve the class/methods in this file.
# - Read tests in tests/topic_07_duck_typing_polymorphism_spec.rb under the "basic exercise" examples.
# - Make tests pass without breaking the example/advanced sections.
#
# Expected outcome:
# - You can run this topic tests and see all examples green after implementation.

class XmlLikeExporter
  def export(rows)
    body = rows.map { |r| "<row><c1>#{r[0]}</c1><c2>#{r[1]}</c2></row>" }.join
    "<rows>#{body}</rows>"
  end
end
en/topic_07_duck_typing_polymorphism/basic_exercise.rb
Ruby course source
# STUDENT TASK (ADVANCED)
# Topic: topic_07_duck_typing_polymorphism
#
# Academic purpose:
# - Show that Ruby extension is often about adding behavior, not building hierarchies.
# - Use a mixin to discuss reuse that is smaller and more local than inheritance.
#
# Real-world use case:
# - Service objects often need cross-cutting behavior such as logging, tracing, or simple auditing.
# - A logging mixin is useful when several services need that ability without pretending they are all
#   the same kind of object.
# - This complements the exporter example by showing two forms of extension in one topic.
#
# Why Ruby is beautiful here:
# - Duck typing keeps the main service open to new exporters.
# - A module can add behavior with very little ceremony.
# - The result is flexible, but still readable when the shared behavior is small.
#
# What to do:
# - Complete the challenge behavior requested by the guide.
# - Be able to explain why a mixin was chosen instead of deeper inheritance.
# - Use tests in tests/topic_07_duck_typing_polymorphism_spec.rb under the "advanced exercise" examples.
#
# Expected outcome:
# - Advanced tests pass and you can discuss the value and risk of implicit contracts.

module Loggable
  def log(message)
    "LOG: #{message}"
  end
end

class LoggedReportService < ReportService
  include Loggable
end
en/topic_07_duck_typing_polymorphism/adv_exercise.rb
Ruby course source
# ANSWER KEY (BASIC)
# Topic: topic_07_duck_typing_polymorphism
#
# Solution idea:
# - Implement the same `export(rows)` behavior contract used by the example exporters.
# - The service only cares that the object responds to `export`.
# - This exporter turns each row into a small XML-like structure.

class XmlLikeExporter
  def export(rows)
    body = rows.map { |row| "<row><c1>#{row[0]}</c1><c2>#{row[1]}</c2></row>" }.join
    "<rows>#{body}</rows>"
  end
end
en/topic_07_duck_typing_polymorphism/answer_basic_exercise.rb
Ruby course source
# ANSWER KEY (ADVANCED)
# Topic: topic_07_duck_typing_polymorphism
#
# Solution idea:
# - A mixin is a good fit for small shared behavior such as logging.
# - `LoggedReportService` inherits the report behavior and mixes in logging.
# - This keeps the shared concern separate from exporter logic.

module Loggable
  def log(message)
    "LOG: #{message}"
  end
end

class LoggedReportService < ReportService
  include Loggable
end
en/topic_07_duck_typing_polymorphism/answer_adv_exercise.rb
Ruby course source
# This spec is your learning companion for topic_07_duck_typing_polymorphism.
#
# How to use this file:
# 1) Run tests and observe failures/successes.
# 2) Keep EXAMPLE specs green from the beginning.
# 3) Implement BASIC exercise until BASIC specs pass.
# 4) Implement ADVANCED exercise and pass edge cases.
#
# Expected final result:
# - All examples in this file pass.
# - You understand both the concept and the implementation tradeoffs.

require_relative "../example"
require_relative "../basic_exercise"
require_relative "../adv_exercise"

RSpec.describe "topic_07_duck_typing_polymorphism" do
  describe "EXAMPLE purpose: understand the reference implementation" do
    it "exports with duck-typed services" do
      rows = [["name", "A"], ["role", "admin"]]

      expect(ReportService.new(exporter: CsvExporter.new).call(rows)).to include("name,A")
      expect(ReportService.new(exporter: JsonExporter.new).call(rows)).to include(%(["name","A"]))
    end
  end

  describe "BASIC EXERCISE purpose: implement the comparable task" do
    it "supports xml-like exporter with same interface" do
      rows = [["name", "A"], ["role", "admin"]]

      out = ReportService.new(exporter: XmlLikeExporter.new).call(rows)
      expect(out).to include("<rows>")
      expect(out).to include("<row>")
    end
  end

  describe "ADVANCED EXERCISE purpose: solve challenge and edge cases" do
    it "mixes loggable behavior into service" do
      service = LoggedReportService.new(exporter: CsvExporter.new)
      expect(service.log("ok")).to eq("LOG: ok")
    end
  end
end
en/topic_07_duck_typing_polymorphism/tests/topic_07_duck_typing_polymorphism_spec.rb
Ruby course source
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"

exec "${ROOT_DIR}/run_tests.sh" "$(basename "${SCRIPT_DIR}")"
en/topic_07_duck_typing_polymorphism/run_topic_tests.sh
Ruby course source

Study Prompts

  1. Read the spec first and identify which behaviors belong to the example, basic exercise, and advanced exercise.

  2. Attempt the exercises before opening the answer files, then compare the design choices.