[swift-users] Feedback for Dependency Injection Framework?

Mike Lewis mikelikespie at gmail.com
Thu Jun 16 12:31:49 CDT 2016


Hi Ian,

Thanks for your insightful feedback. First, apologies for the broken link
in the README. I just updated it (I accidentally linked to the wrong
branch). (the actual demo can be found here:
https://github.com/square/Cleanse/tree/master/Examples/CleanseGithubBrowser)

Anyways, you bring up some good points. I think Cleanse may actually solve
a lot of your concerns, but it may be a symptom of my slapdash README. I'm
going to take a good documentation pass in the coming week and incorporate
your feedback and others.

Going to try to address some of your concerns.


> My first point of confusion is that these libraries seem to be less about
> dependency injection and more about service location. For instance, in your
> GitHub example in the playground, we see the following:
>
> struct GithubListMembersServiceImpl : GithubListMembersService {
>>     let githubURL: TaggedProvider<GithubBaseURL>
>
>
>>
>     func listMembers(organizationName: String, handler: [String] -> ()) {
>>         let url = githubURL
>>             .get()
>>
>> .URLByAppendingPathComponent("orgs/\(organizationName)/public_members")
>>         let dataTask = urlSession.dataTaskWithURL(url) ...
>
>
> To my mind it doesn't look like githubURL was injected. It looks like it
> was looked up in a service locator. In fact, it appears that the Binder is
> a service locator, and you can simply set up multiple service locators for
> multiple environments (with the added flexibility of multiple types via
> tags). I call this solving the problem of inflexible global state (usually
> singletons) by making it a flexible global state (a global service locator).
>

I'm not very familiar with the service locator pattern to be honest,
however, I can assure you that the base URL is being injected. I think
there may be some confusion on either how the GithubListMembersServiceImpl
is being constructed, or tagged providers in general.

For the former, there was feedback regarding the constructors being
implicitly made for structs in this reddit thread
<https://www.reddit.com/r/swift/comments/4o2lno/squarecleanse_swift_dependency_injection/d49pu6c>
.

As far as the TaggedProvider part, they're essentially a workaround for not
being able to have Binding/Qualifier annotations
<https://github.com/google/guice/wiki/BindingAnnotations> in swift

In java/Guice githubURL, would be probably be request as either
"@GithubBaseURL URL" or "@GithubBaseURL Provider<URL>".

In the playground example, one could easily change the service
implementation to be

struct GithubListMembersServiceImpl : GithubListMembersService {
>     let githubURL: *NSURL*


>
    func listMembers(organizationName: String, handler: [String] -> ()) {
>         let url = githubURL
>
> .URLByAppendingPathComponent("orgs/\(organizationName)/public_members")
>         let dataTask = urlSession.dataTaskWithURL(url) ...


and then change the binding statement to be


>       binder
>        .bind()
>        .to(value: NSURL(string: "https://api.github.com")!)


The downside of this in an actual application is that you may want to
inject other URLs. These "Type Tags
<file:///Users/lewis/Development/appointments-cleansed/Frameworks/Cleanse/Documentation/_build/html/README.html#type-tags>"
 TaggedProviders are essentially just a way of wrapping a type (e.g. NSURL)
and giving it a unique key. A lot of the necessity for TaggedProviders
should go away once Cleanse has a concept of Subcomponents
<http://google.github.io/dagger/subcomponents.html> and Scopes like in
Dagger 2.


> I've always thought of dependency injection as being a tool for avoiding
> global state. But in this case it seems to me like the service locator is
> globally managed. The language of service locators is even present in
> Guice's excellent motivation page
> <https://github.com/google/guice/wiki/Motivation> (my emphasis):
>
>
The only "global" state are things that are declared singletons. (which are
still only singletons in the scope of each time one calls build). They have
very similar semantics to singletons in Guice
<https://github.com/google/guice/wiki/Scopes> (however no custom scopes yet
as mentioned above).


> Guice will inspect the annotated constructor, and *lookup* values for
>> each parameter.
>>
>
Cleanse actually achieves the same thing, but without reflection. I
attempted to explain it in this part of the README
<https://github.com/square/Cleanse/blob/master/README.rst#terminating-methods-top1factory-p1---e-1st-arity>,
but I'll be honest, I struggled while attempting to make it both simple and
detailed enough to convey what's going on.


> Another point I've heard people talk about is that these libraries help as
> dependencies grow. I hope this isn't a straw man, as I interpret the
> following line in your README in that way.
>
> As the functionality of this app grows, one may add arguments to
>> RootViewController and its dependencies as well as more modules to satisfy
>> them.
>>
>
> The argument seems to go that this is easy to maintain:
>
> init(a: A)
>
>
> But this is difficult:
>
> init(a: A, b: B, c: C, d: D, e: E, ...)
>
>
> The argument goes further by saying that it becomes especially difficult
> as you may need to pass the dependencies through the some nodes of the
> object graph that seem not to need them. For instance, in the object graph
> A -> B -> C, if C has a dependency on Foo but B does not, why should B know
> about Foo?
>
> But I find that argument unconvincing on two grounds. First, I believe an
> object's dependencies are the union of its and its children's dependencies.
> In the example above, B has an implicit dependency on Foo. "Skipping"
> passing them around or instantiating them in the owning class is actually
> only hiding an object's true dependencies by using global state. Second, as
> a single object's dependencies become more unwieldy it seems in practice to
> indicate an architectural problem where objects are doing too much. These
> problems are related, as the architectural problem may not be as visible
> when the true dependencies are hidden!
>

So this is an interesting point. I'm a strong believer that DI solves this
issue really well.

Let's take this example (I think its similar to the straw man you are
referring to):

https://gist.github.com/mikelikespie/c54a017677265322df7eb785d9f36345

Basically, VC_A, needs serviceA, and a VC_B to do its job, however, for it
to create VC_B it needs VC_B's dependencies + its transitive dependencies
which happen to be serviceA, serviceB, serviceC.

So I agree with the arguments made in the straw man, e.g. don't need to
change the constructor hierarchy all the way down when VC_D needs a new
service or needs to start depending on something. It takes a pretty large
example to start to see these benefits, but its kind of like how you don't
really see a benefit from a quicksort over a bubble sort until you have a
lot of items to sort.

So if we refactor as is (without turning anything into protocols) to use
Cleanse, we come up with something like:

https://gist.github.com/mikelikespie/3abe371bf7f7ab67f71d6cfa22c0145d

Now, say serviceD incurred a new dependency, one would just add it to its
constructor and make sure the dependency is configured... don't have to
change any of the other view controllers in the hierarchy.

This gets me to the next point, testing. VC_A's purpose is to call serviceA
and present a view controller. To properly unit test its functionality, it
may not be necessary to provide a concrete implementation of the VC it
wants to present. In general if one can test a component and its
functionality correctly with less dependencies that's better.

>
> So to me — and with great respect for the work you've done! I know a lot
> of people (even some of my teammates with Dagger) value this approach
> immensely — I don't personally see how the approach adds value over manual
> injection, and I think the complexity is a negative.
>

I appreciate your feedback too! Hopefully will be able to reduce complexity
with iterations and improve documentation.



> I do think there is value in aiding with property injection for the cases
> where we can't use initializer injection (we do often use storyboards at my
> company, although we try to keep them small and unwieldy!), but I think
> those cases are solved with a few clever extensions. See post-script if
> you're curious.
>

Cleanse does support property injection
<https://github.com/square/Cleanse#property-injection>. My examples in the
readme only really demonstrate using it for the AppDelegate, but it also
makes sense for storyboard injection. It is very explicit though (less
magic than other DI frameworks). I also think there would be room for a
cleanse extension that specifically deals with storyboards. (I kinda like
what RxSwift did with RxCocoa).

Here's what the previous example could look like using Storyboards + segues:

https://gist.github.com/mikelikespie/db480360fe9b261cd3441c18a14bcbc9

Its definitely not terse and magical, but I think it is pretty explicit.

I'll check out post-script to see if there's anything to be learned though.
I also plan on including an example app using storyboard in the future.


Thanks for your other examples too! I think we may be on the page on some
of the property injection stuff, but it definitely shows some weakness in
my documentation.


> So: I'd love to know what sorts of use cases you find where this sort of
> library is very helpful.
>
> Thanks,
> Ian
>
> For contrast, I've been handling property injection on iOS with a few
> helpers, depending on how I want to do it.
>
> For segues identified by identifier (at my company we do tend to use
> storyboards with medium frequency; although we keep them separated into
> small collections of scenes rather than large unwieldy ones), extensions
> help create code like this:
>
> let dependency: Dependency       // itself injected, or
>
> // var dependency: Dependency! // if it could not be injected at
>> instantiation
>
>
>
> override func prepareForSegue(segue: UIStoryboardSegue, sender:
>> AnyObject?) {
>>     switch segueFromStoryboardSegue(segue) {
>>     case .GoToCustomViewController(let custom):
>>         custom.inject(dependency: dependency)
>>     }
>> }
>
>
> That's taken from some experiments I did at
> https://github.com/willowtreeapps/segue_handler
>
> Or in simple cases where I mostly care about type, I can do things like
> this:
>
> override func prepareForSegue(segue: UIStoryboardSegue, sender:
>> AnyObject?) {
>>     if let custom: CustomViewController =
>> segue.destinationViewController.injectable() {
>>         custom.inject(dependency: dependency)
>>     }
>> }
>
>
> With the extension here:
> https://gist.github.com/ianterrell/5aa139ab4b835b85cfee8e0f4430b863
>
> Finally, for that style of property injection a precondition in
> viewDidLoad or before usage in other types can ensure that the inject
> method was called.
>
> In my mind the AppDelegate represents the root of my graph, and is
> generally responsible for setting up all the dependencies it needs to care
> about. For integration tests or XCUITests (such as I've done, which is
> limited, which might be skewing my opinion), I use environment variables or
> launch options.
>
>
>
>
> On Tue, Jun 14, 2016 at 7:55 PM, Mike Lewis via swift-users <
> swift-users at swift.org> wrote:
>
>> Hi,
>>
>> I've recently open sourced a dependency injection framework for Swift
>> called Cleanse. https://github.com/square/cleanse
>>
>> It does a couple things I'd consider novel with the type system to make
>> wiring up functions easy without having to use reflection or other runtime
>> or compile-time hacks (I tried elaborating on it in this part of the README
>> here
>> https://github.com/square/Cleanse#dependency-requesting-terminating-methods
>> )
>>
>> Anyways, I'm looking for feedback on the design and use of this library,
>> and maybe strike up a discussion on language features that may make writing
>> a library such as this be able to be "cleaner" in the future. Couple things
>> that come to mind are custom annotations for qualifying types (instead of
>> what we call "type tags"), variadic generic arguments (which we work around
>> by code generating the various arities), or even a plugin architecture to
>> achieve things similar to what can be done with Java annotation processors.
>>
>> I'd also be interested in feedback on some more of the implementation
>> details. e.g. is using this to key objects by type a good thing or a
>> terrible thing?
>> https://github.com/square/Cleanse/blob/master/Cleanse/TypeKeyProtocol.swift
>>
>> Since Swift generics don't seem to be completely understood by the
>> general population, was hoping I could get some concrete feedback here.
>> Even would be stoked with a response to the tune of "You don't need DI in
>> Swift for reasons X, Y, and Z."
>>
>> Thanks!
>> Mike Lewis
>>
>> _______________________________________________
>> swift-users mailing list
>> swift-users at swift.org
>> https://lists.swift.org/mailman/listinfo/swift-users
>>
>>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-users/attachments/20160616/94ee094b/attachment.html>


More information about the swift-users mailing list