• Что бы вступить в ряды "Принятый кодер" Вам нужно:
    Написать 10 полезных сообщений или тем и Получить 10 симпатий.
    Для того кто не хочет терять время,может пожертвовать средства для поддержки сервеса, и вступить в ряды VIP на месяц, дополнительная информация в лс.

  • Пользаватели которые будут спамить, уходят в бан без предупреждения. Спам сообщения определяется администрацией и модератором.

  • Гость, Что бы Вы хотели увидеть на нашем Форуме? Изложить свои идеи и пожелания по улучшению форума Вы можете поделиться с нами здесь. ----> Перейдите сюда
  • Все пользователи не прошедшие проверку электронной почты будут заблокированы. Все вопросы с разблокировкой обращайтесь по адресу электронной почте : info@guardianelinks.com . Не пришло сообщение о проверке или о сбросе также сообщите нам.

Unveiling the Magic of Object-Oriented Programming with Ruby

Lomanu4 Оффлайн

Lomanu4

Команда форума
Администратор
Регистрация
1 Мар 2015
Сообщения
1,481
Баллы
155
Welcome, aspiring code wizards and seasoned developers alike! Today, we embark on an exciting journey into the heart of Ruby, a language renowned for its elegance, developer-friendliness, and its pure embrace of Object-Oriented Programming (OOP). If you've ever wondered how to build robust, maintainable, and intuitive software, OOP with Ruby is a fantastic place to start or deepen your understanding.

What's OOP, Anyway? And Why Ruby?


At its core, Object-Oriented Programming is a paradigm based on the concept of "objects". These objects can contain data, in the form of fields (often known as attributes or properties), and code, in the form of procedures (often known as methods). The main idea is to bundle data and the methods that operate on that data into a single unit.

Why is this cool?

  • Modularity: OOP helps you break down complex problems into smaller, manageable, and self-contained objects.
  • Reusability: Write a class once, create many objects from it. Use inheritance to reuse code from existing classes.
  • Maintainability: Changes in one part of the system are less likely to break other parts.
  • Real-world Mapping: OOP allows you to model real-world entities and their interactions more intuitively.

And why Ruby? Ruby is not just an OOP language; it's purely object-oriented. In Ruby, everything is an object, from numbers and strings to classes themselves. This consistent model makes OOP concepts feel natural and deeply integrated.

Let's dive in!

The Building Blocks: Classes & Objects


The fundamental concepts in OOP are classes and objects.

  • A Class is a blueprint or template for creating objects. It defines a set of attributes and methods that the objects created from the class will have.
  • An Object is an instance of a class. It's a concrete entity that has its own state (values for its attributes) and can perform actions (through its methods).
Anatomy of a Class


In Ruby, you define a class using the class keyword, followed by the class name (which should start with a capital letter), and an end keyword.


# class_definition.rb
# This is a blueprint for creating "Dog" objects.
class Dog
# We'll add more here soon!
end

Simple, right? We've just defined a class named Dog. It doesn't do much yet, but it's a valid class.

Crafting Objects (Instantiation)


To create an object (an instance) from a class, you call the .new method on the class.


# object_instantiation.rb
class Dog
# ...
end

# Create two Dog objects
fido = Dog.new
buddy = Dog.new

puts fido # Output: #<Dog:0x00007f9b1a0b3d40> (your object ID will vary)
puts buddy # Output: #<Dog:0x00007f9b1a0b3d18>

fido and buddy are now two distinct objects, both instances of the Dog class.

Instance Variables: An Object's Memory


Objects need to store their own data. This data is held in instance variables. In Ruby, instance variables are prefixed with an @ symbol. They belong to a specific instance of a class.


# instance_variables.rb
class Dog
def set_name(name)
@name = name # @name is an instance variable
end

def get_name
@name
end
end

fido = Dog.new
fido.set_name("Fido")
buddy = Dog.new
buddy.set_name("Buddy")

puts fido.get_name # Output: Fido
puts buddy.get_name # Output: Buddy

Here, fido's @name is "Fido", and buddy's @name is "Buddy". They each have their own copy of the @name instance variable. If you try to access @name before it's set, it will return nil.

The initialize Method: A Grand Welcome


Often, you want to set up an object's initial state when it's created. Ruby provides a special method called initialize for this purpose. It's like a constructor in other languages. The initialize method is called automatically when you use Class.new.


# initialize_method.rb
class Dog
def initialize(name, breed)
@name = name # Instance variable
@breed = breed # Instance variable
puts "#{@name} the #{@breed} says: Woof! I'm alive!"
end

def get_name
@name
end

def get_breed
@breed
end
end

# Now we pass arguments when creating objects
fido = Dog.new("Fido", "Golden Retriever")
# Output: Fido the Golden Retriever says: Woof! I'm alive!
sparky = Dog.new("Sparky", "Poodle")
# Output: Sparky the Poodle says: Woof! I'm alive!

