From f4ced88c7e961aa5ac01d2629a04e2d3cad07b51 Mon Sep 17 00:00:00 2001 From: wimvelzeboer Date: Mon, 11 Apr 2022 11:34:00 +0100 Subject: [PATCH 1/2] Allow for different implementations of Application factories --- .../main/classes/fflib_Application.cls | 136 +++++++++++++++++- .../main/classes/fflib_IDomainFactory.cls | 74 ++++++++++ .../main/classes/fflib_ISelectorFactory.cls | 55 +++++++ .../main/classes/fflib_IServiceFactory.cls | 18 +++ .../main/classes/fflib_IUnitOfWorkFactory.cls | 38 +++++ .../test/classes/fflib_ApplicationTest.cls | 86 ++++++++++- 6 files changed, 398 insertions(+), 9 deletions(-) diff --git a/sfdx-source/apex-common/main/classes/fflib_Application.cls b/sfdx-source/apex-common/main/classes/fflib_Application.cls index 889c8163320..a432d34c695 100644 --- a/sfdx-source/apex-common/main/classes/fflib_Application.cls +++ b/sfdx-source/apex-common/main/classes/fflib_Application.cls @@ -112,6 +112,9 @@ public virtual class fflib_Application return new fflib_SObjectUnitOfWork(objectTypes, dml); } + /** + * @param mockUow A mock implementation for the unitOfWork factory + */ @TestVisible protected virtual void setMock(fflib_ISObjectUnitOfWork mockUow) { @@ -168,6 +171,24 @@ public virtual class fflib_Application return serviceImpl.newInstance(); } + /** + * Creates or replaces an existing binding for another + * + * @param serviceInterfaceType The Interface type to replace its implementation + * @param replacementImplType The implementation type of the replacement + */ + public virtual void replaceWith(Type serviceInterfaceType, Type replacementImplType) + { + this.m_serviceInterfaceTypeByServiceImplType.put( + serviceInterfaceType, + replacementImplType + ); + } + + /** + * @param serviceInterfaceType The interface type to mock + * @param serviceImpl The mock implementation + */ @TestVisible protected virtual void setMock(Type serviceInterfaceType, Object serviceImpl) { @@ -246,6 +267,27 @@ public virtual class fflib_Application return newInstance(domainSObjectType).selectSObjectsById(recordIds); } + /** + * Helper method to query the given SObject records + * Internally creates an instance of the registered Selector and calls its + * selectSObjectById method. + * It assumes that all Ids are of the given SObjectType, no additional validation is done. + * + * @param recordIds The recordIds to query + * @param sObjectType The SObjectType of the Ids + * + * @return The queried records + * @exception fflib_Application.DeveloperException is thrown if the Ids set is empty + */ + public virtual List selectById(Set recordIds, SObjectType sObjectType) + { + if (recordIds == null || recordIds.size() == 0) + throw new fflib_Application.DeveloperException('Invalid record Id\'s set'); + + return newInstance(sObjectType) + .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 @@ -270,11 +312,36 @@ public virtual class fflib_Application return selectById(relatedIds); } + /** + * Creates or replaces an existing binding for another + * + * @param sObjectType The SObjectType of the selector to replace + * @param replacementImplType The implementation type of the replacement + */ + public virtual void replaceWith(SObjectType sObjectType, Type replacementImplType) + { + this.m_sObjectBySelectorType.put(sObjectType, replacementImplType); + } + + /** + * @param selectorInstance The instance of the mocked selector + */ @TestVisible protected virtual void setMock(fflib_ISObjectSelector selectorInstance) { m_sObjectByMockSelector.put(selectorInstance.sObjectType(), selectorInstance); - } + } + + /** + * @param sObjectType The SObjectType of the selector mock, + * avoids the need to stub the mock to return its SObjectType + * @param selectorInstance The instance of the mocked selector + */ + @TestVisible + protected virtual void setMock(SObjectType sObjectType, fflib_ISObjectSelector selectorInstance) + { + this.m_sObjectByMockSelector.put(sObjectType, selectorInstance); + } } /** @@ -282,7 +349,7 @@ public virtual class fflib_Application **/ public virtual class DomainFactory implements fflib_IDomainFactory { - protected fflib_Application.SelectorFactory m_selectorFactory; + protected fflib_ISelectorFactory m_selectorFactory; protected Map constructorTypeByObject; @@ -302,10 +369,10 @@ public virtual class fflib_Application * @param selectorFactory , e.g. Application.Selector * @param constructorTypeByObject Map of Domain classes by ObjectType **/ - public DomainFactory(fflib_Application.SelectorFactory selectorFactory, + public DomainFactory(fflib_ISelectorFactory selectorFactory, Map constructorTypeByObject) { - m_selectorFactory = selectorFactory; + this.m_selectorFactory = selectorFactory; this.constructorTypeByObject = constructorTypeByObject; this.mockDomainByObject = new Map(); } @@ -319,10 +386,10 @@ public virtual class fflib_Application * @param selectorFactory, e.g. Application.Selector * @param sObjectByDomainConstructorType Map of Apex classes by SObjectType **/ - public DomainFactory(fflib_Application.SelectorFactory selectorFactory, + public DomainFactory(fflib_ISelectorFactory selectorFactory, Map sObjectByDomainConstructorType) { - m_selectorFactory = selectorFactory; + this.m_selectorFactory = selectorFactory; this.constructorTypeByObject = getConstructorTypeByObject(sObjectByDomainConstructorType); this.mockDomainByObject = new Map(); } @@ -338,7 +405,22 @@ public virtual class fflib_Application public virtual fflib_IDomain newInstance(Set recordIds) { return newInstance(m_selectorFactory.selectById(recordIds)); + } + /** + * 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 + * @param sObjectType The SObjectType of the given record Ids + * + * @return Instance of a Domain containing the queried records + * @exception Throws an exception via the Selector Factory if the Ids are not all of the same SObjectType + **/ + public virtual fflib_IDomain newInstance(Set recordIds, Schema.SObjectType sObjectType) + { + return newInstance(m_selectorFactory.selectById(recordIds, sObjectType), sObjectType); } /** @@ -412,18 +494,60 @@ public virtual class fflib_Application ); } + /** + * Creates or replaces an existing binding for another + * + * @param sObjectType The SObjectType of the selector to replace + * @param replacementImplType The implementation type of the replacement + */ + public virtual void replaceWith(Schema.SObjectType sObjectType, Type replacementImplType) + { + this.constructorTypeByObject.put( + (Object) sObjectType, + replacementImplType + ); + } + + /** + * @param mockDomain The instance of the Domain mock + */ @TestVisible protected virtual void setMock(fflib_ISObjectDomain mockDomain) { mockDomainByObject.put((Object) mockDomain.sObjectType(), (fflib_IDomain) mockDomain); } + /** + * @param mockDomain The instance of the Domain mock + */ @TestVisible protected virtual void setMock(fflib_IDomain mockDomain) { mockDomainByObject.put(mockDomain.getType(), mockDomain); } + /** + * @param sObjectType The SObjectType of the Domain mock, + * avoids the need to stub the mock to return its SObjectType + * @param mockDomain The instance of the Domain mock + */ + @TestVisible + protected virtual void setMock(Schema.SObjectType sObjectType, fflib_ISObjectDomain mockDomain) + { + mockDomainByObject.put((Object) sObjectType, mockDomain); + } + + /** + * @param domainType The ObjectType of the Domain mock, + * avoids the need to stub the mock to return its ObjectType + * @param mockDomain The instance of the Domain mock + */ + @TestVisible + protected virtual void setMock(Object domainType, fflib_IDomain mockDomain) + { + mockDomainByObject.put(domainType, mockDomain); + } + protected virtual Map getConstructorTypeByObject(Map constructorTypeBySObjectType) { Map result = new Map(); diff --git a/sfdx-source/apex-common/main/classes/fflib_IDomainFactory.cls b/sfdx-source/apex-common/main/classes/fflib_IDomainFactory.cls index 1e0f982d5ae..0e3f671c753 100644 --- a/sfdx-source/apex-common/main/classes/fflib_IDomainFactory.cls +++ b/sfdx-source/apex-common/main/classes/fflib_IDomainFactory.cls @@ -25,8 +25,82 @@ **/ public interface fflib_IDomainFactory { + /** + * 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 the Domain + **/ fflib_IDomain newInstance(Set recordIds); + + /** + * 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 + * @param sObjectType The Schema.SObjectType of the record Ids, + * Providing this parameter will omit the framework from checking if the Id's are all the same + * and of which SObjectType they are. + * + * @exception Throws an exception via the Selector Factory if the Ids are not all of the same SObjectType + * + * @return Instance of the Domain + **/ + fflib_IDomain newInstance(Set recordIds, Schema.SObjectType sObjectType); + + /** + * 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 of records, e.g.; `List` or `List`) + * + * @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 the Domain containing the given records + **/ fflib_IDomain newInstance(List records); + + /** + * 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 objects A concrete list of Objects, e.g.; `List` or `List`) + * @param objectType + * + * @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 the Domain containing the given Objects + **/ fflib_IDomain newInstance(List objects, Object objectType); + + /** + * 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 but all records in the list will be assumed to be of + * the type specified in sObjectType + * + * @return Instance of the Domain containing the given records + **/ fflib_IDomain newInstance(List records, SObjectType domainSObjectType); + + /** + * Creates or replaces an existing binding for another + * + * @param sObjectType The SObjectType of the domain to replace + * @param replacementImplType The implementation type of the replacement + */ + void replaceWith(Schema.SObjectType sObjectType, Type replacementImplType); } \ No newline at end of file diff --git a/sfdx-source/apex-common/main/classes/fflib_ISelectorFactory.cls b/sfdx-source/apex-common/main/classes/fflib_ISelectorFactory.cls index a39095b289b..595e7c2569e 100644 --- a/sfdx-source/apex-common/main/classes/fflib_ISelectorFactory.cls +++ b/sfdx-source/apex-common/main/classes/fflib_ISelectorFactory.cls @@ -25,7 +25,62 @@ **/ public interface fflib_ISelectorFactory { + /** + * 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 fflib_ISObjectSelector + **/ fflib_ISObjectSelector newInstance(SObjectType sObjectType); + + /** + * 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 + * @exception Is thrown if the record Ids are not all the same or the SObjectType is not registered + * + * @return List of queried records + **/ List selectById(Set recordIds); + + /** + * 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 + * @exception Is thrown if the record Ids are not all the same or the SObjectType is not registered + * @param sObjectType The SObjectType of the provided Ids + * + * @return List of queried records + **/ + List selectById(Set recordIds, Schema.SObjectType sObjectType); + + /** + * 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. + * + * List accounts = + * (List) Application.Selector.selectByRelationship(myOpps, 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 List of queried records + **/ List selectByRelationship(List relatedRecords, SObjectField relationshipField); + + /** + * Creates or replaces an existing binding for another + * + * @param sObjectType The SObjectType of the selector to replace + * @param replacementImplType The implementation type of the replacement + */ + void replaceWith(Schema.SObjectType sObjectType, Type replacementImplType); } \ No newline at end of file diff --git a/sfdx-source/apex-common/main/classes/fflib_IServiceFactory.cls b/sfdx-source/apex-common/main/classes/fflib_IServiceFactory.cls index 93fa4124cc7..3fd95ec1c01 100644 --- a/sfdx-source/apex-common/main/classes/fflib_IServiceFactory.cls +++ b/sfdx-source/apex-common/main/classes/fflib_IServiceFactory.cls @@ -25,5 +25,23 @@ **/ public interface fflib_IServiceFactory { + /** + * 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 + * @exception Is thrown if there is no registered Apex class for the interface type + * + * @return Instance of the requested service class interface type + **/ Object newInstance(Type serviceInterfaceType); + + /** + * Creates or replaces an existing binding for another + * + * @param serviceInterfaceType The Interface type to replace its implementation + * @param replacementImplType The implementation type of the replacement + */ + void replaceWith(Type serviceInterfaceType, Type replacementImplType); } \ No newline at end of file diff --git a/sfdx-source/apex-common/main/classes/fflib_IUnitOfWorkFactory.cls b/sfdx-source/apex-common/main/classes/fflib_IUnitOfWorkFactory.cls index 371ab1ccde9..5094d277712 100644 --- a/sfdx-source/apex-common/main/classes/fflib_IUnitOfWorkFactory.cls +++ b/sfdx-source/apex-common/main/classes/fflib_IUnitOfWorkFactory.cls @@ -25,8 +25,46 @@ **/ public interface fflib_IUnitOfWorkFactory { + /** + * Returns a new fflib_SObjectUnitOfWork configured with the + * SObjectType list provided in the constructor, returns a Mock implementation + * if set via the setMock method + * + * @return Instance of fflib_ISObjectUnitOfWork + **/ fflib_ISObjectUnitOfWork newInstance(); + + /** + * Returns a new fflib_SObjectUnitOfWork configured with the + * SObjectType list provided in the constructor, returns a Mock implementation + * if set via the setMock method + * + * @param dml A custom implementation of the IDML to perform the database operations + * + * @return Instance of fflib_ISObjectUnitOfWork + **/ fflib_ISObjectUnitOfWork newInstance(fflib_SObjectUnitOfWork.IDML dml); + + /** + * Returns a new fflib_SObjectUnitOfWork configured with the + * SObjectType list specified, returns a Mock implementation + * if set via the setMock method + * + * @param objectTypes The hierarchical structure of the SObjectTypes + * + * @return Instance of fflib_ISObjectUnitOfWork + **/ fflib_ISObjectUnitOfWork newInstance(List objectTypes); + + /** + * Returns a new fflib_SObjectUnitOfWork configured with the + * SObjectType list specified, returns a Mock implementation + * if set via the setMock method + * + * @param objectTypes The hierarchical structure of the SObjectTypes + * @param dml A custom implementation of the IDML to perform the database operations + * + * @return Instance of fflib_ISObjectUnitOfWork + **/ fflib_ISObjectUnitOfWork newInstance(List objectTypes, fflib_SObjectUnitOfWork.IDML dml); } \ No newline at end of file diff --git a/sfdx-source/apex-common/test/classes/fflib_ApplicationTest.cls b/sfdx-source/apex-common/test/classes/fflib_ApplicationTest.cls index af1dd15274f..152e62d874e 100644 --- a/sfdx-source/apex-common/test/classes/fflib_ApplicationTest.cls +++ b/sfdx-source/apex-common/test/classes/fflib_ApplicationTest.cls @@ -444,6 +444,64 @@ private class fflib_ApplicationTest System.assert(!customDML.isInsertCalled, 'Oops, custom DML was called'); } + @IsTest + static void itShouldReturnTheReplacedServiceImplementation() + { + // GIVEN a configured service binding resolver with an interface linked to an implementation + System.assert( + Service.newInstance(IOpportunitiesService.class) instanceOf OpportunitiesServiceImpl + ); + + // WHEN we replace the binding and request the implementation for the interface + Service.replaceWith(IOpportunitiesService.class, OpportunitiesServiceAltImpl.class); + Object result = Service.newInstance(IOpportunitiesService.class); + + // THEN it should return the alternative implementation + System.assert( + result instanceof OpportunitiesServiceAltImpl, + 'Incorrect implementation returned, expected the alternative implementation' + ); + } + + @IsTest + static void itShouldReturnTheReplacedSelectorImplementation() + { + // GIVEN a configured service binding resolver with an interface linked to an implementation + System.assert( + Selector.newInstance(Account.SObjectType) instanceOf AccountsSelector + ); + + // WHEN we replace the binding and request the implementation for the interface + Selector.replaceWith(Account.SObjectType, AccountsSelectorAlt.class); + Object result = Selector.newInstance(Account.SObjectType); + + // THEN it should return the alternative implementation + System.assert( + result instanceof AccountsSelectorAlt, + 'Incorrect implementation returned, expected the alternative implementation' + ); + } + + @IsTest + static void itShouldReturnTheReplacedDomainImplementation() + { + // GIVEN a configured service binding resolver with an interface linked to an implementation + final List records = new List(); + System.assert( + Domain.newInstance(records) instanceOf AccountsDomain + ); + + // WHEN we replace the binding and request the implementation for the interface + Domain.replaceWith(Account.SObjectType, AccountsConstructorAlt.class); + Object result = Domain.newInstance(records); + + // THEN it should return the alternative implementation + System.assert( + result instanceof AccountsDomainAlt, + 'Incorrect implementation returned, expected the alternative implementation' + ); + } + public class CustomDML implements fflib_SObjectUnitOfWork.IDML { public boolean isInsertCalled = false; @@ -502,7 +560,16 @@ private class fflib_ApplicationTest Opportunity.SObjectType => OpportuntiesConstructor.class, Contact.SObjectType => ContactsConstructor.class }); - public class AccountsDomain extends fflib_SObjectDomain + + public interface IAccountsDomain extends fflib_ISObjectDomain {} + public class AccountsDomainAlt extends AccountsDomain + { + public AccountsDomainAlt(List records) + { + super(records); + } + } + public virtual class AccountsDomain extends fflib_SObjectDomain implements IAccountsDomain { public AccountsDomain(List sObjectList) { @@ -528,6 +595,15 @@ private class fflib_ApplicationTest } } + public class AccountsConstructorAlt implements fflib_IDomainConstructor + { + public fflib_IDomain construct(List objects) + { + return new AccountsDomainAlt((List) objects); + } + } + + public class OpportuntiesDomain extends fflib_SObjectDomain { public OpportuntiesDomain(List sObjectList) @@ -588,8 +664,8 @@ private class fflib_ApplicationTest return Opportunity.sObjectType; } } - - class AccountsSelector extends fflib_SObjectSelector + + virtual class AccountsSelector extends fflib_SObjectSelector { public List getSObjectFieldList() { @@ -607,6 +683,8 @@ private class fflib_ApplicationTest } } + class AccountsSelectorAlt extends AccountsSelector {} + public interface IContactService { } public interface IOpportunitiesService { } @@ -615,6 +693,8 @@ private class fflib_ApplicationTest public class OpportunitiesServiceImpl implements IOpportunitiesService { } + public class OpportunitiesServiceAltImpl implements IOpportunitiesService { } + public class AccountsServiceImpl implements IAccountService { } public class AccountsServiceMock implements IAccountService { } From 50156487f53f275c04540a92a57ba5caca35dc2d Mon Sep 17 00:00:00 2001 From: wimvelzeboer Date: Sat, 23 Apr 2022 21:02:31 +0100 Subject: [PATCH 2/2] Remove NewInstance with SObjectType method --- .../main/classes/fflib_Application.cls | 16 ---------------- .../main/classes/fflib_IDomainFactory.cls | 16 ---------------- 2 files changed, 32 deletions(-) diff --git a/sfdx-source/apex-common/main/classes/fflib_Application.cls b/sfdx-source/apex-common/main/classes/fflib_Application.cls index a432d34c695..9f93d3176d9 100644 --- a/sfdx-source/apex-common/main/classes/fflib_Application.cls +++ b/sfdx-source/apex-common/main/classes/fflib_Application.cls @@ -407,22 +407,6 @@ public virtual class fflib_Application return newInstance(m_selectorFactory.selectById(recordIds)); } - /** - * 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 - * @param sObjectType The SObjectType of the given record Ids - * - * @return Instance of a Domain containing the queried records - * @exception Throws an exception via the Selector Factory if the Ids are not all of the same SObjectType - **/ - public virtual fflib_IDomain newInstance(Set recordIds, Schema.SObjectType sObjectType) - { - return newInstance(m_selectorFactory.selectById(recordIds, sObjectType), sObjectType); - } - /** * Dynamically constructs an instance of the Domain class for the given records * Will return a Mock implementation if one has been provided via setMock diff --git a/sfdx-source/apex-common/main/classes/fflib_IDomainFactory.cls b/sfdx-source/apex-common/main/classes/fflib_IDomainFactory.cls index 0e3f671c753..231e149a7eb 100644 --- a/sfdx-source/apex-common/main/classes/fflib_IDomainFactory.cls +++ b/sfdx-source/apex-common/main/classes/fflib_IDomainFactory.cls @@ -37,22 +37,6 @@ public interface fflib_IDomainFactory **/ fflib_IDomain newInstance(Set recordIds); - /** - * 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 - * @param sObjectType The Schema.SObjectType of the record Ids, - * Providing this parameter will omit the framework from checking if the Id's are all the same - * and of which SObjectType they are. - * - * @exception Throws an exception via the Selector Factory if the Ids are not all of the same SObjectType - * - * @return Instance of the Domain - **/ - fflib_IDomain newInstance(Set recordIds, Schema.SObjectType sObjectType); - /** * Dynamically constructs an instance of the Domain class for the given records * Will return a Mock implementation if one has been provided via setMock