Files
firebase-ios-sdk/Interop/Firebase Component System.md
T

450 lines
18 KiB
Markdown

# Using Core's Component System
FirebaseCore has a dependency injection system (referred to as "Interop") used to depend on
functionalities provided by other Firebase SDKs. This gives the ability to depend on a typesafe
interface-only API to consume without depending on the entire SDK and simulates optional
dependencies - depending on the definition but not the implementing SDK and only functioning when
the implementing SDK is included.
//TODO(ryanwilson): Table of Contents
## Overview
When a Firebase SDK wants to provide functionality to another Firebase SDK, it must be done through
the Interop system. Both SDKs depend on a shared protocol that describes the functionality provided
by one SDK and required by the other. Let's use `A` and `B`, where `B` depends on functionality
provided by `A` and the functionality is described by protocol `AInterop`.
During configuration, `A` tells Core that it provides functionality for `AInterop` and `B` tells
Core it would like functionality `AInterop` (and specifies whether it is required or optional) as
well as how to instantiate an instance of `B`. When a developer requests `B`, FirebaseCore
instantiates `B` and passes a container that contains the instance of `A` that provides `AInterop`.
`B` has no idea what class `A` is, and it doesn't need to. All `B` needs to know is that it has an
instance of an object that conforms to `AInterop` and provides the functionality it needs.
This system allows Firebase SDKs to depend on each other in a typesafe way and allows us to
explicitly declare version dependencies on the interfaces required instead of the SDK version.
## Protocol Only Frameworks
In order to share protocols between two SDKs, we introduced header only frameworks that declare the
desired protocol(s).
The naming convention for the framework should be `<SDKName>Interop` but the protocols don't have a
strict naming guideline other than having a `FIR` prefix in Objective-C.
Both the implementing and dependent SDK will have a required dependency on this `<SDKName>Interop`
framework: the implementing SDK must conform to the protocols defined and register it with Core,
while the dependent SDK will use the protocol definition to use methods defined by it.
An Interop framework can have multiple protocols, but all should be implemented by the SDK it is
named after.
Protocols *can not* declare class methods. This is an intentional decision to ensure all interfaces
interact properly based on the `FIRApp` that's used.
## Types and Core API
For the rest of the documentation, it's important to be familiar with the various classes and API
provided by Core. Since the SDKs are written in Objective-C, we'll use the Objective-C names. The
Swift names are identical but dropping the `FIR` prefix.
- `@class FIRDependency`
- A dependency on a specific protocol's functionality. Created with the factory method
`[FIRDependency dependencyWithProtocol:isRequired:]`
- `@class FIRComponent`
- A component to register with Core to be consumed by other SDKs. It declares the protocol
offered, dependencies, and a block for Core to instantiate it.
- `@class FIRComponentContainer`
- A container that holds different components that are registered with Core.
- `@protocol FIRComponentRegistrant`
- Describes functionality for SDKs registering components in the `FIRComponentContainer`. It
allows Core to fetch components lazily from the SDK.
- `#define FIR_COMPONENT(protocol, container)` (macro)
- The macro to request an instance conforming to a given protocol from a container. Due to
Objective-C's lightweight generic system, the safest and most readable API is provided by a
macro that uses internal types to give compiler warnings if a developer tries to assign the
result to a variable with the incorrect type.
## Registering with Core
Each Firebase SDK should register with Core in the `+load` method of the class conforming to
`FIRComponentRegistrant`. This needs to happen at `+load` time because Core needs to resolve any
dependencies before a class has a chance to be called by a developer (if called at all).
```
#import <FirebaseCore/FIRComponentContainer.h>
#import <FirebaseCore/FIRComponentRegistrant.h>
@interface FIRFoo <FIRComponentRegistrant>
@end
@implementation FIRFoo
+ (void)load {
// Register with Core's container.
[FIRComponentContainer registerAsComponentRegistrant:self];
}
// TODO: Conform to `FIRComponentRegistrant`. See later sections for more information.
@end
```
### Singletons and Instance Management
All Firebase SDKs provide singleton access for convenience that map to a specific `FIRApp`:
`[FIRAuth auth]`, `[FIRFunctions functionsForApp:]`, etc. Some SDKs can also have multiple instances
per `FIRApp` such as Storage: `[FIRStorage storageForApp:URL:]`.
These instances must be created and managed by Core through the component system. This allows the
`FIRApp` lifecycle to control the lifecycle of instances associated with itself. There are different
ways to do so depending on the SDK's situation.
#### Single Instance per `FIRApp`
The registration for a single instance per `FIRApp` changes if the SDK provides functionality to
other SDKs or not.
##### SDK does not provide functionality (example: Functions)
In this case, the SDK is a "leaf node" since no SDKs depend on it. It has a private, empty protocol
that it uses to register with the container. Using Functions as an example:
```
// FIRFunctions.h
/// Empty protocol to register Functions as a component with Core.
@protocol FIRFunctionsInstanceProvider
@end
/// Privately conform to the protocol for component registration.
@interface FIRFunctions () <FIRFunctionsInstanceProvider, FIRComponentRegistrant>
@end
@implementation FIRFunctions
+ (void)load {
[FIRComponentContainer registerAsComponentRegistrant:self];
}
/// The array of components to register with Core. Since Functions is a leaf node and
/// doesn't provide any functionality to other SDKs, it should use Core for instance
/// management only.
+ (NSArray<FIRComponent *> *)componentsToRegister {
// Each component needs a block for Core to call in order to instantiate instances of the
// desired class.
FIRComponentCreationBlock creationBlock =
^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
// We want the same instance to be returned when requested from the container, enable
// `isCacheable`.
*isCacheable = YES;
// Use an appropriate initializer and inject anything required from the container.
return [[FIRFunctions alloc] initWithApp:container.app];
};
// Create the component that can create instances of `FIRFunctions`.
FIRComponent *internalProvider =
[FIRComponent componentWithProtocol:@protocol(FIRFunctionsInstanceProvider)
creationBlock:creationBlock];
// Return the array of components, in this case only the internal instance provider.
return @[ internalProvider ];
}
+ (FIRFunctions *)functionsWithApp:(FIRApp *)app {
// Get the instance from the `FIRApp`'s container. This will create a new instance the
// first time it is called, and since `isCacheable` is set in the component creation
// block, it will return the existing instance on subsequent calls.
id<FIRFunctionsInstanceProvider> instance =
FIR_COMPONENT(FIRFunctionsInstanceProvider, app.container);
// In the component creation block, we return an instance of `FIRFunctions`. Cast it and
// return it.
return (FIRFunctions *)instance;
}
// ... Other `FIRFunctions` methods.
@end
```
##### SDK provides functionality to other SDKs (example: Auth)
This example will be very similar to the one above, but let's define a simple protocol that Auth
could conform to and provide to other SDKs:
```
// FIRAuthInterop.h in the FirebaseAuthInterop framework.
@protocol FIRAuthInterop
/// Get the current Auth user's UID. Returns nil if there is no user signed in.
- (nullable NSString *)getUserID;
@end
```
```
// FIRAuth.m in the FirebaseAuth framework.
/// Privately conform to the protocol for interop and component registration.
@interface FIRAuth () <FIRAuthInteroperable, FIRComponentRegistrant>
@end
/// The components to register with Core.
+ (NSArray<FIRComponent *> *)componentsToRegister {
// Provide a component that will return an instance of `FIRAuth`.
FIRComponentCreationBlock authCreationBlock =
^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
// Cache so the same `FIRAuth` instance is returned each time.
*isCacheable = YES;
return [[FIRAuth alloc] initWithApp:container.app];
};
FIRComponent *authInterop =
[FIRComponent componentWithProtocol:@protocol(FIRAuthInteroperable)
creationBlock:authCreationBlock];
return @[authInterop];
}
+ (FIRAuth *)authWithApp:(FIRApp *)app {
// Use the instance from the provided app's container.
id<FIRAuthInteroperable> auth = FIR_COMPONENT(FIRAuthInteroperable, app.container);
return (FIRAuth *)auth;
}
```
#### Multiple Instances per `FIRApp`
Instead of directly providing an instance from the container, Firestore and similar SDKs should
create a "provider" that stores and creates instances with the required parameters. This means a
single provider per `FIRApp`, but multiple instances are possible per provider.
```
/// Provider protocol to register with Core.
@protocol FSTFirestoreMultiDBProvider
/// Cached instances of Firestore objects.
@property(nonatomic, strong) NSMutableDictionary<NSString *, FIRFirestore *> *instances;
/// Firestore can be initialized with an app as well as a database. The instance provider is already
/// associated with a `FIRApp` so pass in any other required parameters (in this case, just the
/// database string).
- (FIRFirestore *)firestoreForDatabase:(NSString *)database;
@end
```
Instead of the Firestore class conforming to `FSTInstanceProvider`, the work can be done in a
separate class to keep `Firestore.m` cleaner.
```
/// A concrete implementation for FSTFirestoreMultiDBProvider to create Firestore instances.
@interface FSTFirestoreComponent : NSObject <FSTFirestoreMultiDBProvider,
FIRComponentRegistrant>
/// The `FIRApp` that instances will be set up with.
@property(nonatomic, weak, readonly) FIRApp *app;
/// Cached instances of Firestore objects.
@property(nonatomic, strong) NSMutableDictionary<NSString *, FIRFirestore *> *instances;
/// Default method for retrieving a Firestore instance, or creating one if it doesn't exist.
- (FIRFirestore *)firestoreForDatabase:(NSString *)database;
/// Default initializer.
- (instancetype)initWithApp:(FIRApp *)app NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
@end
@implementation FSTFirestoreInstanceProvider
// Explicitly @synthesize because instances is part of the FSTInstanceProvider protocol.
@synthesize instances = _instances;
+ (void)load {
// Don't forget to register!
[FIRComponentContainer registerAsComponentRegistrant:self];
}
- (instancetype)initWithApp:(FIRApp *)app {
self = [super init];
if (self) {
_instances = [[NSMutableDictionary alloc] init];
_app = app;
}
return self;
}
/// `FSTFirestoreMultiDBProvider` conformance.
- (FIRFirestore *)firestoreForDatabase:(NSString *)database {
// Regular initialization code to create Firestore instances with required parameters...
}
// `FIRComponentRegistrant` conformance.
+ (NSArray<FIRComponent *> *)componentsToRegister {
// Ignore any dependencies for simplicity in this example.
FIRComponentCreationBlock creationBlock =
^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
*isCacheable = YES;
// NOTE: Instead of returning an instance of Firestore, return an instance of the
// instance provider.
return [[FIRFirestoreComponent alloc] initWithApp:container.app];
};
FIRComponent *firestoreProvider =
[FIRComponent componentWithProtocol:@protocol(FSTFirestoreMultiDBProvider)
creationBlock:creationBlock];
return @[ firestoreProvider ];
}
@end
```
All `Firestore.m` needs to do now is call the component container from the singleton calls:
```
+ (instancetype)firestoreForApp:(FIRApp *)app database:(NSString *)database {
id<FSTFirestoreMultiDBProvider> provider =
FIR_COMPONENT(FSTFirestoreMultiDBProvider, app.container);
return [provider firestoreForDatabase:database];
}
```
### Depending on Functionality from Another SDK
*If you haven't already read [Registering with Core](#registering-with-core), please do so until you
get back to this spot as it lays the groundwork necessary to understand this section.*
Adding dependencies is easy once components are registered with Core. Let's take the example from
Functions above and add a dependency to `FIRAuthInterop` defined above.
**Important**: You will also need to add a dependency on the `FirebaseAuthInterop` pod to your SDK's
podspec and any package manager supported.
Before adding the dependency on `FIRAuthInterop`.
```
+ (NSArray<FIRComponent *> *)componentsToRegister {
FIRComponentCreationBlock creationBlock =
^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
*isCacheable = YES;
return [[FIRFunctions alloc] initWithApp:container.app];
};
FIRComponent *internalProvider =
[FIRComponent componentWithProtocol:@protocol(FIRFunctionsInstanceProvider)
creationBlock:creationBlock];
return @[ internalProvider ];
}
```
After adding the dependency on `FIRAuthInterop`. See comments with "ADDED:".
```
+ (NSArray<FIRComponent *> *)componentsToRegister {
FIRComponentCreationBlock creationBlock =
^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
*isCacheable = YES;
// ADDED: Retrieve an instance that conforms to `FIRAuthInterop` from the container.
id<FIRAuthInterop> auth = FIR_COMPONENT(FIRAuthInterop, container);
// ADDED: Note the constructor has a new parameter: auth. It's good practice to inject
// the instance needed in the constructor instead of pulling it from the app
// passed in. This allows for better unit testing with fakes since any object
// can conform to `FIRAuthInterop` and be verified easily.
return [[FIRFunctions alloc] initWithApp:container.app auth:auth];
};
// ADDED: Define a dependency on the `FIRAuthInteroperable` protocol. Declare if the
// dependency is required or not.
FIRDependency *auth =
[FIRDependency dependencyWithProtocol:@protocol(FIRAuthInteroperable)
isRequired:NO];
// ADDED: A longer constructor is used to instantiate the `FIRComponent`; this time
// it includes instantiation timing and an array of dependencies. The timing
// allows components to be initialized upon configure time or lazily, when
// it is requested from the container. Pass in the `auth` dependency created
// above.
FIRComponent *internalProvider =
[FIRComponent componentWithProtocol:@protocol(FIRFunctionsInstanceProvider)
instantiationTiming:FIRInstantiationTimingLazy
dependencies:@[ auth ]
creationBlock:creationBlock];
return @[ internalProvider ];
}
```
Based on the new constructor, Functions can now use the `auth` instance as defined by the
protocol:
```
NSString *userID = [auth getUserID];
if (userID) {
// Auth is available and a user is signed in!
}
```
## Advanced Use Cases
### Providing Multiple Components and Sharing Instances
Consider a situation where an SDK wants to offer functionality defined in multiple protocols
with the same instance. For example, Auth could provide `FIRAuthUserInterop` and
`FIRAuthSignInInterop`. If a single Auth instance should be shared between those two protocols, the
system currently doesn't work.
In order to alleviate this, Auth could create a third (internal only) protocol
(`FIRAuthCombinedInterop`) that conforms to both `FIRAuthUserInterop` and `FIRAuthSignInInterop` and
becomes a dependency for each of those two components and returned in the component creation block.
An abbreviated code sample:
```
+ (NSArray<FIRComponent *> *)componentsToRegister {
// Standard creation block to get an instance of Auth.
FIRComponentCreationBlock authBlock =
^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
*isCacheable = YES;
return [[FIRAuth alloc] initWithApp:container.app];
};
FIRComponentCreationBlock combinedBlock =
^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
// No need to cache, let it use the cached value from the combined component.
return FIR_COMPONENT(FIRAuthCombinedInterop, container);
};
// Declare a self dependency on the combined interop component.
FIRDependency *auth =
[FIRDependency dependencyWithProtocol:@protocol(FIRAuthCombinedInterop)
isRequired:YES];
// Declare the three components provided.
FIRComponent *authComponent =
[FIRComponent componentWithProtocol:@protocol(FIRAuthCombinedInterop)
creationBlock:authBlock];
// Both the user and sign in components depend on the previous component as
// declared in the dependency above.
FIRComponent *userComponent =
[FIRComponent componentWithProtocol:@protocol(FIRAuthUserInterop)
instantiationTiming:FIRInstantiationTimingLazy
dependencies:@[ auth ]
creationBlock:combinedBlock];
FIRComponent *signInComponent =
[FIRComponent componentWithProtocol:@protocol(FIRAuthSignInInterop)
instantiationTiming:FIRInstantiationTimingLazy
dependencies:@[ auth ]
creationBlock:combinedBlock];
return @[ authComponent, userComponent, signInComponent ];
}
```