Understanding Core Computer Language Concepts: Design Patterns, Polymorphism, and Object Relationships

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

AspectTemplate MethodStrategy
MechanismInheritanceComposition
When to useRelated algorithms sharing common structureInterchangeable algorithms
ImplementationSubclasses override methodsDifferent classes implement interface
Change timingCompile-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 typing
class 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) }
end
end
# Using with different types
int_container = Container.new
int_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.inspect
string_container = Container.new
string_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 modules
module 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
end
end
class 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)
end
end
# Usage
numbers = 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 parts
class Engine
def initialize(horsepower)
@horsepower = horsepower
end
def start
puts "Engine with #{@horsepower}hp starting..."
end
def stop
puts "Engine stopping..."
end
end
class Wheel
def initialize(size)
@size = size
end
def rotate
puts "#{@size}\" wheel rotating..."
end
end
class 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."
end
end
# Usage
car = Car.new("Toyota", "Camry")
car.start
car.drive
car.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 Product
class Customer
def initialize(name, email)
@name = name
@email = email
end
def name
@name
end
def email
@email
end
end
class Product
def initialize(name, price)
@name = name
@price = price
end
def name
@name
end
def price
@price
end
end
class 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}"
end
end
# Usage
customer = 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 Tables
class MenuItem
def initialize(name, price)
@name = name
@price = price
end
def description
"#{@name}: $#{@price}"
end
end
class 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
end
end
class Menu
def initialize(cuisine_type)
@cuisine_type = cuisine_type
@items = []
end
def add_item(item)
@items << item
end
def list_items
@items.map(&:description)
end
end
class 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
end
end
# ACQUAINTANCE: Reservation knows about Customer and Restaurant
class 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
end
end
# Usage
restaurant = Restaurant.new("Luigi's Italian Kitchen")
restaurant.setup_menu
restaurant.show_menu
customer = 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

AspectAggregationAcquaintance
RelationshipPart-of, ownsUses, knows-about
LifetimeContainer controlsIndependent
CreationContainer createsExternal creation
Use CaseCar-Engine, House-RoomsCustomer-Order, Client-Service
DependencyStrong couplingLoose 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 types
class 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}"
end
end
class Student < LibraryUser
def check_eligibility
puts "Checking student ID and membership status..."
end
def send_confirmation(book)
puts "Emailing confirmation to student: #{@name}"
end
end
class 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"
end
end
# AGGREGATION: Library owns Books and has Shelves
class 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
end
end
class 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)
end
end
class 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 }
end
end
# STRATEGY: Different checkout strategies
module CheckoutStrategy
def apply_fee(days_borrowed)
raise NotImplementedError
end
end
class StudentCheckoutStrategy
include CheckoutStrategy
def apply_fee(days_borrowed)
days_borrowed > 14 ? days_borrowed - 14 * 0.25 : 0
end
end
class FacultyCheckoutStrategy
include CheckoutStrategy
def apply_fee(days_borrowed)
days_borrowed > 30 ? (days_borrowed - 30) * 0.10 : 0
end
end
class LateFeesCalculator
def initialize(strategy)
@strategy = strategy
end
def calculate(days_borrowed)
@strategy.apply_fee(days_borrowed)
end
def change_strategy(strategy)
@strategy = strategy
end
end
# ACQUAINTANCE: Loan connects User and Book temporarily
class 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}"
end
end
# Usage demonstration
puts "=== 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 method
student = Student.new("John Doe")
student.process_checkout(book1)
faculty = Faculty.new("Dr. Smith")
faculty.process_checkout(book2)
# Loan with acquaintance
loan1 = Loan.new("LOAN001")
loan1.create_loan(student, book1)
loan1.return_book

Conclusion

These four concepts represent essential tools in the software architect’s toolkit:

  1. Template Method – Use inheritance to define algorithm structure
  2. Strategy – Use composition to swap algorithms at runtime
  3. Parameterized Types – Write generic code for multiple data types
  4. 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! ๐Ÿš€

Unknown's avatar

Author: Abhilash

Hi, Iโ€™m Abhilash! A seasoned web developer with 15 years of experience specializing in Ruby and Ruby on Rails. Since 2010, Iโ€™ve built scalable, robust web applications and worked with frameworks like Angular, Sinatra, Laravel, Node.js, Vue and React. Passionate about clean, maintainable code and continuous learning, I share insights, tutorials, and experiences here. Letโ€™s explore the ever-evolving world of web development together!

Leave a comment