Giter VIP home page Giter VIP logo

blindside's Introduction

Blindside: dependency injection for Objective-C

Carthage compatible

Why should I use this?

Blindside helps you write clean code. You can keep your objects loosely coupled and ignorant of the world outside. Your objects can create other objects without needing to know about the other objects' dependencies.

What's with the name?

It's a play on the idea that objects should be blind to the origin of their dependencies. Mostly, however, it's fun to name classes "BSInjector", "BSProvider", etc.

What, are we Java?

Sadly, yes. Objective-C and Java are identical with respect to the object-level dependency management offered by the language: basically none. Global variables, "sharedInstance" singletons, and pass-through parameters are the common patterns used to access dependencies. None are good options.

Guice brought elegant object dependency management to Java, obviating the need for static data, and fulfilling the promise of true OO programming. Blindside seeks to do the same for Objective-C.

What features does it have?

  • Injecting dependencies of UIViewController subclasses, including master/detail view controllers
  • "Constructor injection" via initializers
  • "Setter injection" via properties
  • Injection of third-party classes using categories
  • Binding to instances, blocks, Providers, or classes
  • Scoped bindings, including Singletons
  • Support for creation-time parameters not defined in bindings.

Status

Blindside is alpha software. There's no documentation aside from this readme. My focus is now on building out the documentation, along with examples. I'm using it on my current iOS project and it's working nicely. I've published it as-is to see if it stirs any interest. If you've come across this readme and have some questions, or would like to learn more on how to use Blindside, please get in touch.

How does it work?

You describe your object's dependencies, define bindings to fill those dependencies, then ask the BSInjector to create your objects. Here's a "Hello, World" example:

/**
 * Our view controller. It needs to be created with an api instance.
 */
@interface MyViewController : UIViewController
- (id)initWithApi:(id<MyApi>)api;
@end

@implementation MyViewController

/**
 * Describing MyViewController's dependencies. We want instances initialized using initWithApi:, which takes one arg.
 */
+ (BSInitializer *)bsInitializer {
		return [BSInitializer initializerWithClass:self 
		                                  selector:@selector(initWithApi:) 
		                              argumentKeys:@"myApi", nil];
}

...

@end


@interface MyBlindsideModule : NSObject<BSModule>
@end

@implementation MyBlindsideModule

/**
 * Creating a binding for our MyApi dependency.
 */
- (void)configure:(id<BSBinder>) binder {
		id<MyApi> apiInstance = [[MyApiImpl alloc] initWithEndpoint:@"http://api.mycompany.com"];
		[binder bind:@"myApi" toInstance:apiInstance];
}
@end


@implementation AppDelegate
- (void)applicationDidFinishLaunching {
		...
		// Creating an injector configured with our BSModule. Asking the injector for an instance of our ViewController.
		MyBlindsideModule *module = [[MyBlindsideModule alloc] init];
		id<BSInjector> injector = [Blindside injectorWithModule:module];
		UIViewController *rootViewController = [injector getInstance:[MyViewController class]];
		
		...
}
@end

Obviously you don't need a framework to accomplish a task this trivial. Blindside really helps when MyViewController creates another view controller, which creates another, which has additional dependencies, and so on.

Describing dependencies

Blindside provides dependencies to objects in two ways: via an initializer (e.g. initWithDelegate:), or using properties. You can mix and match the two.

Blindside relies on two class methods for describing dependencies. These methods are added to NSObject in the NSObject+Blindside category, and are meant to be overridden by classes injected with Blindside. The methods are:

  • (BSInitializer *)bsInitializer;
  • (BSPropertySet *)bsProperties;

bsInitializer describes the initialization method to be used when creating instances of a class, including the initializer's selector and arguments. Blindside can use a class' BSInitializer to create instances of the class, with dependencies injected.

bsProperties describes the properties to be injected into already-created objects.

Here's an example implementation of the two methods for a class named House. The House class takes an Address as an initializer arg, and has a property of type UIColor.

@implementation House

+ (BSInitializer *)bsInitializer {
		SEL selector = @selector(initWithAddress:)
		return [BSInitializer initializerWithClass:self selector:selector argumentKeys:[Address class]];
}

+ (BSPropertySet *)bsProperties {
		BSPropertySet *propertySet = [BSPropertySet propertySetWithClass:self propertyNames:@"color", nil];
		[propertySet bindProperty:@"color" toKey:@"myHouseColor"];
		return propertySet;
}
 ...

