Хабрахабр

Сокрытие в 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 был доступен из любого места программы, т.е. Класс Address мы объявляем внутри User, предполагаем, что это не просто некоторый абстрактный адрес, а адрес со специфичной логикой которая нужна только в контексте объектов User. Как это сделать? не только инкапсулируем его внутри User, но и хотим его скрыть для всех других объектов.

Можно попробовать через private.

class User private class Address < String def ==(other_object) # хитрое сравнение end end
end

Загружаем и выполняем например внутри pry и получаем:

User::Address
=> User::Address
User::Address.new
=> ""

Зато есть просто волшебный метод private_constant который сработает как надо. Тем самым убеждаемся, что модификатор private в таком контексте не работает. Теперь мы можем написать 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 видимости объектов. Что произошло? Далее мы можем через private_constant ограничить доступ к SharedCache::Redis. Но перед эти мы помещаем Redis внутрь SharedCache. Облагораживаем и позволяем сделать require внутрь нескольких классов: Однако в таком случае мы уже не сможем достучаться до класса Redis никоим образом, даже если захотим использовать его где-то ещё.

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 `<main>'
[2] pry(main)> require 'redis'
=> false
[3] pry(main)> Redis
NameError: uninitialized constant Redis
from (pry):6:in `<main>'
[4] pry(main)> SharedCache.new.storage
=> Redis
[5] pry(main)> SharedCache2::Redis
NameError: private constant SharedCache2::Redis referenced
from (pry):1:in `<main>'

Для чего это можно использовать:

  • Для сокрытия внутренних служебных классов внутри другого класса или модуля.
  • Инкапсуляция с сокрытием логики внутри сервисных классов — можно запретить обращение к некоторым классов в обход сервисных объектов.
  • Убрать "опасные" классы из Top-Level видимости, например для запрета к обращению к БД из View или сериализаторов. В Rails можно "скрыть" все ActiveRecord классы и давать к ним доступ выборочно в конкретных местах.

И пример реализации require_to который перемещает константы из Top-Level на нужный уровень видимости.

require_to

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть