-
Notifications
You must be signed in to change notification settings - Fork 511
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow for different implementations of Application factories
Fully backwards compatible with the static maps. Old factories show a deprecated message with instruction on refactoring to the new classes. This change allows for other implementation that use a dynamic assignment, similar to force-di.
- Loading branch information
William Velzeboer
committed
Sep 25, 2020
1 parent
b5c833f
commit 14da8f1
Showing
21 changed files
with
1,181 additions
and
183 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
FFLib Apex Common | ||
================= | ||
|
||
# Application class | ||
|
||
### Example - Binding based on static maps in Apex | ||
|
||
```apex | ||
public with sharing class Application | ||
{ | ||
public static final fflib_ServiceBindingResolver Service = | ||
new fflib_ClassicServiceBindingResolver( | ||
new Map<Type, Type> | ||
{ | ||
AccountsService.class => AccountsServiceImp.class | ||
} | ||
); | ||
public static final fflib_SelectorBindingResolver Selector = | ||
new fflib_ClassicSelectorBindingResolver( | ||
new Map<SObjectType, Type> | ||
{ | ||
Account.SObjectType => AccountsSelector.class | ||
} | ||
); | ||
public static final fflib_DomainBindingResolver Domain = | ||
new fflib_ClassicDomainBindingResolver( | ||
Application.Selector, | ||
new Map<SObjectType, Type> | ||
{ | ||
Schema.Account.SObjectType => Accounts.Constructor.class | ||
} | ||
); | ||
public static final fflib_Application.UnitOfWorkFactory UnitOfWork = | ||
new fflib_Application.UnitOfWorkFactory( | ||
new List<SObjectType> | ||
{ | ||
Account.SObjectType, | ||
Contact.SObjectType | ||
} | ||
); | ||
} | ||
``` |
129 changes: 129 additions & 0 deletions
129
...rce/apex-common/application/classes/implementation/fflib_ClassicDomainBindingResolver.cls
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
/** | ||
* File Name: fflib_ClassicDomainBindingResolver | ||
* Description: Domain class binding resolver based on the classic AEP 1.0 definition with static maps | ||
* Copyright (c) 2020 Johnson & Johnson | ||
* | ||
* @author : architect ir. Wilhelmus G.J. Velzeboer | wvelzebo@its.jnj.com | ||
*/ | ||
public virtual class fflib_ClassicDomainBindingResolver implements fflib_DomainBindingResolver | ||
{ | ||
protected fflib_SelectorBindingResolver selectorBindingResolver; | ||
|
||
protected Map<Schema.SObjectType, Type> domainConstructorBySObjectType; | ||
|
||
protected Map<Schema.SObjectType, fflib_ISObjectDomain> mockDomainInstanceBySObjectType; | ||
|
||
/** | ||
* Class constructor | ||
* | ||
* @param selectorBindingResolver | ||
* @param domainConstructorBySObjectType | ||
*/ | ||
public fflib_ClassicDomainBindingResolver( | ||
fflib_SelectorBindingResolver selectorBindingResolver, | ||
Map<SObjectType, Type> domainConstructorBySObjectType) | ||
{ | ||
this.selectorBindingResolver = selectorBindingResolver; | ||
this.domainConstructorBySObjectType = domainConstructorBySObjectType; | ||
this.mockDomainInstanceBySObjectType = new Map<SObjectType, fflib_ISObjectDomain>(); | ||
} | ||
|
||
/** | ||
* Dynamically constructs an instance of a Domain class for the given record Ids | ||
* Internally uses the Selector Factory to query the records before passing to a | ||
* dynamically constructed instance of the application Apex Domain class | ||
* | ||
* @param recordIds A list of Id's of the same type | ||
* @exception Throws an exception via the Selector Factory if the Ids are not all of the same SObjectType | ||
* | ||
* @return Instance of fflib_ISObjectDomain containing the record with the provided Ids | ||
**/ | ||
public virtual fflib_ISObjectDomain newInstance(Set<Id> recordIds) | ||
{ | ||
return newInstance(selectorBindingResolver.selectById(recordIds)); | ||
} | ||
|
||
/** | ||
* Dynamically constructs an instance of the Domain class for the given records | ||
* Will return a Mock implementation if one has been provided via setMock | ||
* | ||
* @param records A concrete list (e.g. List<Account> vs List<SObject>) of records | ||
* @exception Throws an exception if the SObjectType cannot be determined from the list | ||
* or the constructor for Domain class was not registered for the SOBjectType | ||
* | ||
* @return Instance of fflib_ISObjectDomain containing the provided records | ||
**/ | ||
public virtual fflib_ISObjectDomain newInstance(List<SObject> records) | ||
{ | ||
SObjectType domainSObjectType = records.getSObjectType(); | ||
if (domainSObjectType == null) | ||
throw new DeveloperException('Unable to determine SObjectType'); | ||
|
||
// Mock implementation? | ||
if (mockDomainInstanceBySObjectType.containsKey(domainSObjectType)) | ||
return mockDomainInstanceBySObjectType.get(domainSObjectType); | ||
|
||
// Determine SObjectType and Apex classes for Domain class | ||
Type domainConstructorClass = domainConstructorBySObjectType.get(domainSObjectType); | ||
if (domainConstructorClass == null) | ||
throw new DeveloperException('Domain constructor class not found for SObjectType ' + domainSObjectType); | ||
|
||
// Construct Domain class passing in the queried records | ||
fflib_SObjectDomain.IConstructable domainConstructor = | ||
(fflib_SObjectDomain.IConstructable) domainConstructorClass.newInstance(); | ||
return (fflib_ISObjectDomain) domainConstructor.construct(records); | ||
} | ||
|
||
/** | ||
* Dynamically constructs an instance of the Domain class for the given records and SObjectType | ||
* Will return a Mock implementation if one has been provided via setMock | ||
* | ||
* @param records A list records | ||
* @param domainSObjectType SObjectType for list of records | ||
* @exception Throws an exception if the SObjectType is not specified or if constructor for Domain class was not registered for the SObjectType | ||
* | ||
* @remark Will support List<SObject> but all records in the list will be assumed to be of | ||
* the type specified in sObjectType | ||
* | ||
* @return Instance of fflib_ISObjectDomain containing the provided records | ||
**/ | ||
public virtual fflib_ISObjectDomain newInstance(List<SObject> records, SObjectType domainSObjectType) | ||
{ | ||
if (domainSObjectType == null) | ||
throw new DeveloperException('Must specify sObjectType'); | ||
|
||
// Mock implementation? | ||
if (mockDomainInstanceBySObjectType.containsKey(domainSObjectType)) | ||
return mockDomainInstanceBySObjectType.get(domainSObjectType); | ||
|
||
// Determine SObjectType and Apex classes for Domain class | ||
Type domainConstructorClass = domainConstructorBySObjectType.get(domainSObjectType); | ||
if (domainConstructorClass == null) | ||
throw new DeveloperException('Domain constructor class not found for SObjectType ' + domainSObjectType); | ||
|
||
// Construct Domain class passing in the queried records | ||
fflib_SObjectDomain.IConstructable2 domainConstructor = | ||
(fflib_SObjectDomain.IConstructable2) domainConstructorClass.newInstance(); | ||
return (fflib_ISObjectDomain) domainConstructor.construct(records, domainSObjectType); | ||
} | ||
|
||
public void replaceWith(SObjectType domainSObjectType, Type domainConstructorImplementationType) | ||
{ | ||
this.domainConstructorBySObjectType.put(domainSObjectType, domainConstructorImplementationType); | ||
} | ||
|
||
public virtual void setMock(fflib_ISObjectDomain mockDomain) | ||
{ | ||
setMock(mockDomain.sObjectType(), mockDomain); | ||
} | ||
|
||
public virtual void setMock(Schema.SObjectType domainSObjectType, fflib_ISObjectDomain mockDomain) | ||
{ | ||
mockDomainInstanceBySObjectType.put(domainSObjectType, mockDomain); | ||
} | ||
|
||
/** | ||
* Exception representing a developer coding error, not intended for end user eyes | ||
**/ | ||
public class DeveloperException extends Exception {} | ||
} |
5 changes: 5 additions & 0 deletions
5
...common/application/classes/implementation/fflib_ClassicDomainBindingResolver.cls-meta.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata"> | ||
<apiVersion>48.0</apiVersion> | ||
<status>Active</status> | ||
</ApexClass> |
123 changes: 123 additions & 0 deletions
123
...e/apex-common/application/classes/implementation/fflib_ClassicSelectorBindingResolver.cls
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
/** | ||
* File Name: fflib_ClassicSelectorBindingResolver | ||
* Description: Selector class binding resolver based on the classic AEP 1.0 definition with static maps | ||
* Copyright (c) 2020 Johnson & Johnson | ||
* | ||
* @author : architect ir. Wilhelmus G.J. Velzeboer | wvelzebo@its.jnj.com | ||
*/ | ||
public virtual class fflib_ClassicSelectorBindingResolver | ||
implements fflib_SelectorBindingResolver | ||
{ | ||
|
||
protected Map<SObjectType, Type> selectorImplementationTypeBySObjectType; | ||
protected Map<SObjectType, fflib_ISObjectSelector> selectorMockBySObjectType; | ||
|
||
/** | ||
* Class constructor | ||
*/ | ||
public fflib_ClassicSelectorBindingResolver(Map<SObjectType, Type> selectorImplementationTypeBySObjectType) | ||
{ | ||
this.selectorImplementationTypeBySObjectType = selectorImplementationTypeBySObjectType; | ||
this.selectorMockBySObjectType = new Map<SObjectType, fflib_ISObjectSelector>(); | ||
} | ||
|
||
/** | ||
* Creates a new instance of the associated Apex Class implementing fflib_ISObjectSelector | ||
* for the given SObjectType, or if provided via setMock returns the Mock implementation | ||
* | ||
* @param sObjectType An SObjectType token, e.g. Account.SObjectType | ||
* | ||
* @return Instance of the selector implementation | ||
**/ | ||
public virtual fflib_ISObjectSelector newInstance(Schema.SObjectType sObjectType) | ||
{ | ||
// Mock implementation? | ||
if (selectorMockBySObjectType.containsKey(sObjectType)) | ||
return selectorMockBySObjectType.get(sObjectType); | ||
|
||
// Determine Apex class for Selector class | ||
Type selectorClass = selectorImplementationTypeBySObjectType.get(sObjectType); | ||
if (selectorClass == null) | ||
throw new DeveloperException('Selector class not found for SObjectType ' + sObjectType); | ||
|
||
// Construct Selector class and query by Id for the records | ||
return (fflib_ISObjectSelector) selectorClass.newInstance(); | ||
} | ||
|
||
/** | ||
* Replaces the linked implementation for the Apex interface. | ||
* | ||
* @param sObjectType The SObjectType for the new implementation | ||
* @param selectorImplementationType The replacement implementation type for the given SObjectType | ||
*/ | ||
public void replaceWith(Schema.SObjectType sObjectType, Type selectorImplementationType) | ||
{ | ||
this.selectorImplementationTypeBySObjectType.put(sObjectType, selectorImplementationType); | ||
} | ||
|
||
/** | ||
* Helper method to query the given SObject records | ||
* Internally creates an instance of the registered Selector and calls its | ||
* selectSObjectById method | ||
* | ||
* @param recordIds The SObject record Ids, must be all the same SObjectType | ||
* | ||
* @return List of queried SObjects | ||
* @exception Is thrown if the record Ids are not all the same or the SObjectType is not registered | ||
**/ | ||
public virtual List<SObject> selectById(Set<Id> recordIds) | ||
{ | ||
// No point creating an empty Domain class, nor can we determine the SObjectType anyway | ||
if (recordIds == null || recordIds.size() == 0) | ||
throw new DeveloperException('Invalid record Id\'s set'); | ||
|
||
// Determine SObjectType | ||
Schema.SObjectType domainSObjectType = new List<Id>(recordIds)[0].getSobjectType(); | ||
for (Id recordId : recordIds) | ||
{ | ||
if (recordId.getSobjectType() != domainSObjectType) | ||
throw new DeveloperException('Unable to determine SObjectType, Set contains Id\'s from different SObject types'); | ||
} | ||
|
||
// Construct Selector class and query by Id for the records | ||
return newInstance(domainSObjectType).selectSObjectsById(recordIds); | ||
} | ||
|
||
/** | ||
* Helper method to query related records to those provided, for example | ||
* if passed a list of Opportunity records and the Account Id field will | ||
* construct internally a list of Account Ids and call the registered | ||
* Account selector to query the related Account records, e.g. | ||
* <p/> | ||
* List<Account> accounts = | ||
* (List<Account>) Application.Selector.selectByRelationship(myOpportunities, Opportunity.AccountId); | ||
* | ||
* @param relatedRecords used to extract the related record Ids, e.g. Opportunity records | ||
* @param relationshipField field in the passed records that contains the relationship records to query, e.g. Opportunity.AccountId | ||
* | ||
* @return A list of the queried SObjects | ||
**/ | ||
public virtual List<SObject> selectByRelationship(List<SObject> relatedRecords, SObjectField relationshipField) | ||
{ | ||
Set<Id> relatedIds = new Set<Id>(); | ||
for (SObject relatedRecord : relatedRecords) | ||
{ | ||
Id relatedId = (Id) relatedRecord.get(relationshipField); | ||
if (relatedId != null) | ||
{ | ||
relatedIds.add(relatedId); | ||
} | ||
} | ||
return selectById(relatedIds); | ||
} | ||
|
||
public virtual void setMock(fflib_ISObjectSelector selectorInstance) | ||
{ | ||
selectorMockBySObjectType.put(selectorInstance.sObjectType(), selectorInstance); | ||
} | ||
|
||
/** | ||
* Exception representing a developer coding error, not intended for end user eyes | ||
**/ | ||
public class DeveloperException extends Exception {} | ||
} |
5 changes: 5 additions & 0 deletions
5
...mmon/application/classes/implementation/fflib_ClassicSelectorBindingResolver.cls-meta.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata"> | ||
<apiVersion>48.0</apiVersion> | ||
<status>Active</status> | ||
</ApexClass> |
79 changes: 79 additions & 0 deletions
79
...ce/apex-common/application/classes/implementation/fflib_ClassicServiceBindingResolver.cls
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
/** | ||
* File Name: fflib_ClassicServiceBindingResolver | ||
* Description: Service class binding resolver based on the classic AEP 1.0 definition with static maps | ||
* Copyright (c) 2020 Johnson & Johnson | ||
* | ||
* @author : architect ir. Wilhelmus G.J. Velzeboer | wvelzebo@its.jnj.com | ||
*/ | ||
public virtual class fflib_ClassicServiceBindingResolver | ||
implements fflib_ServiceBindingResolver | ||
{ | ||
|
||
protected Map<Type, Type> implementationTypeByInterfaceType; | ||
protected Map<Type, Object> mockImplementationByInterfaceType; | ||
|
||
/** | ||
* Class constructor | ||
* | ||
* @param implementationTypeByInterfaceType A map linking the interface type to its implementation type | ||
*/ | ||
public fflib_ClassicServiceBindingResolver(Map<Type, Type> implementationTypeByInterfaceType) | ||
{ | ||
this.implementationTypeByInterfaceType = implementationTypeByInterfaceType; | ||
this.mockImplementationByInterfaceType = new Map<Type, Object>(); | ||
} | ||
|
||
/** | ||
* Returns a new instance of the Apex class associated with the given Apex interface | ||
* Will return any mock implementation of the interface provided via setMock | ||
* Note that this method will not check the configured Apex class actually implements the interface | ||
* | ||
* @param serviceInterfaceType Apex interface type | ||
* | ||
* @return Instance of the implementation type | ||
* @exception DeveloperException Is thrown if there is no registered Apex class for the interface type | ||
**/ | ||
public virtual Object newInstance(Type serviceInterfaceType) | ||
{ | ||
// Mock implementation? | ||
if (mockImplementationByInterfaceType.containsKey(serviceInterfaceType)) | ||
return mockImplementationByInterfaceType.get(serviceInterfaceType); | ||
|
||
// Create an instance of the type implementing the given interface | ||
Type serviceImpl = implementationTypeByInterfaceType.get(serviceInterfaceType); | ||
if (serviceImpl == null) | ||
throw new DeveloperException('No implementation registered for service interface ' + serviceInterfaceType.getName()); | ||
|
||
return serviceImpl.newInstance(); | ||
} | ||
|
||
/** | ||
* Replaces the linked implementation for the Apex interface. | ||
* | ||
* @param serviceInterfaceType The interface type for this new implementation | ||
* @param serviceImplementationType The replacement implementation type for the given interface type | ||
*/ | ||
public virtual void replaceWith(Type serviceInterfaceType, Type serviceImplementationType) | ||
{ | ||
implementationTypeByInterfaceType.put(serviceInterfaceType, serviceImplementationType); | ||
} | ||
|
||
/** | ||
* Replaces the linked implementation for a mocked implementation, used in unit-test | ||
* | ||
* @param serviceInterfaceType The interface type for this new implementation | ||
* @param serviceInstance The mock instance for the given interface type | ||
*/ | ||
public virtual void setMock(Type serviceInterfaceType, Object serviceInstance) | ||
{ | ||
if (!System.Test.isRunningTest()) | ||
throw new DeveloperException('The setMock method should only be invoked from a unit-test context'); | ||
|
||
mockImplementationByInterfaceType.put(serviceInterfaceType, serviceInstance); | ||
} | ||
|
||
/** | ||
* Exception representing a developer coding error, not intended for end user eyes | ||
**/ | ||
public class DeveloperException extends Exception {} | ||
} |
Oops, something went wrong.