forked from google/pinject
-
Notifications
You must be signed in to change notification settings - Fork 3
/
DESIGN
193 lines (147 loc) · 7.72 KB
/
DESIGN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
Verifying injectability
=======================
Choices:
1. Allow binding specs to require but not actually declare bindings.
2. Require declaring everything to be instantiated when calling new_object_graph().
Benefits of ``require()``
-------------------------
You distribute the requirements to the binding specs that need them. For
example, if program A depends on library B, which depends on library C, which
depends on library D, and library D takes an object graph and instantiates
something, then you don't need to modify program A when instantiating
something new in library D. (Though, this may be a spurious reason, since why
would program A be passing an object graph through several layers of library?)
You don't have to declare up front everything that you may inject. Doing so
may be difficult to do in a large program modified by many people.
You find out about missing dependencies for everything instantiable when the
object graph is created, as opposed to finding out about missing "I'm going to
instantiate this" declarations only when you actually try to instantiate
something (and fail).
Benefits of declaring instantiables
-----------------------------------
You don't have to satisfy requirements, declared by binding specs, for things
you're not actually going to instantiate. (Though, this may be a spurious
reason, since binding specs are really supposed to declare bindings that you'd
use all together, and you could always break up a binding spec with unwanted
requirements.)
Decision
--------
Use ``require()``.
Field injection
===============
Choices:
1. Support field injection.
2. Require initializer injection.
Benefits of field injection
---------------------------
You don't need initializer boilerplate. With initializer injection, you write
the name of each arg three times. ... Maybe I should add a
@copy_args_to_members decorator?
Benefits of requiring initializer injection
-------------------------------------------
With initializers, you have docstrings, a standard place and format for
documentation, whereas there is no such standard for fields.
An initializer makes it clear when your object is fully initialized and (if
you design your objects properly) forces full initialization up front. With
field injection, there's no single place to look for all fields that need
injecting, and no way for an object to guarantee that all fields have values
appropriately set.
Field injection makes manual instantiation more cumbersome. Instead of
writing ``SomeObject(foo, bar, baz)`` on a single line, you have to write ``s
= SomeObject(); s.foo = foo; s.bar = bar; s.baz = baz`` on four lines. You
also can't instantiate the object as a single expression, unless you write a
helper function, in which case you may as well make the helper function be the
initializer, and use initializer injection.
Field injection makes debugging with manual injection more difficult. For
example, because instead of a missing initializer arg being discovered right
where the object is instantiated, the missing field value is discovered only
when it's used and found to be unset.
Decision
--------
Don't support field injection. Require initializer injection.
Passing args directly to provider methods
=========================================
Choices:
1. Allow passing args when calling provider methods.
a. Allow passing args that Pinject has bindings for.
b. Allow passing only args that Pinject does not have bindings for.
c. Allow passing args only designated as user-specifiable.
2. Require provider method args to be injected by Pinject.
Benefits of allowing directly passed args
-----------------------------------------
Sometimes, you need to be able to create lots of objects based on user data
but with some automatically injected args as well. You would normally create
a manually defined factory.
.. code-block:: python
>>> class SomethingFactory(object):
... def __init__(self, injected_arg):
... self._injected_arg = injected_arg
... def new(self, passed_arg):
... return Something(passed_arg, self._injected_arg)
...
>>> class SomeBindingSpec(pinject.BindingSpec):
... def provide_somethings(self, user_data, something_factory):
... return [something_factory.new(datum) for datum in user_data]
...
>>>
The factory is mostly boilerplate. The only interesting part is its ``new()``
method. The boilerplate is there to allow args both specified by user code
and injected by Pinject. You should be able to do away with the boilerplate
by having user code call the provider method directly and having Pinject
supply any args not supplied by user code.
.. code-block:: python
>>> class SomeBindingSpec(pinject.BindingSpec):
... def provide_something(self, passed_arg, injected_arg):
... return Something(passed_arg, self._injected_arg)
... def provide_somethings(self, user_data, provide_something):
... return [provide_something(datum) for datum in user_data]
...
>>>
Benefits of forbidding directly passed args
-------------------------------------------
It's less clear to readers of the code which args are intended to be passed as
part of calling the provider method, and which args are intended to be
injected by Pinject. (It doesn't seem useful to allow both possibilities in
the same binding spec.)
If Pinject can't tell the difference between args that will be passed in by
user code and args that it is supposed to inject, then it cannot automatically
require bindings for the args that it is supposed to inject.
Maybe there could be some way to distinguish args that the user is supposed to
provide.
How to distinguish args passable directly
-----------------------------------------
Two options:
* make such args have defaults; or
* make such args have a default of ``pinject.PASSED_DIRECTLY``.
The latter is probably better, because:
* it doesn't require a default that may not make sense and that the code will check is not set; and
* it is more explicit about what's going on than magically being able to set args if they have defaults.
But this feels a little awkward, because if provider methods get the ability
to designate args as ``PASSED_DIRECTLY``, then that should probably also apply
to initializers. But initializers are much more likely than provider methods
to be used directly (i.e., not via Pinject). Having an arg with a default
means that the calling code doesn't know that it really needs to specify that
arg. The ``PASSED_DIRECTLY`` value will get assigned to a field and only
discovered at a point distant from the initializer call. It would be possible
to make ``@copy_args_to_internal_fields`` check for ``PASSED_DIRECTLY``
values, but that assumes the code is using ``@copy_args_to_internal_fields``.
And it would require all directly passed args to be last, after injected args
(which would have no defaults).
Maybe a decorator would be better, something like:
* ``@skip_injection``
* ``@allow_skipping_injection``
* ``@allow_direct_injection``
* ``@allow_as_provider_arg``
* ``@inject``
A decorator would be explicit, which is good. A decorator would allow Pinject
to figure out what args it's responsible for injecting, which is good.
It's not clear whether to allow injected provider args where either the user
can specify them, or else Pinject provides them. Probably not: users can
always use two params to simulate specified-or-injected. And if it turns out
to be really useful later, adding it maintains backward compatibility.
Decision
--------
Create a ``@inject`` decorator that says which of the function's args are
passed directly vs. injected as dependencies. Have ``@inject`` do double duty
marking initializers as explicitly injectable. Allow args to be passed
directly or injected as dependencies but not both.