puts "#{fido.get_name} is a #{fido.get_breed}." # Output: Fido is a Golden Retriever.
puts "#{sparky.get_name} is a #{sparky.get_breed}." # Output: Sparky is a Poodle.
Instance Methods: What Objects Can Do


Methods defined within a class are called instance methods. They define the behavior of the objects created from that class. They can access and modify the object's instance variables.


# instance_methods.rb
class Dog
def initialize(name)
@name = name
@tricks_learned = 0
end

def bark
puts "#{@name} says: Woof woof!"
end

def learn_trick(trick_name)
@tricks_learned += 1
puts "#{@name} learned to #{trick_name}!"
end

def show_off
puts "#{@name} knows #{@tricks_learned} trick(s)."
end
end

fido = Dog.new("Fido")
fido.bark # Output: Fido says: Woof woof!
fido.learn_trick("sit") # Output: Fido learned to sit!
fido.learn_trick("roll over") # Output: Fido learned to roll over!
fido.show_off # Output: Fido knows 2 trick(s).
Accessors: Controlled Gates to Data


Directly accessing instance variables from outside the class is generally not good practice (it breaks encapsulation, which we'll discuss soon). Instead, we use accessor methods.

Ruby provides convenient shortcuts for creating these:

  • attr_reader :variable_name: Creates a getter method.
  • attr_writer :variable_name: Creates a setter method.
  • attr_accessor :variable_name: Creates both a getter and a setter method.

These take symbols as arguments, representing the instance variable names (without the @).


# accessor_methods.rb
class Cat
# Creates getter for @name and getter/setter for @age
attr_reader :name
attr_accessor :age

def initialize(name, age)
@name = name # Can't be changed after initialization due to attr_reader
@age = age
end

def birthday
@age += 1
puts "Happy Birthday! #{@name} is now #{@age}."
end
end

whiskers = Cat.new("Whiskers", 3)
puts whiskers.name # Output: Whiskers (using getter)
puts whiskers.age # Output: 3 (using getter)
whiskers.age = 4 # Using setter
puts whiskers.age # Output: 4
# whiskers.name = "Mittens" # This would cause an error: NoMethodError (undefined method 'name='...)
# because :name is only an attr_reader
whiskers.birthday # Output: Happy Birthday! Whiskers is now 5.
The Four Pillars of OOP in Ruby


OOP is often described as standing on four main pillars: Encapsulation, Inheritance, Polymorphism, and Abstraction (though Abstraction is often seen as a result of the other three, especially Encapsulation). Let's explore them in the context of Ruby.

1. Encapsulation: The Art of Hiding


Encapsulation is about bundling the data (attributes) and the methods that operate on that data within a single unit (the object). It also involves restricting direct access to some of an object's components, which is known as information hiding.

Why?

  • Control: You control how the object's data is accessed and modified, preventing accidental or unwanted changes.
  • Flexibility: You can change the internal implementation of a class without affecting the code that uses it, as long as the public interface remains the same.
  • Simplicity: Users of your class only need to know about its public interface, not its internal complexities.

Ruby provides three levels of access control for methods:

  • public: Methods are public by default (except initialize, which is effectively private). Public methods can be called by anyone.
  • private: Private methods can only be called from within the defining class, and importantly, only without an explicit receiver. This means you can't do self.private_method (unless it's a setter defined by attr_writer). They are typically helper methods for the internal workings of the class.
  • protected: Protected methods can be called by any instance of the defining class or its subclasses. Unlike private methods, they can be called with an explicit receiver (e.g., other_object.protected_method) as long as other_object is an instance of the same class or a subclass.

Here's an example demonstrating encapsulation with a BankAccount class:


# encapsulation_access_control.rb
class BankAccount
attr_reader :account_number, :holder_name

def initialize(account_number, holder_name, initial_balance)
@account_number = account_number
@holder_name = holder_name
@balance = initial_balance
end

def deposit(amount)
if amount > 0
@balance += amount
log_transaction("Deposited #{amount}")
puts "Deposited #{amount}. New balance: #{@balance}"
else
puts "Deposit amount must be positive."
end
end

def withdraw(amount)
if can_withdraw?(amount)
@balance -= amount
log_transaction("Withdrew #{amount}")
puts "Withdrew #{amount}. New balance: #{@balance}"
else
puts "Insufficient funds."
end
end

def display_balance
puts "Current balance for #{@holder_name}: #{@balance}"
end

def transfer_to(other_account, amount)
if amount > 0 && can_withdraw?(amount)
puts "Attempting to transfer #{amount} to #{other_account.holder_name}"
self.withdraw_for_transfer(amount)
other_account.deposit_from_transfer(amount)
log_transaction("Transferred #{amount} to account #{other_account.account_number}")
puts "Transfer successful."
else
puts "Transfer failed."
end
end

