Сокрытие в 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
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 1

      0
      При большом желании достучаться до такого класса все равно можно, это ж руби.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое