Классы
Для начала мы поговорим о классах. Мы рассмотрим:
- Классы
- Как создаются объекты
- Как получить доступ к данным в них
Обзор объектно-ориентированного программирования
- Определите вещи, с которыми ваша программа имеет дело
- Этими вещами (их “чертежами”) являются классы
- Классы содержат методы (поведение)
- Объекты являются экземплярами этих вещей
- Объекты содержат переменные экземпляров (состояние)
Переменные экземпляров
В Ruby, переменные экземпляров начинаются на @
. К примеру, @name
.
Интересно то, что в Ruby переменные экземпляра не объявляются. Они как бы просто оживают, когда вызваны впервые.
Они доступны для всех методов экземпляра класса.
Создание объекта
Способ создания объектов в Ruby — вызов метода new
в классе, фактически это вызывает метод initialize
.
Метод initialize
в Ruby — это специальный метод, который действует как конструктор, как мы увидим в примере ниже.
Состояние объекта должно быть инициализировано внутри метода initialize
, то есть конструктора.
class Person
def initialize (name, age) # "Конструктор"
@name = name
@age = age
end
def get_info
@additional_info = "Интересно"
"Имя: #{@name}, возраст: #{@age}"
end
end
person1 = Person.new("Джо", 14)
p person1.instance_variables # [:@name, :@age]
puts person1.get_info # => Имя: Джо, возраст: 14
p person1.instance_variables # [:@name, :@age, :@additional_info]
Доступ к данным
Переменные экземпляров являются приватными, они не могут быть доступны вне класса.
Методы имеют публичный доступ по умолчанию.
Для доступа к переменным экземпляров нужно определить методы “getter” / “setter”.
class Person
def initialize (name, age) # Конструктор
@name = name
@age = age
end
def name # name getter
@name
end
def name= (new_name) # name setter
@name = new_name
end
end
person1 = Person.new("Джо", 14)
puts person1.name # Джо
person1.name = "Майк"
puts person1.name # Майк
# puts person1.age # undefined method `age' for #<Person:
Часто логика getter/setter довольно проста: получить существующее значение / установить новое значение.
Но должен быть путь еще проще, чем фактическое определение методов getter/setter.
Так что можно использовать синтаксис attr_*
:
attr_accessor
— getter и setterattr_reader
— только getterattr_writer
— только setter
class Person
attr_accessor :name, :age # геттеры и сеттеры для name и age
end
person1 = Person.new
p person1.name # => nil
person1.name = "Майк"
person1.age = 15
puts person1.name # => Майк
puts person1.age # => 15
person1.age = "пятнадцать"
puts person1.age # => пятнадцать
В этом примере есть две небольшие проблемы:
- При создании, Person находится в неинициализированном состоянии (без name или age)
- Скорее всего, мы хотели бы контролировать максимальный установленный возраст
Чтобы решить это, нужно использовать конструктор и более разумный setter для age.
Но для начала мы должны рассмотреть self
.
self
Внутри метода экземпляра, self
относится к самому объекту.
Обычно, использование self
для вызова других методов того же экземпляра является чуждым.
Но иногда вызов self
является обязательным, к примеру когда он может означать назначение локально переменной.
Вне определения метода экземпляра, self
относится к самому классу.
class Person
attr_reader :age
attr_accessor :name
def initialize (name, ageVar) # Конструктор
@name = name
self.age = ageVar # вызываем age= метод
puts age
end
def age= (new_age) # более разумный setter для age
@age = new_age unless new_age > 120
end
end
person1 = Person.new("Ким", 13) # => 13
puts "Мой возраст #{person1.age}" # => Мой возраст 13
person1.age = 130 # Попытка изменить возраст
puts person1.age # => 13 (setter не дал провести изменение)
Далее мы поговорим о методах/переменных и наследовании классов.
Наследование классов
Мы рассмотрим:
- Оператор
||
- Методы классов и операторы классов
- Наследование классов
Оператор ||
Оператор ||
оценивает левую сторону.
- Если true — возвращает ее
- В другом случае, возвращает правую сторону
@x = @x || 5
в первый раз возвратит5
, в следующий —@x
Краткая форма:
@x ||= 5
— то же, что и сверху
var = var || что-то
Давайте посмотрим чем это может быть полезно.
class Person
attr_reader :age
attr_accessor :name
def initialize (name, age) # Конструктор
@name = name
self.age = age # вызывает age= метод
end
def age= (new_age)
@age ||= 5 # устанавливаем 5 только для первого раза
@age = new_age unless new_age > 120
end
end
person1 = Person.new("Ким", 130)
puts person1.age # => 5 (по умолчанию)
person1.age = 10 # изменяем на 10
puts person1.age # => 10
person1.age = 200 # Пытаемся изменить на 200
puts person1.age # => 10 (всё еще)
Методы и переменные классов
Давайте поговорим методах классов и переменных классов.
Они вызываются на классе, а не экземпляре класса. Так что если вы знакомы с концептом метода static
в Java, это его эквивалент.
Self
вне определения метода относится к объекту Class
.
Как мы увидим, есть еще два способа определить методы класса в Ruby, кроме self
вне определения метода.
Переменные класса начинаются на @@
.
Единичный символ @
дал бы вам переменную экземпляра, как уже упоминалось, а двойной символ даст переменную класса.
class MathFunctions
def self.double(var) # 1. Используем self
times_called; var * 2;
end
class << self # 2. Используем << self
def times_called
@@times_called ||= 0; @@times_called += 1
end
end
end
def MathFunctions.triple(var) # 3. Вне класса
times_called; var * 3
end
# Без создания экземпляра!
puts MathFunctions.double 5 # => 10
puts MathFunctions.triple(3) # => 9
puts MathFunctions.times_called # => 3
Наследование класса
Каждый класс неявно наследуется от Object
. Сам Object
наследуется от BasicObject
.
Множественного наследования нет, вместо этого используются примеси (mixins).
Давайте взглянем на небольшой пример.
class Dog # неявно наследует Object
def to_s
"Собака"
end
def bark
"лает громко"
end
end
class SmallDog < Dog # означает наследование, наследует класс Dog
def bark # Перезаписываем
"лает тихо"
end
end
dog = Dog.new # (кстати, new это метод класса)
small_dog = SmallDog.new
puts "#{dog}1 #{dog.bark}" # => Собака1 лает громко
puts "#{small_dog}2 #{small_dog.bark}" # => Собака2 лает тихо
Модули
Теперь рассмотрим модули.
- Модули
- Как пространства имен
- Как примеси
- Использование встроенных в Ruby модулей, особенно
Enumerable
require_relative
В общем-то, модули являются контейнерами для классов, методов и констант. Или других модулей.
Они походи на Классы, но не могут быть инстанцированы.
Class
наследует Module
и добавляет new
.
Модули полезны для двух целей: пространства имен и примеси.
Модуль как пространство имен
module Sports
class Match # Класс match как часть модуля Sports
attr_accessor :score
end
end
module Patterns
class Match # Класс match как часть модуля Patterns
attr_accessor :complete
end
end
# Итак, мы имеем два разных Match
match1 = Sports::Match.new # заметьте использование оператора ::
match1.score = 40; puts match1.score # => 40
match2 = Patterns::Match.new
match2.complete = true; puts match2.complete # => true
Модуль как примеси
Другой способ использования модулей — это примеси.
В ОО-программировании есть интерфейсы. Они могут определять что класс “может” делать.
Примеси идут немного дальше и позволяют разделять (примешивать) готовый код между несколькими классами.
Кроме того, вы можете включить встроенные модули как Enumerable
, которые могут сделать трудную работу за вас.
module SayMyName
attr_accessor :name
def print_name
puts "Имя: #{@name}"
end
end
class Person
include SayMyName # позволяет включить функциональность этого модуля
end
class Company
include SayMyName
end
person = Person.new
person.name = "Джо"
person.print_name # => Имя: Джо
company = Company.new
company.name = "Google & Microsoft LLC"
company.print_name # => Имя: Google & Microsoft LLC
Модуль Enumerable
Что действительно интересно, вы можете включить встроенные модули из самого Ruby. Когда мы говорили о массивах, мы затрагивали, что они имеют такие методы как map
, select
, reject
, detect
и т.д.
Дело в том, что массивы включают модуль Enumerable и это то что дает им такую функциональность.
Вы можете включить его в свой собственный класс.
Все что вам для этого нужно — обеспечить реализацию метода each
. Как только вы это сделаете, вся функциональность Enumerable будет доступна!
Давайте взглянем на пример.
Скажем, у вас есть команда. Вы можете записать несколько классов в одном файле или сделать несколько файлов — оба метода полностью валидны.
# имя файла - player.rb
class Player
attr_reader :name, :age, :skill_level
def initialize (name, age, skill_level)
@name = name
@age = age
@skill_level = skill_level
end
def to_s
"<#{name}: #{skill_level}(SL), #{age}(возраст)>"
end
end
# team.rb
class Team
include Enumerable # Множество функций
attr_accessor :name, :players
def initialize (name)
@name = name
@players = []
end
def add_players (*players) # splat
@players += players
end
def to_s
"#{@name} team: #{@players.join(", ")}"
end
def each
@players.each { |player| yield player }
end
end
require_relative 'player' # require_relative позволяет включать другие .rb файлы
require_relative 'team'
player1 = Player.new("Боб", 13, 5); player2 = Player.new("Джим", 15, 4.5)
player3 = Player.new("Майк", 21, 5) ; player4 = Player.new("Джо", 14, 5)
player5 = Player.new("Скотт", 16, 3)
red_team = Team.new("Красная команда")
red_team.add_players(player1, player2, player3, player4, player5) # (splat)
# выбирает только игроков от 14 до 20 and отклоняет всех ниже уровня SL 4.5
elig_players = red_team.select {|player| (14..20) === player.age }
.reject {|player| player.skill_level < 4.5}
puts elig_players # => <Джим: 4.5(SL), 15(возраст)>
# => <Джо: 5(SL), 14(возраст)>
Области
Теперь рассмотрим области.
- Области переменных
- Области констант
- Как область работает с блоками
Методы и классы начинают новую область для переменных.
Внешние переменные не имеют отношения к внутренней области.
Всегда можно использовать метод local_variables
, чтобы посмотреть какие переменные входят (а какие — нет) в текущую область.
v1 = "outside" # вне области
class MyClass
def my_method
# p v1 Выдается исключение - нет такой переменной
v1 = "inside" # внутри
p v1
p local_variables
end
end
p v1 # => outside
obj = MyClass.new
obj.my_method # => inside
# => [:v1]
p local_variables # => [:v1, :obj]
p self # => main
Область: константы
- Константа — это любая ссылка, которая начинается с заглавной буквы, включая классы и модули.
- Правила области константы отличаются от правил области переменных.
- Внутренняя область может использовать константы, определенные вне этой области, и также может перезаписывать внешние константы.
- При этом внешнее значение остается неизменным за ее пределами
Давайте взглянем на пример.
module Test
PI = 3.14
class Test2
def what_is_pi
puts PI
end
end
end
Test::Test2.new.what_is_pi # => 3.14
module MyModule
MyConstant = 'Внешняя константа'
class MyClass
puts MyConstant # => Внешняя константа
MyConstant = 'Внутренняя константа'
puts MyConstant # => Внутренняя константа
end
# остается неизменной вне области
puts MyConstant # => Внешняя константа
end
Область: блоки
Блоки, в отличие от методов, наследуют внешнюю область.
Блоки закрыты. Они запоминают контекст в котором они были определены и используют этот контекст при каждом вызове.
Посмотрим на пример для блоков.
class BankAccount
attr_accessor :id, :amount
def initialize(id, amount)
@id = id
@amount = amount
end
end
acct1 = BankAccount.new(123, 200) # id, сумма
acct2 = BankAccount.new(321, 100)
acct3 = BankAccount.new(421, -100)
accts = [acct1, acct2, acct3]
total_sum = 0 # объявляем переменную
accts.each do |eachAcct| # считаем общую сумму в блоке
total_sum += eachAcct.amount
end
# та же область
puts total_sum # => 200
Блок – локальная область
Даже хотя блоки разделяют внешнюю область, переменная созданная внутри блока доступна только этому блоку.
Параметры блока всегда локальны для него, даже если они имеют то же имя, что и переменные внешней области.
Вы можете явно указывать локальные для блока переменные после двоеточия в списке параметров блока.
arr = [5, 4, 1]
cur_number = 10
arr.each do |cur_number|
some_var = 10 # Не доступна вне блока
print cur_number.to_s + " " # => 5 4 1
end
puts # отдает пустую строку
puts cur_number # => 10
adjustment = 5
arr.each do |cur_number;adjustment|
adjustment = 10
print "#{cur_number + adjustment} " # => 15 14 11
end
puts
puts adjustment # => 5 (Не затронута блоком)
Контроль доступа
Теперь рассмотрим:
- Три уровня контроля доступа
- Управление доступом
- Насколько приватен приватный доступ?
При проектировании класса, важно подумать о том как много из него будет раскрыто миру.
Инкапсуляция: старайтесь спрятать внутреннее представление объекта, чтобы позже вы могли изменить его.
Всего Ruby предоставляет три метода контроля доступа: public
, protected
и private
.
Инкапсуляция
В примере ниже детали рассчета рейтинга сохраняются внутри класса.
class Car
def initialize(speed, comfort)
@rating = speed * comfort
end
# Нельзя задать rating извне
def rating
@rating
end
end
puts Car.new(4, 5).rating # => 20
Назначение контроля доступа
В Ruby есть два пути назначения контроля доступа:
- Указать
public
,protected
илиprivate
- Всё до следующего ключевого слова управления доступом будет этого уровня доступа
- Определить методы обычным методом и затем указать
public
,protected
илиprivate
, и перечислить через запятую методы под этими уровнями, используя символы методов
По умолчанию (если вы не указываете ни один из уровней), методы являются public
.
Итак, взглянем на пример такого назначения.
class MyAlgorithm
private
def test1
"Приватный"
end
protected
def test2
"Защищенный"
end
public
def public_again
"Публичный"
end
end
# Второй способ:
class Another
def test1
"Приватный, как объявлено ниже"
end
private :test1
end
Что означают публичный, защищенный и приватный уровни?
public
методы — нет установленного управления доступом. Кто угодно может вызывать эти методы.
protected
методы — могут быть вызваны объектами определяющего класса или его подклассами.
private
методы — не могут быть вызваны с явным получателем.
Исключение: установка атрибута может быть вызвана с явным получателем.
Приватный доступ
class Person
def initialize(age)
self.age = age # Валидно - исключение
puts my_age
# puts self.my_age # Не валидно
# Нельзя использовать self на любом другом получателе
end
private
def my_age
@age
end
def age=(age)
@age = age
end
end
Person.new(24) # => 24