Skip to content

Commit

Permalink
Allow for different implementations of Application factories
Browse files Browse the repository at this point in the history
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
Show file tree
Hide file tree
Showing 21 changed files with 1,181 additions and 183 deletions.
45 changes: 45 additions & 0 deletions docs/Application.md
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
}
);
}
```
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 {}
}
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>
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 {}
}
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>
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 {}
}
Loading

0 comments on commit 14da8f1

Please sign in to comment.