Сокрытие в 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 `<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


ссылка на оригинал статьи https://habr.com/post/419969/


Комментарии

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

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