Объектно-ориентированное программирование
Предпосылки: Функции (определение функции, параметры), Коллекции (массивы, хеши), Память (общие данные, изменение по ссылке).
← Память | Наследование и полиморфизм →
После заметки о памяти уже понятно: один и тот же хеш можно передать в несколько функций и изменить из любого места. Это работает, пока рядом одна сущность и несколько правил. Но в программе управления заказами и продавцами в одном файле быстро оказываются seller, payout_request, listing, shipment, refund, coupon, invoice и support_case. У каждой структуры свои поля, свои проверки, свои статусы и свои прямые записи.
Когда данные и операции живут отдельно
Если seller, payout_request, listing, shipment и другие сущности лежат в хешах, а операции над ними разбросаны по отдельным функциям, то форма данных начинает течь по всему файлу. Любая новая правка требует вспоминать:
- какие поля есть у каждой структуры;
- какие функции их читают;
- какие функции их меняют;
- где есть прямой доступ без проверок.
Это неудобно не для выполнения программы, а для чтения и правки кода.
Вот пример. Его не нужно разбирать построчно — достаточно посмотреть, как быстро один файл превращается в кашу из восьми хешей, тридцати с лишним функций и прямых записей в структуры.
Какие функции знают о каких полях
Поля
sellerзнаютseller_display_name,seller_blocked?,seller_can_withdraw?,seller_risk_badge,payout_ready?,listing_publishable?и прямая записьseller["blocked"] = true. Поляlistingтекут вlisting_badge,listing_final_price_cents,listing_publishable?и прямую записьlisting["price_cents"] = -1000. То же происходит сpayout_request,shipment,refund,invoiceиsupport_case: форма данных уже расползлась по файлу и больше не выглядит локальной деталью.
Класс и объект
Класс собирает данные одной сущности и связанные с ними операции в одном месте:
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
endclass 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.