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)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
endruby
Mixin
module Loggable
def log(message)
"LOG: #{message}"
end
endruby
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 ๐ช
- Read
overview.md. - Read
shortnote.mdcarefully; this is a key Java-to-Ruby mindset topic. - Study
worked_examples.md. - Use
cheatsheet.mdwhile readingexample.rb. - Complete the basic exporter exercise.
- 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
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
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
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
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
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
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}")"
Ruby course source
Study Prompts
Read the spec first and identify which behaviors belong to the example, basic exercise, and advanced exercise.
Attempt the exercises before opening the answer files, then compare the design choices.