Awaking from injection

When working with property injection, it is occationally desirable to have a hook that can be used to finish setting up an object after all dependencies have been injected. Blindside provides a mechanism for this:

@implementation House
- (void)bsAwakeFromPropertyInjection {
	// Finalize instantiation
}
 ...

Note that the use of this method is discouraged because it increases the coupling between your code and Blindside. First look for other appropriate lifecycle methods on your object (e.g. -viewDidLoad for a view controller) that could be used to perform this kind of work.

Swift

You can also use Blindside in Swift. First, add #import <Blindside/Blindside.h> to your Swift's bridging header file to expose the framework to your Swift code.

Create an injector as you would in Objective-C:

let module = MyBlindsideModule()
let injector = Blindside.injectorWithModule(module)

To instantiate your controller with no arguments:

let controller: MyViewController = injector.getInstance(MyViewController.self) as! MyViewController

To pass dynamic arguments (those marked with BS_DYNAMIC) to your controller, use the newly exposed -getInstance:withArgArray: method.

let controller: MyViewController = injector.getInstance(MyViewController.self, withArgArray: [BSNull(), "arg"]) as! MyViewController

Describe your class dependencies like this:

class House : NSObject {
    class override func bsInitializer() -> BSInitializer {
        // `selector` requires the Objective-C method name of your initializer
        return BSInitializer(withClass: self, selector: "initWithAddress:", argumentKeysArray: [Address.self])
    }

    class override func bsProperties() -> BSPropertySet {
        let propertySet = BSPropertySet(withClass: self, propertyNamesArray: ["garage"])
        propertySet.bindProperty("color", toKey: "myHouseColor")
        return propertySet
    }
}

Note that Blindside is only able to create and inject NSObject-derived classes.

Author

Copyright (c) 2012 JB Steadman. This software is licensed under the MIT License.

blindside's People

Contributors

akitchen avatar briancroom avatar christian-smith avatar jbsf avatar jeffh avatar joemasilotti avatar lalunamel avatar odlp avatar wileykestner avatar xtreme-steven-wu avatar zacclark avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

blindside's Issues

Integration issues when used in a framework with cocoapods 1.2.1

When installing Blindside 1.3.1 in my framework using cocapods 1.2.1 and then attempting to integrate my framework in the application, I get the following error:

dyld: Library not loaded: @rpath/Blindside.framework/Blindside
  Referenced from: /Users/tsd036/Library/Developer/CoreSimulator/Devices/13CB5F22-8253-4D2B-A5DE-350D4C50271D/data/Containers/Bundle/Application/27786704-461A-4687-A211-BEF96D83D4DE/aaa-ace-mobile.app/Frameworks/MyFramework.framework/MyFramework
  Reason: image not found

This only started happening when I upgraded to cocoapods 1.2.1 because in cocoapods 1.1.1 it allowed "use_frameworks!" to be embeded inside our swift testing target for Quick and Nimble. Cocoapods 1.2.1 requires "use_frameworks!" to also be included in the parent target which is the framework.

Delegates - Cyclic dependencies

We're trying to set up a dependency graph which includes a delegate with a reference back up the graph. When we try to inject all the deps with Blindside we get an exception "Cyclic dependency found on key…".

How can we use BS to inject an object's delegate? The delegate has a weak reference to a protocol going back up the chain, so we'd expect it to be ok.

Blindside should have a means of notifying an object when its properties have been injected

In places where the use of a BSPropertySet is unavoidable (for example, a view controller instantiated from a storyboard), it would be helpful if Blindside could call something like a "bsAwakeFromPropertyInjection" method on the object after the properties have been injected. It could be added to the category on NSObject.

I may do a pull request unless there is a compelling reason not to do this.

Cocoapods public spec repo

The last step to supporting Cocoapods is to add Blindside's podspec to Cocoapod's specs repo (more info) so it can actually be added by developers. For example, on my last project Blindside was the only external dependency we needed to add as a submodule instead of a pod dependency.

I'd like to go ahead and add Blindside to the public cocoapods repo since I'm on the beach this week anyway. Let me know if you're good with me handling it.

Swift documentation doesn't mention the need to cast return value of `injector.instance`

For example, in the Swift section there is a line

let controller = injector.getInstance(MyViewController.self)

which compiles, but doesn't allow you to use the variable controller because it is an unknown type.