protected

def deposit_from_transfer(amount)
@balance += amount
log_transaction("Received transfer: #{amount}")
end

def withdraw_for_transfer(amount)
@balance -= amount
log_transaction("Initiated transfer withdrawal: #{amount}")
end

private

def can_withdraw?(amount)
@balance >= amount
end

def log_transaction(message)
puts "[LOG] Account #{@account_number}: #{message}"
end
end

# --- Usage ---
acc1 = BankAccount.new("12345", "Alice", 1000)
acc2 = BankAccount.new("67890", "Bob", 500)

acc1.display_balance # Output: Current balance for Alice: 1000
acc1.deposit(200) # Output: Deposited 200. New balance: 1200
acc1.withdraw(50) # Output: Withdrew 50. New balance: 1150

# acc1.log_transaction("Oops") # Error: private method 'log_transaction' called
# acc1.can_withdraw?(50) # Error: private method 'can_withdraw?' called

acc1.transfer_to(acc2, 100)
# Output:
# Attempting to transfer 100 to Bob
# [LOG] Account 12345: Initiated transfer withdrawal: 100
# [LOG] Account 67890: Received transfer: 100
# [LOG] Account 12345: Transferred 100 to account 67890
# Transfer successful.

acc1.display_balance # Output: Current balance for Alice: 1050
acc2.display_balance # Output: Current balance for Bob: 600
2. Inheritance: Standing on the Shoulders of Giants


Inheritance allows a class (the subclass or derived class) to inherit attributes and methods from another class (the superclass or base class). This promotes code reuse and establishes an "is-a" relationship (e.g., a Dog is an Animal).

In Ruby, you denote inheritance using the < symbol.


# inheritance.rb
class Animal
attr_reader :name

def initialize(name)
@name = name
end

def speak
raise NotImplementedError, "Subclasses must implement the 'speak' method."
end

def eat(food)
puts "#{@name} is eating #{food}."
end
end

class Dog < Animal # Dog inherits from Animal
attr_reader :breed

def initialize(name, breed)
super(name) # Calls the 'initialize' method of the superclass (Animal)
@breed = breed
end

# Overriding the 'speak' method from Animal
def speak
puts "#{@name} the #{@breed} says: Woof!"
end

def fetch(item)
puts "#{@name} fetches the #{item}."
end
end

class Cat < Animal # Cat inherits from Animal
def initialize(name, fur_color)
super(name)
@fur_color = fur_color
end

# Overriding the 'speak' method
def speak
puts "#{@name} the cat with #{@fur_color} fur says: Meow!"
end

def purr
puts "#{@name} purrs contentedly."
end
end

# --- Usage ---
generic_animal = Animal.new("Creature")
# generic_animal.speak # This would raise NotImplementedError

fido = Dog.new("Fido", "Labrador")
fido.eat("kibble") # Inherited from Animal. Output: Fido is eating kibble.
fido.speak # Overridden in Dog. Output: Fido the Labrador says: Woof!
fido.fetch("ball") # Defined in Dog. Output: Fido fetches the ball.

mittens = Cat.new("Mittens", "tabby")
mittens.eat("fish") # Inherited. Output: Mittens is eating fish.
mittens.speak # Overridden. Output: Mittens the cat with tabby fur says: Meow!
mittens.purr # Defined in Cat. Output: Mittens purrs contentedly.

puts "#{fido.name} is a Dog."
puts "#{mittens.name} is a Cat."
puts "Is fido an Animal? #{fido.is_a?(Animal)}" # Output: true
puts "Is fido a Cat? #{fido.is_a?(Cat)}" # Output: false
puts "Is fido a Dog? #{fido.is_a?(Dog)}" # Output: true

Key points about inheritance:

  • super: The super keyword calls the method with the same name in the superclass.
    • super (with no arguments): Passes all arguments received by the current method to the superclass method.
    • super() (with empty parentheses): Calls the superclass method with no arguments.
    • super(arg1, arg2): Calls the superclass method with specific arguments.
  • Method Overriding: Subclasses can provide a specific implementation for a method that is already defined in its superclass.
  • Liskov Substitution Principle (LSP): An important principle related to inheritance. It states that objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program. This means subclasses should extend, not fundamentally alter, the behavior of their superclasses.
3. Polymorphism: Many Forms, One Interface


Polymorphism, from Greek meaning "many forms," allows objects of different classes to respond to the same message (method call) in different ways.

Duck Typing in Ruby


Ruby is famous for "duck typing." The idea is: "If it walks like a duck and quacks like a duck, then it must be a duck." In other words, Ruby doesn't care so much about an object's class, but rather about what methods it can respond to.


# polymorphism_duck_typing.rb
class Journalist
def write_article
puts "Journalist: Writing a compelling news story..."
end
end

