In this comprehensive guide, we’ll explore four fundamental concepts in computer science and object-oriented programming: the Template Method pattern, Strategy patterns, parameterized types, and object relationships through aggregation and acquaintance. These concepts form the backbone of modern software design and appear across virtually every programming language.
1. Template Method Pattern: Defining the Skeleton of an Algorithm
What is Template Method?
The Template Method is a behavioral design pattern that defines the skeleton of an algorithm in a base class but lets subclasses override specific steps without changing the algorithm’s structure. Think of it as a recipe where the overall cooking process is fixed, but individual chefs can customize certain steps.
The Core Idea
Instead of having multiple classes each implement the complete algorithm, you create:
- A base/parent class that outlines the overall process
- Subclasses that override specific “hook” methods to customize behavior
This follows the “Hollywood Principle”: “Don’t call us, we’ll call you.” The parent class controls the flow and calls the methods that subclasses provide.
Ruby Example
Let’s create a beverage brewing system:
# Base class defining the template method
class BeverageMaker
def brew
gather_ingredients
heat_water
add_ingredients
steep
serve
end
def gather_ingredients
puts "Gathering ingredients..."
end
def heat_water
puts "Heating water to appropriate temperature..."
end
# These are hook methods that subclasses will override
def add_ingredients
raise NotImplementedError, "Subclasses must implement add_ingredients"
end
def steep
raise NotImplementedError, "Subclasses must implement steep"
end
def serve
puts "Pouring into a cup..."
end
end
# Tea subclass
class TeaMaker < BeverageMaker
def add_ingredients
puts "Adding tea leaves to the infuser..."
end
def steep
puts "Steeping for 3-5 minutes..."
end
end
# Coffee subclass
class CoffeeMaker < BeverageMaker
def add_ingredients
puts "Adding ground coffee to the filter..."
end
def steep
puts "Brewing for 4-6 minutes..."
end
def serve
puts "Adding milk and sugar as desired, then pouring..."
end
end
# Usage
puts "=== Making Tea ==="
tea = TeaMaker.new
tea.brew
puts "\n=== Making Coffee ==="
coffee = CoffeeMaker.new
coffee.brew
Output:
=== Making Tea ===Gathering ingredients...Heating water to appropriate temperature...Adding tea leaves to the infuser...Steeping for 3-5 minutes...Pouring into a cup...=== Making Coffee ===Gathering ingredients...Heating water to appropriate temperature...Adding ground coffee to the filter...Brewing for 4-6 minutes...Adding milk and sugar as desired, then pouring...
Real-World Application: Data Processing
class DataProcessor
def process(file_path)
data = read_file(file_path)
data = validate(data)
data = transform(data)
data = enrich(data)
save_output(data)
end
def read_file(file_path)
raise NotImplementedError
end
def validate(data)
puts "Validating data..."
data
end
def transform(data)
raise NotImplementedError
end
def enrich(data)
puts "Enriching data with metadata..."
data
end
def save_output(data)
raise NotImplementedError
end
end
class CSVProcessor < DataProcessor
def read_file(file_path)
puts "Reading CSV file: #{file_path}"
[["Name", "Age"], ["Alice", 30], ["Bob", 25]]
end
def transform(data)
puts "Transforming CSV data to hash format..."
data
end
def save_output(data)
puts "Saving processed data to database..."
end
end
class JSONProcessor < DataProcessor
def read_file(file_path)
puts "Reading JSON file: #{file_path}"
{"users" => [{"name" => "Alice", "age" => 30}]}
end
def transform(data)
puts "Transforming JSON data to standardized format..."
data
end
def save_output(data)
puts "Saving to API endpoint..."
end
end
Benefits
- Code Reuse: Common logic is written once in the parent class
- Consistency: Ensures all subclasses follow the same algorithm structure
- Flexibility: Subclasses can customize only what they need
- Maintainability: Changes to the overall algorithm are made in one place
2. Strategy Pattern: Encapsulating Interchangeable Algorithms
What is Strategy Pattern?
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from clients that use it. Unlike Template Method, where variations happen through inheritance, Strategy uses composition to swap algorithms at runtime.
The Core Idea
You create:
- A Strategy interface that defines the algorithm contract
- Concrete strategy classes that implement different variants
- A context class that uses a strategy object
This allows you to change the algorithm used without modifying the client code.
Ruby Example
Let’s create a payment processing system:
# Strategy interface (in Ruby, we use duck typing or modules)
module PaymentStrategy
def pay(amount)
raise NotImplementedError
end
end
# Concrete strategies
class CreditCardPayment
include PaymentStrategy
def initialize(card_number, cvv)
@card_number = card_number
@cvv = cvv
end
def pay(amount)
puts "Processing credit card payment of $#{amount}"
puts "Card: #{@card_number[-4..-1]}"
validate_cvv
puts "Payment approved!"
end
private
def validate_cvv
puts "Validating CVV..."
end
end
class PayPalPayment
include PaymentStrategy
def initialize(email)
@email = email
end
def pay(amount)
puts "Sending $#{amount} via PayPal to #{@email}"
authenticate
puts "PayPal payment processed!"
end
private
def authenticate
puts "Authenticating with PayPal..."
end
end
class CryptocurrencyPayment
include PaymentStrategy
def initialize(wallet_address, crypto_type = "Bitcoin")
@wallet_address = wallet_address
@crypto_type = crypto_type
end
def pay(amount)
puts "Sending #{amount} satoshis to wallet #{@wallet_address}"
puts "Cryptocurrency: #{@crypto_type}"
confirm_blockchain
puts "Transaction confirmed on blockchain!"
end
private
def confirm_blockchain
puts "Confirming on blockchain..."
end
end
# Context class
class ShoppingCart
def initialize(payment_strategy)
@payment_strategy = payment_strategy
@total = 0
end
def add_item(price)
@total += price
end
def checkout
@payment_strategy.pay(@total)
end
# Strategy can be changed at runtime
def change_payment_method(new_strategy)
@payment_strategy = new_strategy
end
end
# Usage
puts "=== Customer 1: Credit Card Payment ==="
cart1 = ShoppingCart.new(CreditCardPayment.new("4532-1234-5678-9010", "123"))
cart1.add_item(50)
cart1.add_item(30)
cart1.checkout
puts "\n=== Customer 2: PayPal Payment ==="
cart2 = ShoppingCart.new(PayPalPayment.new("user@example.com"))
cart2.add_item(100)
cart2.checkout
puts "\n=== Customer 3: Changes mind about payment ==="
cart3 = ShoppingCart.new(CreditCardPayment.new("5412-9876-5432-1098", "456"))
cart3.add_item(75)
puts "Initial strategy: Credit Card"
cart3.change_payment_method(CryptocurrencyPayment.new("1A1z7agoat4WYvtQy06YnYs73m7nEChoCM", "Bitcoin"))
puts "Changed strategy: Cryptocurrency"
cart3.checkout
Real-World Application: Sorting Algorithms
module SortStrategy
def sort(array)
raise NotImplementedError
end
end
class BubbleSort
include SortStrategy
def sort(array)
puts "Sorting using Bubble Sort..."
n = array.length
(0...n).each do |i|
(0...n - i - 1).each do |j|
array[j], array[j + 1] = array[j + 1], array[j] if array[j] > array[j + 1]
end
end
array
end
end
class QuickSort
include SortStrategy
def sort(array)
puts "Sorting using Quick Sort..."
return array if array.length <= 1
pivot = array[0]
left = array[1..-1].select { |x| x < pivot }
right = array[1..-1].select { |x| x >= pivot }
sort(left) + [pivot] + sort(right)
end
end
class DataSorter
def initialize(strategy)
@strategy = strategy
end
def execute(data)
@strategy.sort(data)
end
def change_strategy(strategy)
@strategy = strategy
end
end
# Usage
data = [64, 34, 25, 12, 22, 11, 90]
sorter = DataSorter.new(BubbleSort.new)
puts sorter.execute(data.dup).inspect
sorter.change_strategy(QuickSort.new)
puts sorter.execute(data.dup).inspect
Benefits
- Runtime Flexibility: Algorithms can be selected at runtime
- Code Isolation: Each algorithm is encapsulated in its own class
- Easy to Extend: New strategies can be added without modifying existing code
- Testability: Each strategy can be tested independently
Template Method vs. Strategy
| Aspect | Template Method | Strategy |
|---|---|---|
| Mechanism | Inheritance | Composition |
| When to use | Related algorithms sharing common structure | Interchangeable algorithms |
| Implementation | Subclasses override methods | Different classes implement interface |
| Change timing | Compile-time (class selection) | Runtime (object swap) |
3. Parameterized Types: Generic Programming
What are Parameterized Types?
Parameterized types (also called generics) allow you to write code that works with different data types while maintaining type safety. They enable you to create classes and functions that operate on various types specified as parameters.
C++ Templates
C++ uses templates to implement generics at compile-time:
#include <iostream>
#include <vector>
// Generic function template
template <typename T>
T max_value(T a, T b) {
return (a > b) ? a : b;
}
// Generic class template
template <typename T>
class Stack {
private:
std::vector<T> elements;
public:
void push(T value) {
elements.push_back(value);
}
T pop() {
T value = elements.back();
elements.pop_back();
return value;
}
bool is_empty() const {
return elements.empty();
}
};
int main() {
// Using template functions with different types
std::cout << "Max of 5 and 10: " << max_value(5, 10) << std::endl;
std::cout << "Max of 3.5 and 2.1: " << max_value(3.5, 2.1) << std::endl;
// Using template classes
Stack<int> intStack;
intStack.push(10);
intStack.push(20);
std::cout << "Popped: " << intStack.pop() << std::endl;
Stack<std::string> stringStack;
stringStack.push("Hello");
stringStack.push("World");
std::cout << "Popped: " << stringStack.pop() << std::endl;
return 0;
}
Key Features:
- Compile-time code generation: Compiler generates specific code for each type used
- Type safety: Type checking happens at compile time
- Zero runtime overhead: Generic code is instantiated for each type
- Template specialization: Can provide specific implementations for certain types
Ada Generics
Ada’s generics provide a similar mechanism but with a different syntax:
generic
type Item_Type is private;
Max_Length : Integer;
package Stacks is
type Stack_Type is limited private;
procedure Push(S : in out Stack_Type; Item : Item_Type);
procedure Pop(S : in out Stack_Type; Item : out Item_Type);
function Is_Empty(S : Stack_Type) return Boolean;
private
type Item_Array is array (1..Max_Length) of Item_Type;
type Stack_Type is record
Items : Item_Array;
Top : Integer := 0;
end record;
end Stacks;
Usage:
with Stacks;procedure Use_Integer_Stack is package Int_Stacks is new Stacks(Item_Type => Integer, Max_Length => 100); My_Stack : Int_Stacks.Stack_Type;begin Int_Stacks.Push(My_Stack, 42); -- ...end Use_Integer_Stack;
Ruby Generics (Runtime Polymorphism)
Ruby doesn’t have compile-time generics, but uses duck typing and metaprogramming:
# Ruby approach: Using blocks and duck typingclass Container def initialize @items = [] end def add(item) @items << item end def process(&block) @items.each { |item| block.call(item) } end def map(&block) @items.map { |item| block.call(item) } end def select(&block) @items.select { |item| block.call(item) } endend# Using with different typesint_container = Container.newint_container.add(1)int_container.add(2)int_container.add(3)puts "Original integers:"int_container.process { |x| puts x }puts "\nDoubled integers:"doubled = int_container.map { |x| x * 2 }puts doubled.inspectstring_container = Container.newstring_container.add("Hello")string_container.add("World")string_container.add("Ruby")puts "\nOriginal strings:"string_container.process { |s| puts s }puts "\nUppercased strings:"uppercased = string_container.map { |s| s.upcase }puts uppercased.inspect
Using Generic Patterns
# A more sophisticated generic-like pattern using modulesmodule Enumerable def filter_map(&block) map(&block).select { |item| !item.nil? } end def partition_by(&block) Hash.new { |h, k| h[k] = [] }.tap do |hash| each { |item| hash[block.call(item)] << item } end endendclass MyList include Enumerable def initialize(items) @items = items end def each(&block) @items.each(&block) end def map(&block) @items.map(&block) end def select(&block) @items.select(&block) endend# Usagenumbers = MyList.new([1, 2, 3, 4, 5, 6])evens = numbers.partition_by { |n| n.even? ? :even : :odd }puts evens.inspect
Benefits
- Type Safety: Errors caught at compile-time (in typed languages)
- Code Reuse: Write once for multiple types
- Performance: No runtime type checking overhead in compiled languages
- Expressiveness: Can write sophisticated data structures and algorithms
4. Object Aggregation and Acquaintance: Structuring Relationships
Understanding the Difference
Aggregation and acquaintance are two ways objects relate to each other in object-oriented design:
- Aggregation (has-a relationship): An object contains another object as a part of its structure. The contained object is a permanent part of the container.
- Acquaintance (uses-a relationship): An object temporarily knows about another object, typically passed as a parameter or obtained through a method call. The relationship is less permanent.
Aggregation Examples
Aggregation represents a “part-of” relationship where an object owns or contains other objects:
# Strong aggregation: Car owns its partsclass Engine def initialize(horsepower) @horsepower = horsepower end def start puts "Engine with #{@horsepower}hp starting..." end def stop puts "Engine stopping..." endendclass Wheel def initialize(size) @size = size end def rotate puts "#{@size}\" wheel rotating..." endendclass Car def initialize(make, model) @make = make @model = model # Aggregation: Car contains Engine and Wheels @engine = Engine.new(200) @wheels = [ Wheel.new(18), Wheel.new(18), Wheel.new(18), Wheel.new(18) ] end def start puts "Starting #{@make} #{@model}..." @engine.start end def drive @wheels.each(&:rotate) puts "Car is moving!" end def stop @engine.stop puts "Car stopped." endend# Usagecar = Car.new("Toyota", "Camry")car.startcar.drivecar.stop
Key characteristics of aggregation:
- The container creates/owns the contained objects
- The contained objects are part of the container’s structure
- Destroying the container may destroy the contained objects
- The relationship is relatively permanent
Acquaintance Examples
Acquaintance represents a “knows-about” relationship where objects interact but don’t own each other:
# Acquaintance: Order knows about Customer and Productclass Customer def initialize(name, email) @name = name @email = email end def name @name end def email @email endendclass Product def initialize(name, price) @name = name @price = price end def name @name end def price @price endendclass Order def initialize(order_id) @order_id = order_id @customer = nil # Acquaintance: will know about a customer @products = [] # Acquaintance: will know about products @total = 0 end # Receives a customer as a parameter def assign_customer(customer) @customer = customer puts "Order #{@order_id} assigned to #{customer.name}" end # Receives products as parameters def add_product(product, quantity = 1) @products << { product: product, quantity: quantity } @total += product.price * quantity end def display_summary puts "\n=== Order Summary ===" puts "Order ID: #{@order_id}" puts "Customer: #{@customer.name}" puts "Items:" @products.each do |item| puts " - #{item[:product].name}: $#{item[:product].price} x #{item[:quantity]}" end puts "Total: $#{@total}" endend# Usagecustomer = Customer.new("Alice Johnson", "alice@example.com")product1 = Product.new("Laptop", 999)product2 = Product.new("Mouse", 25)order = Order.new("ORD-001")order.assign_customer(customer)order.add_product(product1)order.add_product(product2, 2)order.display_summary
Key characteristics of acquaintance:
- Objects are passed as parameters or obtained through method calls
- The relationship is temporary and context-dependent
- Objects don’t create or own each other
- Objects can exist independently
Real-World Comparison: Restaurant System
# AGGREGATION: Restaurant owns its Menu and Tablesclass MenuItem def initialize(name, price) @name = name @price = price end def description "#{@name}: $#{@price}" endendclass Table def initialize(table_number, capacity) @table_number = table_number @capacity = capacity @is_occupied = false end def occupy @is_occupied = true end def free @is_occupied = false end def available? !@is_occupied endendclass Menu def initialize(cuisine_type) @cuisine_type = cuisine_type @items = [] end def add_item(item) @items << item end def list_items @items.map(&:description) endendclass Restaurant def initialize(name) @name = name # AGGREGATION: Restaurant owns these objects @menu = Menu.new("Italian") @tables = [ Table.new(1, 4), Table.new(2, 6), Table.new(3, 2) ] end def setup_menu @menu.add_item(MenuItem.new("Pasta Carbonara", 15)) @menu.add_item(MenuItem.new("Lasagna", 18)) @menu.add_item(MenuItem.new("Tiramisu", 8)) end def show_menu puts "=== #{@name} Menu ===" @menu.list_items.each { |item| puts item } end def reserve_table(party_size) available_table = @tables.find { |t| t.available? && t.capacity >= party_size } if available_table available_table.occupy "Table reserved!" else "No suitable tables available" end endend# ACQUAINTANCE: Reservation knows about Customer and Restaurantclass Reservation def initialize(reservation_id) @reservation_id = reservation_id @customer = nil @restaurant = nil @time = nil @party_size = nil end def make_reservation(customer, restaurant, time, party_size) @customer = customer @restaurant = restaurant @time = time @party_size = party_size puts "Reservation #{@reservation_id} made for #{customer.name} at #{time} for #{party_size} people" end def confirm puts "Confirming reservation for #{@customer.name}..." result = @restaurant.reserve_table(@party_size) puts result endend# Usagerestaurant = Restaurant.new("Luigi's Italian Kitchen")restaurant.setup_menurestaurant.show_menucustomer = Customer.new("Bob Smith", "bob@example.com")reservation = Reservation.new("RES-001")reservation.make_reservation(customer, restaurant, "7:00 PM", 4)reservation.confirm
When to Use Each
| Aspect | Aggregation | Acquaintance |
|---|---|---|
| Relationship | Part-of, owns | Uses, knows-about |
| Lifetime | Container controls | Independent |
| Creation | Container creates | External creation |
| Use Case | Car-Engine, House-Rooms | Customer-Order, Client-Service |
| Dependency | Strong coupling | Loose coupling |
Benefits
Aggregation:
- Clear ownership and lifecycle management
- Encapsulation of related components
- Simplified understanding of object structure
Acquaintance:
- Loose coupling between objects
- Better testability and modularity
- More flexible object interactions
- Easier to extend and modify
Putting It All Together: A Complete Example
Let’s create a library system that demonstrates all four concepts:
# TEMPLATE METHOD: Base class for different user typesclass LibraryUser def initialize(name) @name = name @borrowed_books = [] end def process_checkout(book) check_eligibility check_availability(book) checkout_book(book) send_confirmation(book) end protected def check_eligibility raise NotImplementedError end def check_availability(book) puts "Checking if #{book.title} is available..." end def checkout_book(book) @borrowed_books << book puts "Book checked out successfully" end def send_confirmation(book) puts "Sending confirmation to #{@name}" endendclass Student < LibraryUser def check_eligibility puts "Checking student ID and membership status..." end def send_confirmation(book) puts "Emailing confirmation to student: #{@name}" endendclass Faculty < LibraryUser def check_eligibility puts "Checking faculty status..." end def checkout_book(book) @borrowed_books << book puts "Faculty member can borrow up to 20 items" endend# AGGREGATION: Library owns Books and has Shelvesclass Book attr_reader :title, :author def initialize(title, author, isbn) @title = title @author = author @isbn = isbn @is_available = true end def available? @is_available end def checkout @is_available = false end def return_book @is_available = true endendclass Shelf def initialize(section, capacity) @section = section @capacity = capacity @books = [] end def add_book(book) @books << book if @books.length < @capacity end def list_books @books.map(&:title) endendclass Library def initialize(name) @name = name # AGGREGATION: Library owns shelves and manages books @shelves = { fiction: Shelf.new("Fiction", 100), science: Shelf.new("Science", 100), history: Shelf.new("History", 100) } @all_books = [] end def add_book(book, section) @all_books << book @shelves[section].add_book(book) end def find_book(title) @all_books.find { |book| book.title == title } endend# STRATEGY: Different checkout strategiesmodule CheckoutStrategy def apply_fee(days_borrowed) raise NotImplementedError endendclass StudentCheckoutStrategy include CheckoutStrategy def apply_fee(days_borrowed) days_borrowed > 14 ? days_borrowed - 14 * 0.25 : 0 endendclass FacultyCheckoutStrategy include CheckoutStrategy def apply_fee(days_borrowed) days_borrowed > 30 ? (days_borrowed - 30) * 0.10 : 0 endendclass LateFeesCalculator def initialize(strategy) @strategy = strategy end def calculate(days_borrowed) @strategy.apply_fee(days_borrowed) end def change_strategy(strategy) @strategy = strategy endend# ACQUAINTANCE: Loan connects User and Book temporarilyclass Loan def initialize(loan_id) @loan_id = loan_id @user = nil @book = nil @checkout_date = nil end def create_loan(user, book) @user = user @book = book @checkout_date = Date.today puts "Loan #{@loan_id}: #{user.class} borrowed '#{book.title}'" end def return_book days = (Date.today - @checkout_date).to_i fee_calculator = LateFeesCalculator.new(StudentCheckoutStrategy.new) fee = fee_calculator.calculate(days) puts "Book returned. Days borrowed: #{days}, Late fee: $#{fee}" endend# Usage demonstrationputs "=== Library Management System ==="# Setup library (aggregation)library = Library.new("City Public Library")book1 = Book.new("The Ruby Way", "Hal Fulton", "ISBN001")book2 = Book.new("Design Patterns", "Gang of Four", "ISBN002")library.add_book(book1, :science)library.add_book(book2, :fiction)# User checkout with template methodstudent = Student.new("John Doe")student.process_checkout(book1)faculty = Faculty.new("Dr. Smith")faculty.process_checkout(book2)# Loan with acquaintanceloan1 = Loan.new("LOAN001")loan1.create_loan(student, book1)loan1.return_book
Conclusion
These four concepts represent essential tools in the software architect’s toolkit:
- Template Method – Use inheritance to define algorithm structure
- Strategy – Use composition to swap algorithms at runtime
- Parameterized Types – Write generic code for multiple data types
- Aggregation/Acquaintance – Structure object relationships appropriately
Understanding when and how to apply each concept leads to more flexible, maintainable, and scalable software. Ruby’s flexibility makes these patterns particularly elegant to implement, though the principles apply across all modern programming languages.
The key is choosing the right tool for the right problem: use Template Method when you have variations of a fixed process, use Strategy for interchangeable algorithms, use generics for type-flexible code, and use appropriate aggregation/acquaintance patterns to structure your object relationships cleanly.
Happy Coding! ๐