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)
endendAddressWe 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 Addressbe 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)# хитрое сравнениеendendendWe load and execute for example inside pryand we get:
User::Address
=> User::Address
User::Address.new
=> ""This ensures that the modifier privatedoes not work in this context. But there is just a magic method private_constantthat will work as it should. After all, classes in Ruby are also constants. Now we can write private_constant :Addressand when trying to access to User::Addresscatch 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'classSharedCacheendAnd 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.setbypass 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_constwe remove Redis from the actual Top-Level visibility of objects. But before these we put Redis inside SharedCache. Further we can through private_constantaccess limit to SharedCache::Redis. However, in this case we will not be able to reach the class in Redisany way, even if we want to use it somewhere else. We ennoble and allow to make requireinside several classes:
classSharedCache
require_to 'redis', :Redis
private_constant :Redisdefstorage
Redis
endendclassSharedCache2
require_to 'redis', :Redis
private_constant :RedisendAttempts 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_tothat 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