Инкапсуляция, self и Struct/Data
Хороший объект прячет внутренности и показывает только нужный интерфейс. Ruby даёт уровни доступа к методам, ключевое слово self и удобные заготовки для объектов-значений.
Суть:privateпрячет методы от внешних вызовов,publicоткрывает;selfссылается на текущий объект;StructиData.defineбыстро создают классы-значения без ручногоinitialize.
Инкапсуляция — это сокрытие деталей реализации. Снаружи объект — чёрный ящик с понятными кнопками (публичные методы), а как он устроен внутри (приватные методы, переменные) — его личное дело. Это позволяет менять внутренности, не ломая код, который пользуется объектом.
class BankAccount
def initialize
@balance = 0
end
def deposit(amount) # публичный — точка входа
@balance += validate(amount)
end
def balance = @balance
private # ниже — всё приватное
def validate(amount) # внутренняя кухня
raise "сумма <= 0" if amount <= 0
amount
end
end
acc = BankAccount.new
acc.deposit(100)
puts acc.balance # => 100
# acc.validate(50) => NoMethodError: private method
Разбор: self и объекты-значения
self внутри метода — это «текущий объект». Чаще всего его опускают, но он обязателен при вызове сеттера (self.balance = 10), иначе Ruby создаст локальную переменную. А для простых «контейнеров данных» вместо полноценного класса берут Struct (изменяемый) или современный Data.define (неизменяемый, с Ruby 3.2).
# Data.define — неизменяемый объект-значение (Ruby 3.2+)
Point = Data.define(:x, :y) do
def distance_to_origin = Math.sqrt(x**2 + y**2)
end
p = Point.new(x: 3, y: 4) # или Point.new(3, 4)
puts p.x # => 3
puts p.distance_to_origin # => 5.0
# p.x = 10 => ошибка: Data заморожен, менять нельзя
# Struct — изменяемый, с перебором членов
Coord = Struct.new(:lat, :lon)
c = Coord.new(55.7, 37.6)
c.lat = 56.0 # можно менять
puts c.to_a.inspect # => [56.0, 37.6]
Как работает под капотом
Уровни доступа — это не «защита» в строгом смысле, а соглашение, которое Ruby проверяет при вызове. private-метод нельзя вызвать с явным получателем (obj.method) — только изнутри, на неявном self. Data.define и Struct «под капотом» сами генерируют initialize, методы доступа, ==, to_h и поддержку паттерн-матчинга (deconstruct_keys из раздела про case/in).
объект BankAccount
+-----------------------------+
| PUBLIC (видно снаружи): |
| deposit, balance | <-- интерфейс
| ------------------------ |
| PRIVATE (только внутри): |
| validate | <-- реализация
| @balance (состояние) |
+-----------------------------+
|
acc.deposit(100) --> OK (public)
acc.validate(50) --> NoMethodError (private)
Та же идея «неизменяемый объект-значение» на Python — это frozen dataclass:
# Та же логика на Python ▶
from dataclasses import dataclass
import math
@dataclass(frozen=True) # неизменяемый, как Data.define
class Point:
x: float
y: float
def distance_to_origin(self):
return math.sqrt(self.x**2 + self.y**2)
p = Point(3, 4)
print(p.distance_to_origin()) # 5.0
Частые ошибки
- Забыть self у сеттера. Внутри метода
balance = 10создаст локальную переменную, а не вызовет сеттер. Нужноself.balance = 10. - Делать всё публичным. Если наружу торчат внутренние методы, любой код начнёт на них опираться, и рефакторинг станет невозможным.
- Брать Struct там, где нужна неизменяемость. Если объект-значение не должен меняться, используйте
Data.define— он замораживается автоматически.
Best practices
- Открывайте минимальный интерфейс: всё, что не часть публичного API, делайте
private. - Для простых неизменяемых контейнеров данных предпочитайте
Data.defineручному классу — меньше кода, есть==и паттерн-матчинг из коробки. - Используйте
selfявно только там, где он нужен (сеттеры, методы класса) — в остальных местах его опускают.
Глубже: protected и зачем он нужен
Между публичными и приватными методами есть третий, менее известный уровень — protected. Разница тонкая, но важная. Приватный метод нельзя вызвать с явным получателем вообще — только на неявном self изнутри объекта. Защищённый же метод можно вызвать с явным получателем, но лишь из другого объекта того же класса (или потомка). Зачем это нужно? Классический сценарий — сравнение двух объектов одного класса по их внутреннему состоянию. Чтобы метод > у объекта a мог заглянуть в баланс объекта b, баланс b должен быть доступен — но открывать его всему миру через публичный геттер не хочется. protected — идеальный компромисс: «свои» (объекты того же класса) видят это поле друг у друга, а посторонние — нет. На практике protected используется реже private, и многие проекты обходятся без него, но знать о нём стоит: встретив его в чужом коде, вы поймёте замысел автора. А общий принцип остаётся прежним: открывайте наружу минимум, прячьте максимум, и тогда внутренности объекта можно будет менять, не ломая зависящий от него код.
Итог. Инкапсуляция прячет детали: private-методы зовутся только изнутри. self — текущий объект, обязателен для сеттеров. Struct (изменяемый) и Data.define (неизменяемый, современный) экономят код для объектов-значений, давая ==, to_h и паттерн-матчинг даром.