diff --git a/sfdx-source/apex-common/main/classes/fflib_Application.cls b/sfdx-source/apex-common/main/classes/fflib_Application.cls index 889c8163320..9f93d3176d9 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,6 @@ public virtual class fflib_Application public virtual fflib_IDomain newInstance(Set recordIds) { return newInstance(m_selectorFactory.selectById(recordIds)); - } /** @@ -412,18 +478,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..231e149a7eb 100644 --- a/sfdx-source/apex-common/main/classes/fflib_IDomainFactory.cls +++ b/sfdx-source/apex-common/main/classes/fflib_IDomainFactory.cls @@ -25,8 +25,66 @@ **/ 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 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 { }