Объектно-ориентированное программирование

Предпосылки: Функции (определение функции, параметры), Коллекции (массивы, хеши), Память (общие данные, изменение по ссылке).

Память | Наследование и полиморфизм

После заметки о памяти уже понятно: один и тот же хеш можно передать в несколько функций и изменить из любого места. Это работает, пока рядом одна сущность и несколько правил. Но в программе управления заказами и продавцами в одном файле быстро оказываются seller, payout_request, listing, shipment, refund, coupon, invoice и support_case. У каждой структуры свои поля, свои проверки, свои статусы и свои прямые записи.

Когда данные и операции живут отдельно

Если seller, payout_request, listing, shipment и другие сущности лежат в хешах, а операции над ними разбросаны по отдельным функциям, то форма данных начинает течь по всему файлу. Любая новая правка требует вспоминать:

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

Это неудобно не для выполнения программы, а для чтения и правки кода.

Вот пример. Его не нужно разбирать построчно — достаточно посмотреть, как быстро один файл превращается в кашу из восьми хешей, тридцати с лишним функций и прямых записей в структуры.

Класс и объект

Класс собирает данные одной сущности и связанные с ними операции в одном месте:

class Seller
  def initialize(name, verified, balance_cents)
    @name = name
    @verified = verified
    @balance_cents = balance_cents
  end
 
  def name
    @name
  end
 
  def can_withdraw?
    @verified && @balance_cents >= 10_000
  end
 
  def deposit(amount)
    @balance_cents = @balance_cents + amount
  end
end

class Seller ... end описывает тип объекта. Всё между class и end относится к продавцу как к сущности: и данные, и операции над ними.

def initialize(...) — специальный метод, который Ruby вызывает при создании нового объекта. Значения name, verified и balance_cents приходят снаружи, а @name, @verified и @balance_cents становятся состоянием конкретного продавца. Префикс @ показывает: это данные объекта, доступные его методам.

Метод — это та же функция, но привязанная к объекту. name возвращает имя продавца, can_withdraw? проверяет правило, deposit меняет баланс.

Такой ход встречается не только в Ruby. В Java и C# данные и методы тоже собирают в class. В Rust ту же роль часто играет пара struct + impl: данные лежат в структуре, а связанные операции описываются рядом.

Seller.new(...) создаёт новый объект и вызывает initialize. Точка в seller.name означает «вызвать метод name у объекта seller»:

seller = Seller.new("Alice Store", true, 9_000)
 
puts seller.name
puts seller.can_withdraw?   # false
 
seller.deposit(2_000)
puts seller.can_withdraw?   # true

Теперь правило вывода денег и изменение баланса живут рядом с данными продавца. Внешнему коду больше не нужно помнить, в каком хеше лежит balance_cents и какой ещё флаг надо проверить.

Инкапсуляция

Инкапсуляция (от лат. capsula — «коробочка») означает: объект сам контролирует, как его меняют.

В таком дизайне внешний код не пишет что-то вроде seller["balance_cents"] = seller["balance_cents"] + 2000. Он вызывает deposit, а проверку делает через can_withdraw?. Это не технический запрет, а договор о границах: состояние меняется через понятные точки входа.

Так код снаружи видит меньше деталей. Чтобы понять, как можно работать с продавцом, достаточно посмотреть методы класса.

Класс окупается тогда, когда у сущности есть данные и правила работы с ними. Если данные просто передаются из функции в функцию без собственного поведения — хеша достаточно.

Коллекция объектов

Объекты удобно хранить и обрабатывать в коллекциях. В примере ниже .each обходит массив, вызывая для каждого элемента блок кода между do и end. Имя |seller| — параметр блока: на каждом шаге он указывает на текущий элемент массива:

sellers = [
  Seller.new("Alice Store", true, 18_000),
  Seller.new("North Goods", false, 7_000)
]
 
sellers.each do |seller|
  if seller.can_withdraw?
    puts seller.name + ": payout ready"
  else
    puts seller.name + ": payout blocked"
  end
end

Массив остаётся массивом, но теперь его элементы умеют сами отвечать за своё поведение.

Класс Seller работает, пока все продавцы подчиняются одним правилам. Но что делать, если в системе появляются способы доставки — самовывоз, курьер, экспресс — каждый со своей ценой и сроком? Для этого нужны наследование и полиморфизм.

Sources

Как Ruby реализует классы и объекты на уровне интерпретатора — в Объекты и классы.

  • Dahl, O.-J. & Nygaard, K., 1966, SIMULA: an ALGOL-based simulation language. Communications of the ACM.
  • Kay, A., 1993, The Early History of Smalltalk. ACM SIGPLAN Notices.
  • Thomas, D. et al., 2023, Programming Ruby 3.3. Pragmatic Bookshelf.

Память | Наследование и полиморфизм