Классы, объекты и переменные

Андрей Васильев

2020

Задача

Мы управляем магазином подержанных книг. Каждую неделю проводится инвентаризация. Работники сканируют бар-коды на книгах и сохраняют их в CSV-списки.

Пример файла

"Date","ISBN","Price"
"2019-04-12","978-1-9343561-0-4",939.45
"2019-04-13","978-1-9343561-6-6",645.67
"2019-04-14","978-1-9343560-7-4",836.95

Задачи системы

Идентификация ключевых элементов

При проектировании решения в объектно-ориентированном подходе сначала необходимо идентифицировать ключевые сущности

Для нашего случая выделим следующие сущности:

Описание классов в Ruby

Для описания классов используется конструкция

class ClassName
  # Содержимое класса
end

Создание объектов в Ruby

Для создания объектов класса в Ruby используется метод new

object_one = ClassName.new
object_two = ClassName.new

В примере выше мы создали 2 объекта класса ClassName и записали их в переменные object_one и object_two

Методу можно передать данные, которые будут использованы при инициализации объекта:

book = BookInStock.new('978-1-9343560-7-4', 10.2)

Инициализация объектов в Ruby

После создания каждого объекта Ruby инициализирует объект, вызывая метод initiailize и передавая ему параметры из конструктора

class BookInStock
  def initialize(isbn, price)
    @isbn = isbn
    @price = Float(price) # Может выбросить исключение
  end
end

«Печать» объектов

#<Object:0x000056409cbcf7e0>

Преобразование к строке

При преобразовании объекта к строке вызывается метод to_s, который можно переопределить для своего класса

Для работы методов p и pp иногда разумно реализовать метод inspect, который формирует представление об объекте с точки зрения программиста

Атрибуты объекта

class Book
  ..
  def isbn
    @isbn
  end
end
book = Book.new('AAA-53-555', 500)
puts book.isbn

Упрощённое создание атрибутов объектов

В языке Ruby есть методы-помощники, упрощающие задачу описания атрибута на основе переменной экземпляра

Метод attr_reader создаёт методы для получения значений переменных экземпляра

attr_reader :isbn
attr_reader :price
attr_reader :isbn, :price

Методу можно передать несколько имён переменных, но это усложняет совместную работу при использовании систем контроля версий

Опасность публикации атрибутов на чтение

При написании классов согласно методологии объектно-ориентированного программирования мы должны придерживаться принципа инкапсуляции, при котором данными объекта должен управлять только этот объект

Чтение атрибута — это получение ссылки на «внутренний» объект

Если данный объект можно изменить, тогда нельзя сказать, что класс с данным атрибутом полностью управляет своими данными

Изменяемые объекты в Ruby

Неизменяемые объекты в Ruby

Изменение атрибутов

Обычным для объектно-ориентированных языков способом изменения атрибута является создание специального метода

public void setPrice(double newPrice) {
    price = newPrice
}
def isbn=(isbn)
  @isbn = isbn
end
book.isbn = '978-1-9343561-0-4'

Методы для создания атрибутов

Метод attr_accessor создаёт методы для чтения и записи данных в переменные экземпляра

attr_accessor :isbn
attr_accessor :price

Метод attr_writer создаёт метод для записи данных. Зачастую не используется, так как сложно представить ситуацию, когда надо записывать данные, но не считывать их.

Виртуальные атрибуты

В Ruby вы всегда взаимодействуете с методами, а не с переменными экземпляра. Это позволяет зафиксировать интерфейс объекта и легко менять его реализацию в будущем

Можно описать атрибут, который выполняет более сложные действия по сравнению с чтением и записью

Виртуальный атрибут — стоимость книги в копейках

def price_in_copeks
  Integer(@price * 100 + 0.5)
end
def price_in_copeks=(copeks)
  @price = copeks / 100.0
end

Взаимодействие между классами

Во время решения реальных задач с помощью классов мы описываем не только реальные объекты, но также и технические элементы, необходимые для достижения цели

В нашем приложении необходимо обрабатывать информацию о множестве книг, которая записана в CSV-файлы

Класс Books — чтение и обработка набора данных

Определим интерфейс класса, который мы хотим реализовать

Чтение данных из CSV-файла

Класс Books должен считывать данные из нескольких CSV-файлов, которые собираются разными устройствами

CSV.foreach('file.csv', headers:true) do |row|
  puts "#{row['ISBN']}, #{row['Price']}"
end
"Date","ISBN","Price"
"2013-04-12","978-1-9343561-0-4",39.45

Хранение информации о книгах

Класс Books должен сохранять информацию о всех считанных книгах. Для её хранения будем использовать массив.

Альтернативные имена методов

Ruby позволяет разработчикам определить альтернативные имена для публичных методов. Для создания псевдонима следует использовать метод alias_method.

В Ruby 2.5 ввели альтернативное название для метода pushappend

Структурирование файлов приложения

Обычно один исходный файл на языке Ruby содержит один класс или один модуль, что

Желательно отделять модули, ответственные за взаимодействие с внешним миром (пользователи, файлы) от модулей, которые реализуют обработку данных

Подключение внешних файлов

Для подключения других файлов используются методы

Относительный путь строится относительно текущего файла

Пример структуры ФС
Пример структуры ФС

Контроль доступа к методам класса

Ruby предоставляет 3 уровня контроля доступа к методам

Отличия от знакомых вам языков программирования

Указание контроля доступа

Для указания контроля доступа используются методы public, protected, private

Указание уровня доступа для секции

class MyClass
  private
  def method_one
  end
  def method_two
  end
end

Указание уровня доступа для конкретных методов

class MyClass
  pritave :method_one, :method_two
end

Переменные

Связь переменных и объектов
Связь переменных и объектов

Предотвращение непродуманных изменений

Использование явного копирования

person1 = 'Tim'
person2 = person1.dup
person1[0] = 'J'

Запрет всех последующих изменений

person1 = 'Tim'
person2 = person1
person1.freeze
person2[0] = 'J' # => Ошибка изменения константы

Проектирование объекта неизменяемым

При применении данной техники вместо изменения текущего объекта создаётся копия оригинального объекта

class Maslo
  attr_reader :weight # Только лишь чтение
  def initialize(weight)
    @weight = weight
  end
  def take(weight)
    new Maslo(@weight - weight) # Новый объект
  end
end