Hiding in Ruby. We also hide classes from Top-Level
In order not to go far, we will immediately define the terms.
- Encapsulation - the packaging of data and functions into a single component.
- Concealment - is a design principle, which consists in delimiting the access of different parts of the program to the internal components of each other.
Taken from the wiki . In the Ruby programming language with encapsulation, everything seems to be fine. With hiding at first sight, too, we have access to local variables, variable instances, different levels of access methods ( public
, protected
, private
). But sometimes this may not be enough.
Consider the following example.
classUserclassAddress < Stringdef==(other_object)# хитрое сравнениеendenddefinitialize(name:, address:nil)
@name = name
@address = Address.new(address)
endend
Address
We declare the class inside User
, we assume that it is not just some abstract address, but an address with specific logic that is needed only in the context of objects User
. And what's more, we don’t want this one to Address
be accessible from anywhere in the program, i.e. We not only encapsulate it inside User
, but also want to hide it for all other objects. How to do it?
You can try through private
.
classUser
private
classAddress < Stringdef==(other_object)# хитрое сравнениеendendend
We load and execute for example inside pry
and we get:
User::Address
=> User::Address
User::Address.new
=> ""
This ensures that the modifier private
does not work in this context. But there is just a magic method private_constant
that will work as it should. After all, classes in Ruby are also constants. Now we can write private_constant :Address
and when trying to access to User::Address
catch the error:
NameError: private constant User::Address referenced
Now we set the task more difficult. Add a caching class that redis will use.
#shared_cache.rbrequire'redis'classSharedCacheend
And it seems that nothing foreshadows trouble, as long as somewhere in the middle of the View, inside the erb of the template, someone does not want to write directly redis.get
/ redis.set
bypass even SharedCache. We treat as follows:
require'redis'
SharedCache.send :const_set, :Redis, Redis
Object.send :remove_const, :Redis
Redis
NameError: uninitialized constant Redis
from (pry):7:in`__pry__'
What happened? Through a call, remove_const
we remove Redis from the actual Top-Level visibility of objects. But before these we put Redis inside SharedCache
. Further we can through private_constant
access limit to SharedCache::Redis
. However, in this case we will not be able to reach the class in Redis
any way, even if we want to use it somewhere else. We ennoble and allow to make require
inside several classes:
classSharedCache
require_to 'redis', :Redis
private_constant :Redisdefstorage
Redis
endendclassSharedCache2
require_to 'redis', :Redis
private_constant :Redisend
Attempts to call 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>'
What it can be used for:
- To hide internal utility classes inside another class or module.
- Encapsulation with the concealment of logic within the service classes - you can prohibit access to some classes bypassing the service objects.
- Remove "dangerous" classes from Top-Level visibility, for example, to ban access to the database from View or serializers. In Rails, you can "hide" all ActiveRecord classes and give them access selectively in specific places.
And an example of implementation require_to
that moves constants from Top-Level to the desired level of visibility.
classObjectdefconst_hidesym, obj
_hidden_consts.const_set sym, obj
Object.send :remove_const, sym
enddefhidden_constants
_hidden_consts.constants
enddefhidden_constsym
_hidden_consts.const_get sym
enddefrequire_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
endend