Invoking a private class method from a private instance method

I am new to Ruby and come from the C # world. In C #, it is legal to do such things:

public class Test { public void Method() { PrivateMethod(); } private void PrivateMethod() { PrivateStaticMethod(); } private static void PrivateStaticMethod() { } } 

Is it possible to do something similar in Ruby?

A bit of context: I have a Rails application ... One of the models has a private method that installs some dependencies. There is a class method that creates an initialized instance of the model. For legacy reasons, there are some model examples that are not initialized correctly. I have added an instance method that initializes "uninitialized" instances where I want to execute the same initialization logic. Is there a way to avoid duplication?

Example:

 class MyModel < ActiveRecord::Base def self.create_instance model = MyModel.new model.init_some_dependencies # this fails model end def initialize_instance // do some other work other_init // call private method init_some_dependencies end private def init_some_dependencies end end 

I tried converting my private method to a private class method, but I still get the error message:

 class MyModel < ActiveRecord::Base def self.create_instance model = MyModel.new MyModel.init_some_dependencies_class(model) model end def initialize_instance # do some other work other_init # call private method init_some_dependencies end private def init_some_dependencies MyModel.init_some_dependencies_class(self) # now this fails with exception end def self.init_some_dependencies_class(model) # do something with model end private_class_method :init_some_dependencies_class end 
+6
source share
4 answers

First let me try to explain why the code is not working.

 class MyModel < ActiveRecord::Base def self.create_instance model = MyModel.new # in here, you are not inside of the instance scope, you are outside of the object # so calling model.somemething can only access public method of the object. model.init_some_dependencies ... end ... 

You can get around the private method call with model.send :init_some_dependencies . But I think that in this case is probably the best solution.

I would suggest that init_some_dependencies probably contains more business / domain logic rather than persistence. Therefore, I propose to bring this logic to a "domain object" (or some call it a service object). This is just a simple ruby ​​object that contains the domain logic.

Thus, you can separate the persistence logic into ActiveRecord and the domain logic with this class. Therefore, you will not inflate the ActiveRecord model. And you get the bonus of testing domain logic without having to use ActiveRecord. This will speed up your test.

You can create a file, for example `lib / MyModelDomain.rb '

 class MyModelDomain attr_accessor :my_model def initialize(my_model) @my_model = my_model end def init_some_dependencies my_model.property = 'some value example' end end 

Now you can use this object to say something like this

 class MyModel < ActiveRecord::Base def self.create_instance model = MyModel.new domain = MyModelDomain.new(model) domain.init_some_dependencies domain.my_model end def initialize_instance // do some other work other_init domain = MyModelDomain.new(self) domain.init_some_dependencies end end 

You can also move initialize_instance if you wish

Some resource that goes into this template:

+4
source

you can use

 model = MyModel.new model.send :init_some_dependencies 

to bypass method visibility checks.

+1
source

In C #, it is legal to do such things:

 public class Test { public void Method() { PrivateMethod(); } private void PrivateMethod() { PrivateStaticMethod(); } private static void PrivateStaticMethod() { } } 

Is it possible to do something similar in Ruby?

Yes:

 class Test def method private_method() end def self.greet puts 'Hi' end private_class_method :greet private def private_method self.class.class_eval do greet end end end Test.new.method Test.greet --output:-- Hi 1.rb:23:in `<main>': private method `greet' called for Test:Class (NoMethodError) 

But ruby ​​does not strictly respect confidentiality. For instance,

 class Dog def initialize @private = "secret password" end end puts Dog.new.instance_variable_get(:@private) --output:-- secret password 

ruby gives you freedom of access to private things with little effort:

 Test.new.method Test.class_eval do greet end --output:-- Hi Hi 

In ruby, a private method means that you cannot explicitly specify the recipient for the method, that is, there cannot be a name and a dot to the left of the method. But in ruby, a method without a receiver implicitly uses self as a receiver. Therefore, to call a private method, you just need to create a context where self is the correct recipient. Both class_eval and instance_eval change themselves inside the block to the receiver, for example

 some_obj.instance_eval do #Inside here, self=some_obj #Go crazy and call private methods defined in some_obj class here end 

You can apply these rules to this situation:

 (ahmy wrote:) First let me try to explain why the code does not work class MyModel < ActiveRecord::Base def self.create_instance model = MyModel.new # in here, you are not inside of the instance scope, you are outside of the object # so calling model.somemething can only access public method of the object. model.init_some_dependencies # this fails ... end ... 

The "context of this" and the "scope" are what a headache. All you need to remember: you cannot call a private method with an explicit receiver. The init_some_dependencies method has been defined as a private method, but it has a "model". written to his left. This is an explicit receiver. Explosion! Error.

Here is the solution:

 class MyModel def self.create_instance #In here, self=MyModel puts self model = MyModel.new model.instance_eval do #Changes self to model inside the block #In here, self=model init_some_dependencies #Implicitly uses self as the receiver, so that line is equivalent to model.init_some_dependencies end end private def init_some_dependencies puts "Dependencies have been initialized!" end end MyModel.create_instance --output:-- MyModel Dependencies have been initialized! 

Or, as ahmy and LBg pointed out, you can use Object # send () to call private methods:

 class MyModel def self.create_instance model = MyModel.new model.send(:init_some_dependencies, 10, 20) end private def init_some_dependencies(*args) puts "Dependencies have been initialized with: #{args}!" end end MyModel.create_instance --output:-- Dependencies have been initialized with: [10, 20]! 
-1
source

it certainly does.

Ruby, some OO strategies ( private and public keywords, etc.) come from C ++, so you can get almost the same use.

-3
source

Source: https://habr.com/ru/post/951433/


All Articles