Dependency Injection (DI) is a software design pattern where an object receives its dependencies from an external source rather than creating them internally. This promotes loose coupling, making code more modular, testable, and easier to maintain. Instead of a class creating its own dependencies, it receives them as arguments (constructor injection), properties (setter injection), or through an interface (abstract entity injection). The term “abstract entity” used in this document shall refer to either an interface or abstract class.
Thg DependencyV1 class allows an abstract entity to be bound to a concrete implementation. It supports setter and abstract entity injection. An application can use one of the inject methods to perform injection. This allows the same implementation class to be used in many places in the code base. It allows for the`implementation to be changed application wide easily.
The custom object, Binding__c, may be used to configure the binding from the property or abstract entity to a concrete class.
By default, the class injected into an application will be a singleton. For this reason, the implementation class must not have state that may change in use.
If an implementation class is required to have state, then the class must implement the DependencyV1.Prototype interface.
See the reference Apex docs for the API. It is suggested that you keep a page open with the Apex docs loaded for your reference whilst reading this page.
If you wish to try the Dependency Injection example code, see Geting Started.
A binding is the mapping from a property or abstract entity to its concrete implementation. The bindings are stored in the DependencyV1 class as a Map called the registry. The key to the registry is the Type of the property or abstract entity.
Two types of binding are supported.
For the first type, the abstract entity can only have one binding in the registry.
The second type can have multiple bindings of the property or abstract entity in the registry. Each must have a unique action.
Each implementation class must have a public or global no-op constructor. An exception will be thrown if an attempt is made to bind an abstract entity to a class that doesn’t. To allow a class to be bound that has a private or protected no-op constructor, the TypesV1.Factory interface can be used. Create an inner class in the class to be registered that implements the interface.
See Types for more information.
The registry may be initialised either programmatically or through the Binding__c custom object.
See BindingInitialisation.cls for examples of programmatic and custom object binding initialisation.
Programmatic initialisation can be performed using the DependencyV1.bind method. If an application requires a default registry to be setup, the application must call these methods to add the bindings.
The bindings can be configured using the Binding__c custom object. The values in the custom object records will override the bindings currently in the registry.
The fields of the custom object are.
Field | Type | Description |
---|---|---|
Type__c | Mandatory | The class name of the property or abstract entity to be bound. |
Implementation__c | Mandatory | The class name of the concrete class that implements the abstract entity. |
Action__c | Optional | The action to be used in combination with the Type__c field to identity the binding. |
All Types to be bound to an implementation will be validated as they are bound. Any new Type to be bound must have a validator registered for the Type. To do this, a new BindingCheck metadata record must be added which defines the validator used to validate the Type.
Validation of the implementation bound to a Type is performed by adding a class that implements the Dependency.BindingCheck interface and registering it in a custom metadata for the Type. record.
The BindingCheck__mdt custom metadata object has the following fields;
Field | Type | Description |
---|---|---|
Type__c | Mandatory | The class name of the property or abstract entity to be validated. |
BindingCheck__c | Mandatory | The class name of the BindingCheck implementation to validate the binding. |
IsUnitTest__c | Mandatory | If true, the BindingCheck is for unit test use only. |
The BindingCheck.validate method returns a Dependency.ValidationResult object which notifies the caller of the result of the validation. If a failure notification is returned, a DependencyV1.APIException is thrown with the message set to the value recorded in the ValidationResult.errorMessage field.
The classes/BindingChecks.cls class and customMetadata directory in the example directory show how to register a BindingCheck for each of the example classes.
Important: An Exception will be thrown on the first use of the DependencyV1 class if any of the bindings added to it either programmatically, or from the custom object, do not have a BindingCheck class to validate it.
Injection is the import of the concrete class bound to the Type into an application . The binding can be assigned to an interface or abstract class (abstract entity binding) or to a variable or member variable (property binding).
Implementations of interfaces and abstract classes defined in the registry can be injected into an application. The abstract entity implementation should provide functionality restricted to an interface or abstract class. To meet the Single Use of SOLID and Separation of Concerns requirements, it must not be possible to cast the injected abstract entity to its implementation class to access additional functionality.
See AbstractEntityInjection.cls for an example of injecting an abstract entity.
The default binding in the example has been configured to create the Account synchronously.
Run AbstractEntityInjection.reset in Anonymous Apex to clear any current Bindings for AccountManager.
Run AbstractEntityInjection.example(‘Joe Blogs) in Anonymous Apex. You will see the following debug output. The Account has been created in the scope of the current request and can be SELECTed using SOQL.
USER_DEBUG|[23]|DEBUG|Account:{Name=Joe Bloggs, Id=****************}
DEBUG|(Account:{Id=****************, Name=Joe Bloggs})
There may be a use case for a customer where they don’t want the new Account to be returned by a SOQL SELECT in the scope of the request. With the synchronous implementation of the interface, the Account is created in the scope of the current request so will always be returned by a SOQL SELECT.
How can we defer creation of the Account till after the current request is completed? We can use an Apex Job. Add an Apex Job to create the Account and the job will be run when the current request returns control to Salesforce.
To try this out, add the binding to enable the creation of the Account asynchronously. Log on to your org. From the App Launcher select Force Framework. Select the Dependency Bindings tab. Create a new record. Assign it the following values.
Field | Value |
---|---|
Type | AccountClasses.AccountManager |
Implementation | AccountClasses.AsyncAccountManager |
Run AbstractEntityInjection.example(‘Joe Blogs) in Anonymous Apex, you will see the following debug output.
USER_DEBUG|[23]|DEBUG|null
DEBUG|DEBUG|()
The second DEBUG statement shows that the Account has not been created in the scope of the current request.
Confirm that an Account named Joe Bloggs has been created. And use Setup > Apex Jobs to confirm that a Queueable of class AccountClasses ran recently which created the Account.
For any customers who want the Account to be created outside the scope of the current request, you can enable that by simply adding a Binding to their org. No application logic needs to be changed.
Values can be injected into an application as assignments to variables, including member variables.
See PropertyInjection.cls for an example of injecting a property.
The default registry entry for the animals Map has 100 sheep, 50 cows and 2000 hens. If you run PropertyInjection.run in Anonymous Apex, you will see the following debug output.
DEBUG|The farm has 100 sheep
DEBUG|The farm has 50 cows
DEBUG|The farm has 2000 hens
There is an inner class named NewConfiguration in the PropertyInjection class. This can be used to override the base registry binding for the Map setup in the test.
Run the following DML in Anonymous Apex. If using the Force Framework package, you will need to add the package namespace, forcefw__, to the object and the fields.
insert new Binding__c(Type__c = (Map<String, Integer>.class).getName(), Action__c = 'animals', Implementation__c = PropertyInjection.NewConfiguration.class.getName());```
The new registry entry for the animals Map has 1 sheep, 2 cows and 3 hens. If you run PropertyInjection.run in Anonymous Apex, you will see the following debug output.
DEBUG|The farm has 1 sheep
DEBUG|The farm has 2 cows
DEBUG|The farm has 3 hens
The new values have been successfully injected into the application.
Log on to your org. From the App Launcher select Force Framework. Select the Dependency Bindings tab. If you choose All, you will see the binding that was added to bind the class with the new configuration to the animals Map.
Delete the Binding. Then run PropertyInjection.run in Anonymous Apex, you will see that the values displayed are now the default values.
In the Dependency Bindings tab, create a new record. Assign it the following values.
Field | Value |
---|---|
Type | Map<String,Integer> |
Action | animals |
Implementation | PropertyInjection.NewConfiguration |
Run PropertyInjection.run in Anonymous Apex, you will see that the new configuration values are displayed.
If this was production, you’d have just re-configured your application without having to create custom metadata, a custom object or a custom setting to manage the configuration. To change the configuration, you just need to write a new Apex class and add a binding to the registry.
Trying to inject a Type into an application for which there is no binding in the registry will throw an Exception. The DependencyV1.isBound methods can be used to check if a binding exists.
See HasInjection.cls for an example of checking for a binding’s existence.
Before running any of the example code, run HasInjection.reset from Anonymous Apex. This will clear all the bindings for AccountClasses.AccountManager.
Run HasInjection.has(‘Joe Bloggs’) from Anonymous Apex. You should see the following DEBUG message. This shows that no Binding exists for the AccountClasses.AccountManager interface.
DEBUG|Injection for AccountClasses.AccountManager does not exist
Log on to your org. From the App Launcher select Force Framework. Select the Dependency Bindings tab. Create a new record. Assign it the following values;
Field | Value |
---|---|
Type | AccountClasses.AccountManager |
Implementation | AccountClasses.SyncAccountManager |
Run HasInjection.has(‘Joe Bloggs’) from Anonymous Apex. You should no longer see the DEBUG message recording a non-existent binding. And an Account named Joe Bloggs should have been added to the org.
Run HasInjection.action(‘Joe Bloggs’) from Anonymous Apex. You should see the following DEBUG message. This shows that no Binding exists for the AccountClasses.AccountManager interface with an action of ASYNC.
DEBUG|Injection for AccountClasses.AccountManager with action ASYNC does not exist
Log on to your org. From the App Launcher select Force Framework. Select the Dependency Bindings tab. Create a new record. Assign it the following values.
Field | Value |
---|---|
Type | AccountClasses.AccountManager |
Action | ASYNC |
Implementation | AccountClasses.AsyncAccountManager |
Run HasInjection..action(‘Joe Bloggs’) from Anonymous Apex. You should no longer see the DEBUG message recording a non-existent binding. And an Account named Joe Bloggs should have been added to the org.
To check the Account was added asynchronously, go to Setup > Apex Jobs and confirm a Queueable of class AccountClasses was run.