Skip to content

Commit 14da8f1

Browse files
author
William Velzeboer
committed
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.
1 parent b5c833f commit 14da8f1

21 files changed

+1181
-183
lines changed

docs/Application.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
FFLib Apex Common
2+
=================
3+
4+
# Application class
5+
6+
### Example - Binding based on static maps in Apex
7+
8+
```apex
9+
public with sharing class Application
10+
{
11+
public static final fflib_ServiceBindingResolver Service =
12+
new fflib_ClassicServiceBindingResolver(
13+
new Map<Type, Type>
14+
{
15+
AccountsService.class => AccountsServiceImp.class
16+
}
17+
);
18+
19+
public static final fflib_SelectorBindingResolver Selector =
20+
new fflib_ClassicSelectorBindingResolver(
21+
new Map<SObjectType, Type>
22+
{
23+
Account.SObjectType => AccountsSelector.class
24+
}
25+
);
26+
27+
public static final fflib_DomainBindingResolver Domain =
28+
new fflib_ClassicDomainBindingResolver(
29+
Application.Selector,
30+
new Map<SObjectType, Type>
31+
{
32+
Schema.Account.SObjectType => Accounts.Constructor.class
33+
}
34+
);
35+
36+
public static final fflib_Application.UnitOfWorkFactory UnitOfWork =
37+
new fflib_Application.UnitOfWorkFactory(
38+
new List<SObjectType>
39+
{
40+
Account.SObjectType,
41+
Contact.SObjectType
42+
}
43+
);
44+
}
45+
```
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* File Name: fflib_ClassicDomainBindingResolver
3+
* Description: Domain class binding resolver based on the classic AEP 1.0 definition with static maps
4+
* Copyright (c) 2020 Johnson & Johnson
5+
*
6+
* @author : architect ir. Wilhelmus G.J. Velzeboer | [email protected]
7+
*/
8+
public virtual class fflib_ClassicDomainBindingResolver implements fflib_DomainBindingResolver
9+
{
10+
protected fflib_SelectorBindingResolver selectorBindingResolver;
11+
12+
protected Map<Schema.SObjectType, Type> domainConstructorBySObjectType;
13+
14+
protected Map<Schema.SObjectType, fflib_ISObjectDomain> mockDomainInstanceBySObjectType;
15+
16+
/**
17+
* Class constructor
18+
*
19+
* @param selectorBindingResolver
20+
* @param domainConstructorBySObjectType
21+
*/
22+
public fflib_ClassicDomainBindingResolver(
23+
fflib_SelectorBindingResolver selectorBindingResolver,
24+
Map<SObjectType, Type> domainConstructorBySObjectType)
25+
{
26+
this.selectorBindingResolver = selectorBindingResolver;
27+
this.domainConstructorBySObjectType = domainConstructorBySObjectType;
28+
this.mockDomainInstanceBySObjectType = new Map<SObjectType, fflib_ISObjectDomain>();
29+
}
30+
31+
/**
32+
* Dynamically constructs an instance of a Domain class for the given record Ids
33+
* Internally uses the Selector Factory to query the records before passing to a
34+
* dynamically constructed instance of the application Apex Domain class
35+
*
36+
* @param recordIds A list of Id's of the same type
37+
* @exception Throws an exception via the Selector Factory if the Ids are not all of the same SObjectType
38+
*
39+
* @return Instance of fflib_ISObjectDomain containing the record with the provided Ids
40+
**/
41+
public virtual fflib_ISObjectDomain newInstance(Set<Id> recordIds)
42+
{
43+
return newInstance(selectorBindingResolver.selectById(recordIds));
44+
}
45+
46+
/**
47+
* Dynamically constructs an instance of the Domain class for the given records
48+
* Will return a Mock implementation if one has been provided via setMock
49+
*
50+
* @param records A concrete list (e.g. List<Account> vs List<SObject>) of records
51+
* @exception Throws an exception if the SObjectType cannot be determined from the list
52+
* or the constructor for Domain class was not registered for the SOBjectType
53+
*
54+
* @return Instance of fflib_ISObjectDomain containing the provided records
55+
**/
56+
public virtual fflib_ISObjectDomain newInstance(List<SObject> records)
57+
{
58+
SObjectType domainSObjectType = records.getSObjectType();
59+
if (domainSObjectType == null)
60+
throw new DeveloperException('Unable to determine SObjectType');
61+
62+
// Mock implementation?
63+
if (mockDomainInstanceBySObjectType.containsKey(domainSObjectType))
64+
return mockDomainInstanceBySObjectType.get(domainSObjectType);
65+
66+
// Determine SObjectType and Apex classes for Domain class
67+
Type domainConstructorClass = domainConstructorBySObjectType.get(domainSObjectType);
68+
if (domainConstructorClass == null)
69+
throw new DeveloperException('Domain constructor class not found for SObjectType ' + domainSObjectType);
70+
71+
// Construct Domain class passing in the queried records
72+
fflib_SObjectDomain.IConstructable domainConstructor =
73+
(fflib_SObjectDomain.IConstructable) domainConstructorClass.newInstance();
74+
return (fflib_ISObjectDomain) domainConstructor.construct(records);
75+
}
76+
77+
/**
78+
* Dynamically constructs an instance of the Domain class for the given records and SObjectType
79+
* Will return a Mock implementation if one has been provided via setMock
80+
*
81+
* @param records A list records
82+
* @param domainSObjectType SObjectType for list of records
83+
* @exception Throws an exception if the SObjectType is not specified or if constructor for Domain class was not registered for the SObjectType
84+
*
85+
* @remark Will support List<SObject> but all records in the list will be assumed to be of
86+
* the type specified in sObjectType
87+
*
88+
* @return Instance of fflib_ISObjectDomain containing the provided records
89+
**/
90+
public virtual fflib_ISObjectDomain newInstance(List<SObject> records, SObjectType domainSObjectType)
91+
{
92+
if (domainSObjectType == null)
93+
throw new DeveloperException('Must specify sObjectType');
94+
95+
// Mock implementation?
96+
if (mockDomainInstanceBySObjectType.containsKey(domainSObjectType))
97+
return mockDomainInstanceBySObjectType.get(domainSObjectType);
98+
99+
// Determine SObjectType and Apex classes for Domain class
100+
Type domainConstructorClass = domainConstructorBySObjectType.get(domainSObjectType);
101+
if (domainConstructorClass == null)
102+
throw new DeveloperException('Domain constructor class not found for SObjectType ' + domainSObjectType);
103+
104+
// Construct Domain class passing in the queried records
105+
fflib_SObjectDomain.IConstructable2 domainConstructor =
106+
(fflib_SObjectDomain.IConstructable2) domainConstructorClass.newInstance();
107+
return (fflib_ISObjectDomain) domainConstructor.construct(records, domainSObjectType);
108+
}
109+
110+
public void replaceWith(SObjectType domainSObjectType, Type domainConstructorImplementationType)
111+
{
112+
this.domainConstructorBySObjectType.put(domainSObjectType, domainConstructorImplementationType);
113+
}
114+
115+
public virtual void setMock(fflib_ISObjectDomain mockDomain)
116+
{
117+
setMock(mockDomain.sObjectType(), mockDomain);
118+
}
119+
120+
public virtual void setMock(Schema.SObjectType domainSObjectType, fflib_ISObjectDomain mockDomain)
121+
{
122+
mockDomainInstanceBySObjectType.put(domainSObjectType, mockDomain);
123+
}
124+
125+
/**
126+
* Exception representing a developer coding error, not intended for end user eyes
127+
**/
128+
public class DeveloperException extends Exception {}
129+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
3+
<apiVersion>48.0</apiVersion>
4+
<status>Active</status>
5+
</ApexClass>
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* File Name: fflib_ClassicSelectorBindingResolver
3+
* Description: Selector class binding resolver based on the classic AEP 1.0 definition with static maps
4+
* Copyright (c) 2020 Johnson & Johnson
5+
*
6+
* @author : architect ir. Wilhelmus G.J. Velzeboer | [email protected]
7+
*/
8+
public virtual class fflib_ClassicSelectorBindingResolver
9+
implements fflib_SelectorBindingResolver
10+
{
11+
12+
protected Map<SObjectType, Type> selectorImplementationTypeBySObjectType;
13+
protected Map<SObjectType, fflib_ISObjectSelector> selectorMockBySObjectType;
14+
15+
/**
16+
* Class constructor
17+
*/
18+
public fflib_ClassicSelectorBindingResolver(Map<SObjectType, Type> selectorImplementationTypeBySObjectType)
19+
{
20+
this.selectorImplementationTypeBySObjectType = selectorImplementationTypeBySObjectType;
21+
this.selectorMockBySObjectType = new Map<SObjectType, fflib_ISObjectSelector>();
22+
}
23+
24+
/**
25+
* Creates a new instance of the associated Apex Class implementing fflib_ISObjectSelector
26+
* for the given SObjectType, or if provided via setMock returns the Mock implementation
27+
*
28+
* @param sObjectType An SObjectType token, e.g. Account.SObjectType
29+
*
30+
* @return Instance of the selector implementation
31+
**/
32+
public virtual fflib_ISObjectSelector newInstance(Schema.SObjectType sObjectType)
33+
{
34+
// Mock implementation?
35+
if (selectorMockBySObjectType.containsKey(sObjectType))
36+
return selectorMockBySObjectType.get(sObjectType);
37+
38+
// Determine Apex class for Selector class
39+
Type selectorClass = selectorImplementationTypeBySObjectType.get(sObjectType);
40+
if (selectorClass == null)
41+
throw new DeveloperException('Selector class not found for SObjectType ' + sObjectType);
42+
43+
// Construct Selector class and query by Id for the records
44+
return (fflib_ISObjectSelector) selectorClass.newInstance();
45+
}
46+
47+
/**
48+
* Replaces the linked implementation for the Apex interface.
49+
*
50+
* @param sObjectType The SObjectType for the new implementation
51+
* @param selectorImplementationType The replacement implementation type for the given SObjectType
52+
*/
53+
public void replaceWith(Schema.SObjectType sObjectType, Type selectorImplementationType)
54+
{
55+
this.selectorImplementationTypeBySObjectType.put(sObjectType, selectorImplementationType);
56+
}
57+
58+
/**
59+
* Helper method to query the given SObject records
60+
* Internally creates an instance of the registered Selector and calls its
61+
* selectSObjectById method
62+
*
63+
* @param recordIds The SObject record Ids, must be all the same SObjectType
64+
*
65+
* @return List of queried SObjects
66+
* @exception Is thrown if the record Ids are not all the same or the SObjectType is not registered
67+
**/
68+
public virtual List<SObject> selectById(Set<Id> recordIds)
69+
{
70+
// No point creating an empty Domain class, nor can we determine the SObjectType anyway
71+
if (recordIds == null || recordIds.size() == 0)
72+
throw new DeveloperException('Invalid record Id\'s set');
73+
74+
// Determine SObjectType
75+
Schema.SObjectType domainSObjectType = new List<Id>(recordIds)[0].getSobjectType();
76+
for (Id recordId : recordIds)
77+
{
78+
if (recordId.getSobjectType() != domainSObjectType)
79+
throw new DeveloperException('Unable to determine SObjectType, Set contains Id\'s from different SObject types');
80+
}
81+
82+
// Construct Selector class and query by Id for the records
83+
return newInstance(domainSObjectType).selectSObjectsById(recordIds);
84+
}
85+
86+
/**
87+
* Helper method to query related records to those provided, for example
88+
* if passed a list of Opportunity records and the Account Id field will
89+
* construct internally a list of Account Ids and call the registered
90+
* Account selector to query the related Account records, e.g.
91+
* <p/>
92+
* List<Account> accounts =
93+
* (List<Account>) Application.Selector.selectByRelationship(myOpportunities, Opportunity.AccountId);
94+
*
95+
* @param relatedRecords used to extract the related record Ids, e.g. Opportunity records
96+
* @param relationshipField field in the passed records that contains the relationship records to query, e.g. Opportunity.AccountId
97+
*
98+
* @return A list of the queried SObjects
99+
**/
100+
public virtual List<SObject> selectByRelationship(List<SObject> relatedRecords, SObjectField relationshipField)
101+
{
102+
Set<Id> relatedIds = new Set<Id>();
103+
for (SObject relatedRecord : relatedRecords)
104+
{
105+
Id relatedId = (Id) relatedRecord.get(relationshipField);
106+
if (relatedId != null)
107+
{
108+
relatedIds.add(relatedId);
109+
}
110+
}
111+
return selectById(relatedIds);
112+
}
113+
114+
public virtual void setMock(fflib_ISObjectSelector selectorInstance)
115+
{
116+
selectorMockBySObjectType.put(selectorInstance.sObjectType(), selectorInstance);
117+
}
118+
119+
/**
120+
* Exception representing a developer coding error, not intended for end user eyes
121+
**/
122+
public class DeveloperException extends Exception {}
123+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
3+
<apiVersion>48.0</apiVersion>
4+
<status>Active</status>
5+
</ApexClass>
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* File Name: fflib_ClassicServiceBindingResolver
3+
* Description: Service class binding resolver based on the classic AEP 1.0 definition with static maps
4+
* Copyright (c) 2020 Johnson & Johnson
5+
*
6+
* @author : architect ir. Wilhelmus G.J. Velzeboer | [email protected]
7+
*/
8+
public virtual class fflib_ClassicServiceBindingResolver
9+
implements fflib_ServiceBindingResolver
10+
{
11+
12+
protected Map<Type, Type> implementationTypeByInterfaceType;
13+
protected Map<Type, Object> mockImplementationByInterfaceType;
14+
15+
/**
16+
* Class constructor
17+
*
18+
* @param implementationTypeByInterfaceType A map linking the interface type to its implementation type
19+
*/
20+
public fflib_ClassicServiceBindingResolver(Map<Type, Type> implementationTypeByInterfaceType)
21+
{
22+
this.implementationTypeByInterfaceType = implementationTypeByInterfaceType;
23+
this.mockImplementationByInterfaceType = new Map<Type, Object>();
24+
}
25+
26+
/**
27+
* Returns a new instance of the Apex class associated with the given Apex interface
28+
* Will return any mock implementation of the interface provided via setMock
29+
* Note that this method will not check the configured Apex class actually implements the interface
30+
*
31+
* @param serviceInterfaceType Apex interface type
32+
*
33+
* @return Instance of the implementation type
34+
* @exception DeveloperException Is thrown if there is no registered Apex class for the interface type
35+
**/
36+
public virtual Object newInstance(Type serviceInterfaceType)
37+
{
38+
// Mock implementation?
39+
if (mockImplementationByInterfaceType.containsKey(serviceInterfaceType))
40+
return mockImplementationByInterfaceType.get(serviceInterfaceType);
41+
42+
// Create an instance of the type implementing the given interface
43+
Type serviceImpl = implementationTypeByInterfaceType.get(serviceInterfaceType);
44+
if (serviceImpl == null)
45+
throw new DeveloperException('No implementation registered for service interface ' + serviceInterfaceType.getName());
46+
47+
return serviceImpl.newInstance();
48+
}
49+
50+
/**
51+
* Replaces the linked implementation for the Apex interface.
52+
*
53+
* @param serviceInterfaceType The interface type for this new implementation
54+
* @param serviceImplementationType The replacement implementation type for the given interface type
55+
*/
56+
public virtual void replaceWith(Type serviceInterfaceType, Type serviceImplementationType)
57+
{
58+
implementationTypeByInterfaceType.put(serviceInterfaceType, serviceImplementationType);
59+
}
60+
61+
/**
62+
* Replaces the linked implementation for a mocked implementation, used in unit-test
63+
*
64+
* @param serviceInterfaceType The interface type for this new implementation
65+
* @param serviceInstance The mock instance for the given interface type
66+
*/
67+
public virtual void setMock(Type serviceInterfaceType, Object serviceInstance)
68+
{
69+
if (!System.Test.isRunningTest())
70+
throw new DeveloperException('The setMock method should only be invoked from a unit-test context');
71+
72+
mockImplementationByInterfaceType.put(serviceInterfaceType, serviceInstance);
73+
}
74+
75+
/**
76+
* Exception representing a developer coding error, not intended for end user eyes
77+
**/
78+
public class DeveloperException extends Exception {}
79+
}

0 commit comments

Comments
 (0)