Сокрытие в Ruby. А ещё скрываем классы из Top-Level
Что бы далеко не ходить, сразу определимся с терминами.
- Инкапсуляция — упаковка данных и функций в единый компонент.
- Сокрытие — представляет собой принцип проектирования, заключающийся в разграничении доступа различных частей программы к внутренним компонентам друг друга.
Взято с вики. В языке программирования Ruby с инкапсуляцией вроде как всё хорошо. С сокрытием на первый взгляд тоже, нам доступны локальные переменные, переменные инстансов, разные уровни доступа к методам (public
, protected
, private
). Но иногда этого может не хватать.
Рассмотрим следующий пример.
class User
class Address < String
def ==(other_object)
# хитрое сравнение
end
end
def initialize(name:, address: nil)
@name = name
@address = Address.new(address)
end
end
Класс Address
мы объявляем внутри User
, предполагаем, что это не просто некоторый абстрактный адрес, а адрес со специфичной логикой которая нужна только в контексте объектов User
. И более того, мы не хотим, что бы этот самый Address
был доступен из любого места программы, т.е. не только инкапсулируем его внутри User
, но и хотим его скрыть для всех других объектов. Как это сделать?
Можно попробовать через private
.
class User
private
class Address < String
def ==(other_object)
# хитрое сравнение
end
end
end
Загружаем и выполняем например внутри pry
и получаем:
User::Address
=> User::Address
User::Address.new
=> ""
Тем самым убеждаемся, что модификатор private
в таком контексте не работает. Зато есть просто волшебный метод private_constant
который сработает как надо. Ведь классы в руби это тоже константы. Теперь мы можем написать private_constant :Address
и при попытке доступа к User::Address
словить ошибку:
NameError: private constant User::Address referenced
Теперь ставим задачку посложнее. Добавляем класс кэширования который будет использовать redis.
#shared_cache.rb
require 'redis'
class SharedCache
end
И вроде бы ничего не предвещает беды, до тех пор пока где то посреди View, внутри erb шаблона, кто-нибудь не захочет написать напрямую redis.get
\ redis.set
в обход даже SharedCache. Лечим следующим образом:
require 'redis'
SharedCache.send :const_set, :Redis, Redis
Object.send :remove_const, :Redis
Redis
NameError: uninitialized constant Redis
from (pry):7:in `__pry__'
Что произошло? Через вызов remove_const
мы убираем Redis фактически из Top-Level видимости объектов. Но перед эти мы помещаем Redis внутрь SharedCache
. Далее мы можем через private_constant
ограничить доступ к SharedCache::Redis
. Однако в таком случае мы уже не сможем достучаться до класса Redis
никоим образом, даже если захотим использовать его где-то ещё. Облагораживаем и позволяем сделать require
внутрь нескольких классов:
class SharedCache
require_to 'redis', :Redis
private_constant :Redis
def storage
Redis
end
end
class SharedCache2
require_to 'redis', :Redis
private_constant :Redis
end
Попытки вызова Redis:
[1] pry(main)> SharedCache::Redis
NameError: private constant SharedCache::Redis referenced
from (pry):1:in `'
[2] pry(main)> require 'redis'
=> false
[3] pry(main)> Redis
NameError: uninitialized constant Redis
from (pry):6:in `'
[4] pry(main)> SharedCache.new.storage
=> Redis
[5] pry(main)> SharedCache2::Redis
NameError: private constant SharedCache2::Redis referenced
from (pry):1:in `'
Для чего это можно использовать:
- Для сокрытия внутренних служебных классов внутри другого класса или модуля.
- Инкапсуляция с сокрытием логики внутри сервисных классов — можно запретить обращение к некоторым классов в обход сервисных объектов.
- Убрать «опасные» классы из Top-Level видимости, например для запрета к обращению к БД из View или сериализаторов. В Rails можно «скрыть» все ActiveRecord классы и давать к ним доступ выборочно в конкретных местах.
И пример реализации require_to
который перемещает константы из Top-Level на нужный уровень видимости.
class Object
def const_hide sym, obj
_hidden_consts.const_set sym, obj
Object.send :remove_const, sym
end
def hidden_constants
_hidden_consts.constants
end
def hidden_const sym
_hidden_consts.const_get sym
end
def require_to(name, sym, to: nil)
require name
if Object.const_defined? sym
obj = Object.const_get sym
const_hide sym, obj
else
obj = hidden_const sym
end
(to || self).const_set sym, obj
end
private
def _hidden_consts
@@_hidden_consts ||= Class.new
end
end