In order for that variable to be recognized as an instance of MyViewController it should be

let controller = injector.getInstance(MyViewController.self) as! MyViewController
OR
let controller: MyViewController = injector.getInstance(MyViewController.self) as! MyViewController

As someone new to Swift, that bit tripped me up.

Official Cocoapod entry

Would it be possible to add this to the official Cocoapods spec? I would like to use this as a dependency for another Pod but it needs to be 'official'.

Playing around on my machine, it should be as easy as:

  1. Update Blindside.podspec to use 1.0.0 as the version (example in my fork)
  2. Create a 1.0.0 tag
  3. pod spec lint (should pass)
  4. pod trunk push Blindside.podspec

Thanks for considering! I would do it on my own but it would have to be under my repo. And then that pod name would be gone.

Injecting dependencies into Swift objects

Is it possible to inject dependencies into Swift objects? Or, one step away, classes written in Swift but still inheriting from NSObject?

I can override bsInitializer() no problem but the "Swiftified" header for BSInitializer shows no initializer methods. Same goes for BSPropertySet.

Command-click on BSPropertySet() from a Swift file to see the header I am speaking of.

Warn users that the object they're requesting holds has a key from a different bundle

Given that you're writing a spec for a class under test (DogPicsViewController)

Given that you accidentally include ImageFetcher in your application target (DogPics) and your test target (DogPicsSpecs) that calls the main application it's host (DogPics is the host application of DogPicsSpecs)

Given than you inject a custom implementation (FakeImageFetcher) of a dependency of the class under test into the injector
[injector bind:[ImageFetcher class] toInstance: fakeImageFetcher]

Given that you ask for an instance of that class (ImageFetcher) in the class under test, either through bsinitializer or bsproperties

@interface DogPicsViewController
  @property (strong, nonatomic) ImageFetcher *imageFetcher;
@end

When the injector goes to grab the requested ImageFetcher for the class DogPicsViewController while running a spec, it will initialize a new instance of ImageFetcher even though you've faked out and bound an instance in your specs.

This happens because you've included your ImageFetcher in both your host application and spec bundles.

To somebody who hasn't already encountered this a few times, this behavior is totally impossible to figure out ("why's it not getting the thing I just bound? Aren't these two classes equal?")

The injector should warn the user that this is happening.

The world implodes when I bind to a previously bound class, then attempt retrieve an instance

We found ourselves in a situation where we wanted to override a binding; the key was a class which had been previously bound to an instance. The world imploded.

        context(@"when the key is a class which has an existing binding", ^{
            it(@"should build the class without infinite recursion", ^{
                [injector bind:[Address class] toInstance:[NSNull null]];
                [injector bind:[Address class] toClass:[Address class]];
                id address = [injector getInstance:[Address class]];
                expect(address).to(be_instance_of([Address class]));
            });
        });

Alternatively, is there a recommended way to remove a binding to restore the default behavior? This came up in a testing scenario.

Ahead-of-time runtime configuration validation

This is both an RFC as well as a "todo" reminder for myself, or another enterprising contributor.

Objective-C's dynamic nature and the way Blindside interacts with it means that only a limited amount of compile-time checking can be performed on an injector's configuration and classes' dependency declarations. Additionally, runtime validity checking is performed on a just-in-time basis, resulting in the possibility of unexpected exceptions lurking behind every call to ``getInstance:`. I would propose providing opt-in ahead-of-time validity checking, available at the time of injector instantiation, after module configuration has been performed.

I imagine these checks could be run as part of an integration test suite, or even upon standard app launches for debug builds.

I've thought of the following possible checks so far:

  • Ensure that all classes' BSInitializerdeclarations refer to an available initializer method (see -[BSInitializer validate]
  • Ensure all BSInitializer argument keys actually exist (a key can be considered to exist if it either a) is explicitly bound in the injector, or b) is a class which Blindside can construct without binding.)
  • Ensure all BSPropertySet declarations refer to usable properties on the class (see -[BSProperty determineReturnType])
  • Ensure all BSPropertySet bindings refer to keys which actually exist
  • Check for obvious circular dependencies

I would suggest that the entry point for these checks be a new method introduced on the Blindside factory class which expands upon the existing methods, adding one or more extra parameters for opting into the configuration validity checking. Using a bit mask option set or a parameter object could be a good way to allow future extensions to this feature without requiring breaking API changes.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.