Polymerase
A service-oriented, Objective C and Swift dependency injection framework, for macOS and iOS.
Features dynamic Xcode storyboard integration for services and parameters.
This framework provides a dependency injection container class
(GCDIDefinitionContainer
) that has a standardised way of defining services
(objects) within the application. The premise is quite simple: you have to
tell the container how to construct your service, for when it is asked for. A
service may require other services to be injected into it via the initialiser,
or setters.
Services in the container are assigned identifiers, which are used to create references to them. A service definition is added to the container using the following code.
[_container setService:@"example_service" definition:^(GCDIDefinition *definition) {
[definition useClassNamed:@"MyExampleClass"];
[definition useInitializerNamed:@"initWithDependency:"];
[definition injectArguments:@[
[GCDIReference referenceForServiceId:@"dependency_service"],
]];
}];
container.setService("example_service", definition:{ (definition: GCDIDefinition?) in
definition!.klass = "MyExampleService" // Or with namespace: MyApplication.MyExampleService
definition!.initializer = "init(dependency:)"
definition!.arguments = [
GCDIReference(forServiceId:"dependency_service"),
];
});
It may seem strange to record a class name and a selector as a string;
definitions do have methods that support passing Class
and SEL
types if you
wanted to add definitions to the container using code, definitions do still
convert the Class
into a string. The benefit of recording this data in
string format, means that do not need to use code to store the definitions for
all of our services, and code can be used for actual logic.
Examples using Class
and SEL
types:
[_container setService:@"example_service" definition:^(GCDIDefinition *definition) {
[definition useClass:[MyExampleClass class]];
[definition useInitializer:@"initWithDependency:"];
[definition injectArguments:@[
[GCDIReference referenceForServiceId:@"dependency_service"],
]];
}];
container.setService("example_service", definition:{ (definition: GCDIDefinition?) in
definition!.use(MyExampleService.self)
definition!.useInitializer(#selector(MyExampleService.init(dependency:)))
definition!.arguments = [
GCDIReference(forServiceId:"dependency_service"),
];
});
Move over, property list.
One of the biggest advantages of dependency injection, is that it also centralises where objects are constructed, making it easier for others to understand how the application works. Service definitions can all be defined in the one place, using simple key-value recording. While the obvious choice is property lists, these are a dated, and ugly way of representing basic data - not to mention requiring specialised software in order to present and manipulate them in a friendly format.
Cue YAML language: a easy to read text format, for storing configuration,
that supports all the basic data types (strings, numbers, dictionaries and
arrays). A YAML compiler script is bundled with this framework, that you can use
with a build rule, to allow you to write all of your service definitions and
define parameters in YAML format. An Objective C Yaml category implementation
for your container class will be generated, and compiled with your project. This
means that when you call [[MyContainerClass alloc] init]
, your container will
already have all of your service definitions loaded into it, and be ready to go.
# Tell the compiler what class to generate the Yaml category implementation for.
# The compiler by default will use the basename of the YAML file, therefore
# without this line, you would need to call this file MyContainerClass.yml
ContainerClass: MyContainerClass
Language: ObjC # or 'Swift', defaults to ObjC.
# Define parameters that are to be added to the container.
Parameters:
car_factory.class: CarFactory
spark_plug_factory: '@a_spark_plug_factory'
# Service definitions are listed here.
Services:
car_factory:
Class: '%car_factory.class%' # Parameter substituted.
# Initializer: 'init' is used by default.
engine_factory:
Class: EngineFactory
Initializer: 'initWithSparkPlugFactory:' # 'init(sparkPlugFactory:)' in swift.
Arguments:
- '%spark_plug_factory%' # Parameter substituted.
a_spark_plug_factory:
Class: EngineSparkPlugFactory
Parameters
Parameters are referenced by using strings enclosed in %
's. For example,
'%car_factory.class%'
will resolve to 'CarFactory'
here. Parameters do not
have to be strings, however you are able to substitute string parameters within
strings ('I want to %foo%'
).
Dynamic parameters can be added at runtime:
[_container setParameter:@"NSBundle.mainBundle" value:[NSBundle mainBundle]];
container.setParameter("NSBundle.mainBundle", value:NSBundle.mainBundle())
The above can be called after (or during) initialising the container, however
you can still use %NSBundle.mainBundle%
as a parameter in the YAML file, as it
will be available when the container is constructing the service.
Services
References to other services are created by using strings starting with @
,
followed by the name of the service that is being referenced. You can use @?
to just pass in nil
, if the service definition does not exist when the
container is constructing the service; rather than throwing a service not found
exception. You can also escape creating a service reference with @@
- this
will just give you a string starting with @. Service reference strings can be
nested within arrays and dictionaries.
YAML arrays and hashes will also be converted to NSArray
and NSDictionary
data types, numbers will be converted into NSNumber
. For example:
ingenium:
Factory: '@engine_factory'
Initializer: 'engineWithBrand:andCylinders:' # 'engineWithBrand:(_:andCylinders:)'
Arguments:
- 'INGENIUM'
- 4
Shared: false
jaguar:
Factory: '@car_factory'
Initializer: 'carWithConfig:andEngine:' # 'carWithConfig(_:andEngine:)'
Arguments:
- { Brand: 'Jaguar', Wheels: 4 }
- '@ingenium'
Shared: false
The jaguar
configuration will invoke the selector carWithConfig:andEngine:
on the car_factory
service, with the dictionary @{ @"Brand": @"Jaguar", @"Wheels": @4 }
, and fetch an ingenium
engine from the above service
definition. By stating that a service is not Shared
, the container will not
create a shared singleton instance; therefore whenever a jaguar
or ingenium
engine is requested from the container, a new instance will be created.
Compiling the Yaml implementation for your container class
You need to add a build rule for YAML files that uses the YAML compiler in this
framework, and add the output implementation file to the build. This only
generates the Yaml category implementation (@implementation (Yaml)
), so you
will still need to have both an @interface
and @implementation
for your
class.
Storyboard integration.
Storyboard integration is easily achieved, and all you have to do is tell the
container to inject into storyboards. One important factor here, is that the
container is initialised before the storyboard has been constructed. Otherwise,
the container will not be able to inject dependencies into your view
controllers. The best place to do this is in your AppDelegate's - (id)init
method; and assign it to a strong/retained property on your AppDelegate.
_container = [[MyContainerClass alloc] init];
[_container setContainerInjectsIntoStoryboards:TRUE];
In order to have a service injected into your controller, all you need to do is
utilise the User Defined Runtime Attributes interface, in Xcode. By adding
attributes with the Key Path of the property that you would like to inject the
service into, Type String
, with Value of @example_service
, the service
will be constructed by the container and injected in. All of the substitutions
available in the YAML file are valid, such as passing parameters with esclosing
%
's.
Include framework
#import <Polymerase/Polymerase.h>
import Polymerase
Libraries for embedding
Download 1.0.0-alpha3 frameworks for iOS and OSX (Objective C and Swift)
Documentation is currently in the process of being put together, there are a series of todo's, however feel free to raise any feature requests.
Bugs and feature requests
Please use the GitHub issues page to raise these.
This framework has been largely modelled after the Symfony dependency injection component, for PHP.
Jake Wise
© 2016 GroovyCarrot