What do people use DI for? Usually in order to change the behavior of the code during the tests, avoiding calls to external services or just to test the object in isolation from the environment. Of course, DHH says that we can stop Time.now
, and enjoy the green points of the tests without much fuss, but you shouldn’t blindly believe everything DHH says. Personally, I like the point of view of Piotr Solnica, presented in this post . He gives this example:
class Hacker def self.build(layout = 'us') new(Keyboard.new(layout: layout)) end def initialize(keyboard) @keyboard = keyboard end # stuff end
The keyboard
parameter in the constructor is dependency injection. This approach allows you to test the class Hacker
, passing moki instead of the real Keyboard
instance. Insulation, all things:
describe Hacker do let(:keyboard) { mock('keyboard') } it 'writes awesome ruby code' do hacker = Hacker.new(keyboard) # some expectations end end
But what I like in the example above is an elegant trick with the .build
method in which the keyboard is initialized. In DI discussions, I have seen quite a few tips that suggested initializing dependencies into the calling code, for example, into controllers. Yeah, and then search for the whole Hacker entry project to see which class is specifically used for the keyboard, well, well. Whether business. .build
: default usecase in a prominent place, you need not look for anything.
Consider the following example:
class ExternalService def self.build options = Config.connector_options new(ExternalServiceConnector.new(options)) end def initialize(connector) @connector = connector end def accounts @connector.do_some_api_call end end class SomeController def index authorize! ExternalService.build.accounts end end
It can be seen that the controller creates the ExternalService using real objects (even though this is hidden in the ExternalService.build
method), which we try to avoid by implementing DI. How to handle this situation?
Substitute ExternalService.build
. Actually, what DHH was talking about, but there is one important point: replacing .build
, we do not change the behavior of the class instances, only the wrapper. Example on RSpec:
connector = instance_double(ExternalServiceConnector, do_some_api_call: []) allow(ExternalService).to receive(:build) { ExternalService.new(connector) }
It seems to me that the most effective is the combination of the second and third approaches: with the help of the second we test exceptional situations, with the help of the third we make sure that there are no errors in the code that instantiates the objects.
Despite what was written above, I am not against the use of IoC containers in general; it is just useful to remember that there are alternatives.
Links used in the post:
Source: https://habr.com/ru/post/308188/
All Articles