class Blogger
def write_article
puts "Blogger: Crafting an engaging blog post..."
end
end

class Novelist
def write_masterpiece
puts "Novelist: Weaving an epic tale..."
end

def write_article
puts "Novelist: Penning a thoughtful essay for a magazine."
end
end

def publish_content(writers)
writers.each do |writer|
# We don't care about the class, only if it can 'write_article'
if writer.respond_to?(:write_article)
writer.write_article
else
puts "#{writer.class} cannot write an article in the conventional sense."
end
end
end

writers_list = [Journalist.new, Blogger.new, Novelist.new]
publish_content(writers_list)
# Output:
# Journalist: Writing a compelling news story...
# Blogger: Crafting an engaging blog post...
# Novelist: Penning a thoughtful essay for a magazine.
Polymorphism via Inheritance


This is what we saw with the Animal, Dog, and Cat example. Each animal speaks differently.


# polymorphism_inheritance.rb
class Animal
attr_reader :name

def initialize(name)
@name = name
end

def speak
raise NotImplementedError, "Subclasses must implement the 'speak' method."
end
end

class Dog < Animal
def speak
puts "#{@name} says: Woof!"
end
end

class Cat < Animal
def speak
puts "#{@name} says: Meow!"
end
end

class Cow < Animal
def speak
puts "#{@name} says: Mooo!"
end
end

animals = [Dog.new("Buddy"), Cat.new("Whiskers"), Cow.new("Bessie")]
animals.each do |animal|
animal.speak # Each animal responds to 'speak' in its own way
end
# Output:
# Buddy says: Woof!
# Whiskers says: Meow!
# Bessie says: Mooo!

Here, we can treat Dog, Cat, and Cow objects uniformly as Animals and call speak on them, and the correct version of speak is executed.

Beyond the Pillars: Advanced Ruby OOP


Ruby's OOP capabilities extend further, offering powerful tools for flexible and expressive code.

Modules: Ruby's Swiss Army Knife


Modules in Ruby serve two primary purposes:

  1. Namespacing: Grouping related classes, methods, and constants to prevent name collisions.
  2. Mixins: Providing a collection of methods that can be "mixed into" classes, adding behavior without using inheritance. This is Ruby's way of achieving multiple inheritance-like features.
Mixins: Adding Behavior (include)


When a module is included in a class, its methods become instance methods of that class.


# modules_mixins.rb
module Swimmable
def swim
puts "#{name_for_action} is swimming!"
end
end

module Walkable
def walk
puts "#{name_for_action} is walking!"
end
end

# A helper method that classes using these modules should implement
module ActionNameable
def name_for_action
# Default implementation, can be overridden by the class
self.respond_to?(:name) ? self.name : self.class.to_s
end
end

class Fish
include Swimmable
include ActionNameable
# Provides name_for_action
attr_reader :name

def initialize(name)
@name = name
end
end

class Dog
include Swimmable
include Walkable
include ActionNameable
attr_reader :name

def initialize(name)
@name = name
end
end

class Robot
include Walkable # Robots can walk, but not swim (usually!)
include ActionNameable

def name_for_action # Overriding for specific robot naming
"Unit 734"
end
end

nemo = Fish.new("Nemo")
nemo.swim # Output: Nemo is swimming!
# nemo.walk # Error: NoMethodError

buddy = Dog.new("Buddy")
buddy.swim # Output: Buddy is swimming!
buddy.walk # Output: Buddy is walking!

bot = Robot.new
bot.walk # Output: Unit 734 is walking!

The Enumerable module is a classic example of a mixin in Ruby's standard library. If your class implements an each method and includes Enumerable, you get a wealth of iteration methods (map, select, reject, sort_by, etc.) for free!

Namespacing: Keeping Things Tidy


Modules can also be used to organize your code and prevent naming conflicts.


# modules_namespacing.rb
module SportsAPI
class Player
def initialize(name)
@name = name
puts "SportsAPI Player #{@name} created."
end
end

module Football
class Player # This is SportsAPI:?:Player
def initialize(name, team)
@name = name
@team = team
puts "Football Player #{@name} of #{@team} created."
end
end
end
end

module MusicApp
class Player # This is MusicApp::Player
def initialize(song)
@song = song
puts "MusicApp Player playing #{@song}."
end
end
end

player1 = SportsAPI::Player.new("John Doe")
# Output: SportsAPI Player John Doe created.

player2 = SportsAPI:?:Player.new("Leo Messi", "Inter Miami")
# Output: Football Player Leo Messi of Inter Miami created.

player3 = MusicApp::Player.new("Bohemian Rhapsody")
# Output: MusicApp Player playing Bohemian Rhapsody.
Blocks, Procs, and Lambdas: Objects of Behavior


