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

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

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

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

  • Выяснить количество книг каждого наименования
  • Подсчитать общую стоимость всех книг

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

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

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

  • Сущность, описывающая одну книгу, BookInStock
  • Коллекция книг, содержащихся в наличии, Books

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

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

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

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

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

object_one = ClassName.new
object_two = ClassName.new
  • Метод создаёт новый объект и возвращает ссылку на него
  • Метод принадлежит классу и вызывается на классе
  • Метод ::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
  • Метод используется для установки значения переменным экземпляра класса, которые определяют состояние объекта
  • Переменные экземпляра начинаются с символа @
  • Установленные значения переменных должны быть корректны, при необходимости метод может проверять переданные в него данные
  • После создания и инициализации объект должен корректно обрабатывать вызовы всех публичных методов

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

  • Методы p и pp показывают внутреннее состояние объекта
  • Метод puts пытается преобразовать объект к строке
  • Стандартный способ представления: имя класса и уникальный идентификатор
#<Object:0x000056409cbcf7e0>

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

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

  • Метод to_s не принимает аргументов
  • Метод 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 — метод для чтения значения переменной, при этом переменная не становится публичной
attr_reader :isbn, :price

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

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

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

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

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

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

  • Строка
  • Массив
  • Хеш

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

  • Числа
  • Строковые литералы
# frozen_string_literal: true

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

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

public void setPrice(double newPrice) {
    price = newPrice
}
  • В Ruby принято оформлять взаимодействие с атрибутами, как с обычными переменными
  • Для этого метод, устанавливающий значение атрибута, имеет на конце символ =
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
  • Чтение значения: book.price_in_copeks
  • Присваивание значения: book.price_in_copeks = 15

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

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

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

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

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

  • Чтение информации из нескольких CVS-файлов
    • read_in_csv_data
  • Вычисление нужных характеристик
    • total_value_in_stock
    • number_of_each_isbn

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

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

  • Стандартная поставка интерпретатора Ruby включает в себя библиотеку csv
  • Библиотека предоставляет класс CSV, позволяющий читать и записывать CSV-документы
  • Для чтения можно воспользоваться методом foreach
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 должен сохранять информацию о всех считанных книгах. Для её хранения будем использовать массив.

  • Пустые массивы обычно создаются с помощью литерала []
  • Метод << добавляет объект в конец массива
  • Метод push добавляет один или несколько объектов в конец массива

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

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

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

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

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

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

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

  • Основная задача первых состоит в проверке введённых данных, организации взаимодействия с пользователем
  • Задача вторых — выполнение нетривиальной обработки данных

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

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

  • require для подключения внешних библиотек
  • require_relative для подключения собственных файлов по относительному пути

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

Пример структуры ФС
require_relative 'book'
require_relative 'util/timer'
require_relative '../book'
  • Подключение файла book из файла main
  • Подключение файла timer из файла main
  • Подключение файла book из файла timer

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

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

  • Публичные (public) методы могут быть вызваны любым объектом
    • По умолчанию все методы кроме initialize являются публичными
  • Защищённые (protected) методы могут быть вызваны из любого объекта данного класса
  • Приватные (private) методы могут быть вызваны только лишь внутри данного класса

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

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

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

Для указания контроля доступа используются методы 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
person1[0] = 'J' # => 'Jim'
person2[0] # => 'Jim'
Связь переменных и объектов
  • Оператор присваивания записывает ссылку на объект в переменную
  • Единое состояние объекта доступно из всех переменных, которые содержат в себе ссылку на данный объект

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

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

person1 = 'Tim'
person2 = person1.dup
person1[0] = 'J'
  • Объект person2 содержит копию данных
  • При изменении объекта person2 объект person1 не изменяется

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

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
  • Удобно для многопоточного программирования
  • Внешний разработчик не может привести объект в некорректное состояние