In Ruby, blocks are chunks of code that can be passed to methods. Procs and lambdas are objects that encapsulate these blocks of code, allowing them to be stored in variables, passed around, and executed later. They are a key part of Ruby's functional programming flavor and interact deeply with OOP.

  • Blocks: Not objects themselves, but can be converted to Proc objects. Often used for iteration or customizing method behavior.
  • Procs (Proc.new or proc {}): Objects that represent a block of code. They have "lenient arity" (don't strictly check the number of arguments) and return from the context where they are defined.
  • Lambdas (lambda {} or -> {}): Also Proc objects, but with "strict arity" (raise an error if the wrong number of arguments is passed) and return from the lambda itself, not the enclosing method.

# blocks_procs_lambdas.rb
# Method that accepts a block
def custom_iterator(items)
puts "Starting iteration..."
items.each do |item|
yield item # 'yield' executes the block passed to the method
end
puts "Iteration finished."
end

my_array = [1, 2, 3]
custom_iterator(my_array) do |number|
puts "Processing item: #{number * 10}"
end
# Output:
# Starting iteration...
# Processing item: 10
# Processing item: 20
# Processing item: 30
# Iteration finished.

# --- Procs ---
my_proc = Proc.new { |name| puts "Hello from Proc, #{name}!" }
my_proc.call("Alice") # Output: Hello from Proc, Alice!
my_proc.call # Output: Hello from Proc, ! (lenient arity, name is nil)

# Proc with return
def proc_test
p = Proc.new { return "Returned from Proc inside proc_test" }
p.call
return "Returned from proc_test method" # This line is never reached
end
puts proc_test # Output: Returned from Proc inside proc_test

# --- Lambdas ---
my_lambda = lambda { |name| puts "Hello from Lambda, #{name}!" }
# Alternative syntax: my_lambda = ->(name) { puts "Hello from Lambda, #{name}!" }
my_lambda.call("Bob") # Output: Hello from Lambda, Bob!
# my_lambda.call # ArgumentError: wrong number of arguments (given 0, expected 1) (strict arity)

# Lambda with return
def lambda_test
l = lambda { return "Returned from Lambda" }
result = l.call
puts "Lambda call result: #{result}"
return "Returned from lambda_test method" # This line IS reached
end
puts lambda_test
# Output:
# Lambda call result: Returned from Lambda
# Returned from lambda_test method

Blocks, Procs, and Lambdas allow you to pass behavior as arguments, which is incredibly powerful for creating flexible and reusable methods and classes.

Metaprogramming: Ruby Talking to Itself (A Glimpse)


Metaprogramming is writing code that writes code, or code that modifies itself or other code at runtime. Ruby's dynamic nature makes it exceptionally well-suited for metaprogramming. This is an advanced topic, but here's a tiny taste:

  • send: Allows you to call a method by its name (as a string or symbol).
  • define_method: Allows you to create methods dynamically.

# metaprogramming_glimpse.rb
class Greeter
def say_hello
puts "Hello!"
end
end

g = Greeter.new
g.send(:say_hello) # Output: Hello! (Same as g.say_hello)

class DynamicHelper
# Dynamically define methods for each attribute
['name', 'email', 'city'].each do |attribute|
define_method("get_#{attribute}") do
instance_variable_get("@#{attribute}")
end
define_method("set_#{attribute}") do |value|
instance_variable_set("@#{attribute}", value)
puts "Set #{attribute} to #{value}"
end
end

def initialize(name, email, city)
@name = name
@email = email
@city = city
end
end

helper = DynamicHelper.new("Jane Doe", "jane@example.com", "New York")
helper.set_email("jane.d@example.com") # Output: Set email to jane.d@example.com
puts helper.get_email # Output: jane.d@example.com
puts helper.get_name # Output: Jane Doe

Metaprogramming is powerful but can make code harder to understand and debug if overused. Use it judiciously!

Common Design Patterns in Ruby


Design patterns are reusable solutions to commonly occurring problems within a given context in software design. Ruby's features often provide elegant ways to implement these patterns.

Singleton


Ensures a class only has one instance and provides a global point of access to it. Ruby has a Singleton module.


# design_pattern_singleton.rb
require 'singleton'

class ConfigurationManager
include Singleton # Makes this class a Singleton
attr_accessor :setting

def initialize
# Load configuration (e.g., from a file)
@setting = "Default Value"
puts "ConfigurationManager initialized."
end
end

config1 = ConfigurationManager.instance
config2 = ConfigurationManager.instance

puts "config1 object_id: #{config1.object_id}"
puts "config2 object_id: #{config2.object_id}" # Same as config1
# Output: ConfigurationManager initialized. (only once)
# Output: config1 object_id:...
# Output: config2 object_id:... (same as above)

config1.setting = "New Value"
puts config2.setting # Output: New Value
Decorator


Adds new responsibilities to an object dynamically. Ruby's modules and SimpleDelegator can be used.


# design_pattern_decorator.rb
require 'delegate' # For SimpleDelegator

class SimpleCoffee
def cost
10
end

def description
"Simple coffee"
end
end

# Decorator base class (optional, but good practice)
class CoffeeDecorator < SimpleDelegator
def initialize(coffee)
super(coffee) # Delegates methods to the wrapped coffee object
@component = coffee
end

def cost
@component.cost
end

def description
@component.description
end
end

class MilkDecorator < CoffeeDecorator
def cost
super + 2 # Add cost of milk
end

def description
super + ", milk"
end
end

class SugarDecorator < CoffeeDecorator
def cost
super + 1 # Add cost of sugar
end

def description
super + ", sugar"
end
end

my_coffee = SimpleCoffee.new
puts "#{my_coffee.description} costs #{my_coffee.cost}"
# Output: Simple coffee costs 10

milk_coffee = MilkDecorator.new(my_coffee)
puts "#{milk_coffee.description} costs #{milk_coffee.cost}"
# Output: Simple coffee, milk costs 12

sweet_milk_coffee = SugarDecorator.new(milk_coffee)
puts "#{sweet_milk_coffee.description} costs #{sweet_milk_coffee.cost}"
# Output: Simple coffee, milk, sugar costs 13

# You can also wrap directly
super_coffee = SugarDecorator.new(MilkDecorator.new(SimpleCoffee.new))
puts "#{super_coffee.description} costs #{super_coffee.cost}"
# Output: Simple coffee, milk, sugar costs 13
Strategy


Defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.


# design_pattern_strategy.rb
class Report
attr_reader :title, :text
attr_accessor :formatter

def initialize(title, text, formatter)
@title = title
@text = text
@formatter = formatter
end

def output_report
@formatter.output_report(self) # Delegate to the strategy
end
end

class HTMLFormatter
def output_report(report_context)
puts "<html><head><title>#{report_context.title}</title></head><body>"
report_context.text.each { |line| puts "<p>#{line}</p>" }
puts "</body></html>"
end
end

class PlainTextFormatter
def output_report(report_context)
puts "*** #{report_context.title} ***"
report_context.text.each { |line| puts line }
end
end

report_data = ["This is line 1.", "This is line 2.", "Conclusion."]
html_report = Report.new("Monthly Report", report_data, HTMLFormatter.new)
html_report.output_report
# Output:
# <html><head><title>Monthly Report</title></head><body>
# <p>This is line 1.</p>
# <p>This is line 2.</p>
# <p>Conclusion.</p>
# </body></html>

puts "\n--- Changing strategy ---\n"
plain_text_report = Report.new("Weekly Update", report_data, PlainTextFormatter.new)
plain_text_report.output_report
# Output:
# *** Weekly Update ***
# This is line 1.
# This is line 2.
# Conclusion.

# We can even change the strategy on an existing object
puts "\n--- Changing strategy on html_report ---\n"
html_report.formatter = PlainTextFormatter.new
html_report.output_report
# Output:
# *** Monthly Report ***
# This is line 1.
# This is line 2.
# Conclusion.
A Practical Example: Building a Mini Adventure Game


Let's tie some of these concepts together with a very simple text-based adventure game.

Modules for Capabilities


We start by defining modules for capabilities that can be mixed into different classes.


module Describable
attr_accessor :description

def look
description || "There's nothing particularly interesting about it."
end
end

module Carryable
attr_accessor :weight

def pickup_text
"You pick up the #{name}."
end

def drop_text
"You drop the #{name}."
end
end
GameItem Class


Next, we define a base class for game items, which includes the Describable module.


class GameItem
include Describable

attr_reader :name

def initialize(name, description)
@name = name
self.description = description
end
end
Weapon and Potion Classes


We can create subclasses for specific types of items, like weapons and potions.


class Weapon < GameItem
include Carryable # Weapons can be carried
attr_reader :damage

def initialize(name, description, damage, weight)
super(name, description)
@damage = damage
@weight = weight
end

def attack_text(target_name)
"#{name} attacks #{target_name} for #{@damage} damage!"
end
end

class Potion < GameItem
include Carryable # Potions can be carried
attr_reader :heal_amount

def initialize(name, description, heal_amount, weight)
super(name, description)
@heal_amount = heal_amount
@weight = weight
end

def drink_text(drinker_name)
"#{drinker_name} drinks the #{name} and recovers #{heal_amount} health."
end
end
Scenery Class


For items that are part of the environment and can't be carried.


class Scenery < GameItem
# Scenery is just describable, not carryable
def initialize(name, description)
super(name, description)
end
end
Character Class


The Character class represents both the player and NPCs, with methods for health management, inventory, and actions.


class Character
include Describable # Characters can be described
attr_reader :name, :max_hp
attr_accessor :hp, :current_room, :inventory

def initialize(name, description, hp)
@name = name
@description = description
@hp = hp
@max_hp = hp
@inventory = []
@current_room = nil
end

def alive?
@hp > 0
end

def take_damage(amount)
@hp -= amount
@hp = 0 if @hp < 0
puts "#{name} takes #{amount} damage. Current HP: #{@hp}."
die unless alive?
end

def heal(amount)
@hp += amount
@hp = @max_hp if @hp > @max_hp
puts "#{name} heals for #{amount}. Current HP: #{@hp}."
end

def die
puts "#{name} has been defeated!"
end

def add_to_inventory(item)
if item.is_a?(Carryable)
inventory << item
puts item.pickup_text
else
puts "You can't carry the #{item.name}."
end
end

def drop_from_inventory(item_name)
item = inventory.find { |i| i.name.downcase == item_name.downcase }
if item
inventory.delete(item)
current_room.items << item # Drop it in the current room
puts item.drop_text
else
puts "You don't have a #{item_name}."
end
end

def display_inventory
if inventory.empty?
puts "#{name}'s inventory is empty."
else
puts "#{name}'s inventory:"
inventory.each { |item| puts "- #{item.name} (#{item.description})" }
end
end

def attack(target, weapon)
if weapon.is_a?(Weapon) && inventory.include?(weapon)
puts weapon.attack_text(target.name)
target.take_damage(weapon.damage)
elsif !inventory.include?(weapon)
puts "You don't have the #{weapon.name} in your inventory."
else
puts "You can't attack with the #{weapon.name}."
end
end

def drink_potion(potion)
if potion.is_a?(Potion) && inventory.include?(potion)
puts potion.drink_text(self.name)
heal(potion.heal_amount)
inventory.delete(potion) # Potion is consumed
elsif !inventory.include?(potion)
puts "You don't have the #{potion.name} in your inventory."
else
puts "You can't drink the #{potion.name}."
end
end
end
Room Class


Rooms contain items and characters and have exits to other rooms.


class Room
include Describable
attr_accessor :items, :characters
attr_reader :name, :exits

def initialize(name, description)
super() # For Describable
@name = name
self.description = description # Use setter from Describable
@exits = {} # direction => room_object
@items = []
@characters = []
end

def add_exit(direction, room)
@exits[direction.to_sym] = room
end

def full_description
output = ["--- #{name} ---"]
output << description
output << "Items here: #{items.map(&:name).join(', ')}" if items.any?
output << "Others here: #{characters.reject { |c| c.is_a?(Player) }.map(&:name).join(', ')}" if characters.any? { |c| !c.is_a?(Player) }
output << "Exits: #{exits.keys.join(', ')}"
output.join("\n")
end
end
Player Class


The Player class inherits from Character and adds methods for movement and interaction.


class Player < Character
def initialize(name, description, hp)
super(name, description, hp)
end

def move(direction)
if current_room.exits[direction.to_sym]
self.current_room.characters.delete(self)
self.current_room = current_room.exits[direction.to_sym]
self.current_room.characters << self
puts "You move #{direction}."
puts current_room.full_description
else
puts "You can't go that way."
end
end

def look_around
puts current_room.full_description
end

def look_at(target_name)
# Check items in room or inventory, or characters in room
target = current_room.items.find { |i| i.name.downcase == target_name.downcase } ||
inventory.find { |i| i.name.downcase == target_name.downcase } ||
current_room.characters.find { |c| c.name.downcase == target_name.downcase } ||
(current_room.name.downcase == target_name.downcase ? current_room : nil)
if target
puts target.look
else
puts "You don't see a '#{target_name}' here."
end
end

def take_item(item_name)
item = current_room.items.find { |i| i.name.downcase == item_name.downcase }
if item
if item.is_a?(Carryable)
current_room.items.delete(item)
add_to_inventory(item)
else
puts "You can't take the #{item.name}."
end
else
puts "There is no '#{item_name}' here to take."
end
end
end
Game Setup and Loop


Finally, we set up the game world and implement a simple game loop for user interaction.


# --- Game Setup ---
# Items
sword = Weapon.new("Iron Sword", "A trusty iron sword.", 10, 5)
health_potion = Potion.new("Health Potion", "Restores 20 HP.", 20, 1)
old_tree = Scenery.new("Old Tree", "A gnarled, ancient tree. It looks climbable but you're busy.")
shiny_key = GameItem.new("Shiny Key", "A small, shiny brass key.")
shiny_key.extend(Carryable) # Make key carryable by extending the instance
shiny_key.weight = 0.5

# Rooms
forest_clearing = Room.new("Forest Clearing", "You are in a sun-dappled forest clearing. Paths lead north and east.")
dark_cave = Room.new("Dark Cave", "It's damp and dark here. You hear a faint dripping sound. A path leads south.")
treasure_room = Room.new("Treasure Chamber", "A small chamber, surprisingly well-lit. A path leads west.")

# Place items in rooms
forest_clearing.items << sword
forest_clearing.items << old_tree
dark_cave.items << health_potion
treasure_room.items << shiny_key

# Connect rooms
forest_clearing.add_exit("north", dark_cave)
dark_cave.add_exit("south", forest_clearing)
forest_clearing.add_exit("east", treasure_room)
treasure_room.add_exit("west", forest_clearing)

# Characters
player = Player.new("Hero", "A brave adventurer.", 100)
goblin = Character.new("Goblin", "A nasty-looking goblin.", 30)

# Place characters
player.current_room = forest_clearing
forest_clearing.characters << player
dark_cave.characters << goblin

# --- Simple Game Loop ---
puts "Welcome to Mini Adventure!"
puts player.current_room.full_description

loop do
break unless player.alive?
print "\n> "
command_line = gets.chomp.downcase.split
action = command_line[0]
target = command_line[1..-1].join(' ') if command_line.length > 1

case action
when "quit"
puts "Thanks for playing!"
break
when "look"
if target.nil? || target.empty?
player.look_around
else
player.look_at(target)
end
when "n", "north"
player.move("north")
when "s", "south"
player.move("south")
when "e", "east"
player.move("east")
when "w", "west"
player.move("west")
when "inv", "inventory"
player.display_inventory
when "take", "get"
if target
player.take_item(target)
else
puts "Take what?"
end
when "drop"
if target
player.drop_from_inventory(target)
else
puts "Drop what?"
end
when "attack"
if target
enemy = player.current_room.characters.find { |c| c.name.downcase == target.downcase && c != player }
weapon_to_use = player.inventory.find { |i| i.is_a?(Weapon) } # Simplistic: use first weapon
if enemy && weapon_to_use
player.attack(enemy, weapon_to_use)
# Simple enemy AI: goblin attacks back if alive
if enemy.alive? && enemy.current_room == player.current_room
puts "#{enemy.name} retaliates!"
# For simplicity, goblin has no weapon, just base damage
enemy_weapon_mock = Weapon.new("Claws", "Goblin Claws", 5, 0) # mock weapon for attack logic
enemy.inventory << enemy_weapon_mock # temporarily give it to goblin for attack logic
enemy.attack(player, enemy_weapon_mock)
enemy.inventory.delete(enemy_weapon_mock)
end
elsif !enemy
puts "There's no one here named '#{target}' to attack."
elsif !weapon_to_use
puts "You have no weapon to attack with!"
end
else
puts "Attack who?"
end
when "drink"
if target
potion_to_drink = player.inventory.find { |i| i.name.downcase == target.downcase && i.is_a?(Potion) }
if potion_to_drink
player.drink_potion(potion_to_drink)
else
puts "You don't have a potion named '#{target}'."
end
else
puts "Drink what?"
end
when "help"
puts "Commands: look, look [target], n/s/e/w, inv, take [item], drop [item], attack [target], drink [potion], quit, help"
else
puts "Unknown command. Type 'help' for a list of commands."
end
# Remove dead characters from rooms
player.current_room.characters.reject! { |c| !c.alive? }
end
puts "Game Over."

This example showcases:

  • Classes: GameItem, Weapon, Potion, Scenery, Character, Player, Room.
  • Inheritance: Player < Character, Weapon < GameItem, etc.
  • Modules & Mixins: Describable, Carryable for adding shared behavior.
  • Encapsulation: Instance variables are generally accessed via methods (though some attr_accessors are used for simplicity here).
  • Polymorphism: look method from Describable used by various classes. The attack and drink_potion methods check item types (is_a?).
  • Object Composition: Room has items and characters; Player has an inventory.

Feel free to expand on this! Add more rooms, items, puzzles, and character interactions.

Conclusion: Your OOP Journey with Ruby


Object-Oriented Programming is a powerful paradigm, and Ruby provides an exceptionally pleasant and productive environment to wield it. From the straightforward syntax for classes and objects to the flexibility of mixins and metaprogramming, Ruby empowers you to build elegant, maintainable, and expressive software.

We've covered a lot of ground:

  • The basics of classes, objects, instance variables, and methods.
  • The core pillars: Encapsulation, Inheritance, and Polymorphism (especially Ruby's duck typing).
  • Advanced tools like Modules (for mixins and namespacing), Blocks/Procs/Lambdas, and a peek into Metaprogramming.
  • How design patterns can be implemented in Ruby.
  • A practical example to see these concepts in action.

The journey into mastering OOP is ongoing. Keep practicing, keep building, and keep exploring. Ruby's rich ecosystem and community are there to support you. Happy coding!


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

 
Вверх Снизу