|
| 1 | +/** |
| 2 | + * Copyright (c) 2014, FinancialForce.com, inc |
| 3 | + * All rights reserved. |
| 4 | + * |
| 5 | + * Redistribution and use in source and binary forms, with or without modification, |
| 6 | + * are permitted provided that the following conditions are met: |
| 7 | + * |
| 8 | + * - Redistributions of source code must retain the above copyright notice, |
| 9 | + * this list of conditions and the following disclaimer. |
| 10 | + * - Redistributions in binary form must reproduce the above copyright notice, |
| 11 | + * this list of conditions and the following disclaimer in the documentation |
| 12 | + * and/or other materials provided with the distribution. |
| 13 | + * - Neither the name of the FinancialForce.com, inc nor the names of its contributors |
| 14 | + * may be used to endorse or promote products derived from this software without |
| 15 | + * specific prior written permission. |
| 16 | + * |
| 17 | + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND |
| 18 | + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
| 19 | + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL |
| 20 | + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| 21 | + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS |
| 22 | + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| 23 | + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| 24 | + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 25 | +**/ |
| 26 | + |
| 27 | +/** |
| 28 | + * Class provides inner classes implementing factories for the main components |
| 29 | + * of the Apex Enterprise Patterns, Service, Unit Of Work, Selector and Domain. |
| 30 | + * See the sample applications Application.cls file for an example |
| 31 | + **/ |
| 32 | +public class fflib_Application |
| 33 | +{ |
| 34 | + /** |
| 35 | + * Class implements a Unit of Work factory |
| 36 | + **/ |
| 37 | + public class UnitOfWorkFactory |
| 38 | + { |
| 39 | + private List<SObjectType> m_objectTypes; |
| 40 | + private fflib_ISObjectUnitOfWork m_mockUow; |
| 41 | + |
| 42 | + /** |
| 43 | + * Constructs a Unit Of Work factory |
| 44 | + * |
| 45 | + * @param objectTypes List of SObjectTypes in dependency order |
| 46 | + **/ |
| 47 | + public UnitOfWorkFactory(List<SObjectType> objectTypes) |
| 48 | + { |
| 49 | + m_objectTypes = objectTypes.clone(); |
| 50 | + } |
| 51 | + |
| 52 | + /** |
| 53 | + * Returns a new fflib_SObjectUnitOfWork configured with the |
| 54 | + * SObjectType list provided in the constructor, returns a Mock implementation |
| 55 | + * if set via the setMock method |
| 56 | + **/ |
| 57 | + public fflib_ISObjectUnitOfWork newInstance() |
| 58 | + { |
| 59 | + // Mock? |
| 60 | + if(m_mockUow!=null) |
| 61 | + return m_mockUow; |
| 62 | + return new fflib_SObjectUnitOfWork(m_objectTypes); |
| 63 | + } |
| 64 | + |
| 65 | + @TestVisible |
| 66 | + private void setMock(fflib_ISObjectUnitOfWork mockUow) |
| 67 | + { |
| 68 | + m_mockUow = mockUow; |
| 69 | + } |
| 70 | + } |
| 71 | + |
| 72 | + /** |
| 73 | + * Simple Service Factory implementaiton |
| 74 | + **/ |
| 75 | + public class ServiceFactory |
| 76 | + { |
| 77 | + private Map<Type, Type> m_serviceInterfaceTypeByServiceImplType; |
| 78 | + |
| 79 | + private Map<Type, Object> m_serviceInterfaceTypeByMockService; |
| 80 | + |
| 81 | + /** |
| 82 | + * Constructs a simple Service Factory, |
| 83 | + * using a Map of Apex Interfaces to Apex Classes implementing the interface |
| 84 | + * Note that this will not check the Apex Classes given actually implement the interfaces |
| 85 | + * as this information is not presently available via the Apex runtime |
| 86 | + * |
| 87 | + * @param serviceInterfaceTypeByServiceImplType Map ofi interfaces to classes |
| 88 | + **/ |
| 89 | + public ServiceFactory(Map<Type, Type> serviceInterfaceTypeByServiceImplType) |
| 90 | + { |
| 91 | + m_serviceInterfaceTypeByServiceImplType = serviceInterfaceTypeByServiceImplType; |
| 92 | + m_serviceInterfaceTypeByMockService = new Map<Type, Object>(); |
| 93 | + } |
| 94 | + |
| 95 | + /** |
| 96 | + * Returns a new instance of the Apex class associated with the given Apex interface |
| 97 | + * Will return any mock implementation of the interface provided via setMock |
| 98 | + * Note that this method will not check the configured Apex class actually implements the interface |
| 99 | + * |
| 100 | + * @param serviceInterfaceType Apex interface type |
| 101 | + * @exception Is thrown if there is no registered Apex class for the interface type |
| 102 | + **/ |
| 103 | + public Object newInstance(Type serviceInterfaceType) |
| 104 | + { |
| 105 | + // Mock implementation? |
| 106 | + if(m_serviceInterfaceTypeByMockService.containsKey(serviceInterfaceType)) |
| 107 | + return m_serviceInterfaceTypeByMockService.get(serviceInterfaceType); |
| 108 | + |
| 109 | + // Create an instance of the type impleneting the given interface |
| 110 | + Type serviceImpl = m_serviceInterfaceTypeByServiceImplType.get(serviceInterfaceType); |
| 111 | + if(serviceImpl==null) |
| 112 | + throw new DeveloperException('No implementation registered for service interface ' + serviceInterfaceType.getName()); |
| 113 | + return serviceImpl.newInstance(); |
| 114 | + } |
| 115 | + |
| 116 | + @TestVisible |
| 117 | + private void setMock(Type serviceInterfaceType, Object serviceImpl) |
| 118 | + { |
| 119 | + m_serviceInterfaceTypeByMockService.put(serviceInterfaceType, serviceImpl); |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + /** |
| 124 | + * Class implements a Selector class factory |
| 125 | + **/ |
| 126 | + public class SelectorFactory |
| 127 | + { |
| 128 | + private Map<SObjectType, Type> m_sObjectBySelectorType; |
| 129 | + private Map<SObjectType, fflib_ISObjectSelector> m_sObjectByMockSelector; |
| 130 | + |
| 131 | + /** |
| 132 | + * Consturcts a Selector Factory linking SObjectType's with Apex Classes implement the fflib_ISObjectSelector interface |
| 133 | + * Note that the factory does not chekc the given Apex Classes implement the interface |
| 134 | + * currently this is not possible in Apex. |
| 135 | + * |
| 136 | + * @Param sObjectBySelectorType Map of SObjectType's to Selector Apex Classes |
| 137 | + **/ |
| 138 | + public SelectorFactory(Map<SObjectType, Type> sObjectBySelectorType) |
| 139 | + { |
| 140 | + m_sObjectBySelectorType = sObjectBySelectorType; |
| 141 | + m_sObjectByMockSelector = new Map<SObjectType, fflib_ISObjectSelector>(); |
| 142 | + } |
| 143 | + |
| 144 | + /** |
| 145 | + * Creates a new instance of the associated Apex Class implementing fflib_ISObjectSelector |
| 146 | + * for the given SObjectType, or if provided via setMock returns the Mock implementaton |
| 147 | + * |
| 148 | + * @param sObjectType An SObjectType token, e.g. Account.SObjectType |
| 149 | + **/ |
| 150 | + public fflib_ISObjectSelector newInstance(SObjectType sObjectType) |
| 151 | + { |
| 152 | + // Mock implementation? |
| 153 | + if(m_sObjectByMockSelector.containsKey(sObjectType)) |
| 154 | + return m_sObjectByMockSelector.get(sObjectType); |
| 155 | + |
| 156 | + // Determine Apex class for Selector class |
| 157 | + Type selectorClass = m_sObjectBySelectorType.get(sObjectType); |
| 158 | + if(selectorClass==null) |
| 159 | + throw new DeveloperException('Selector class not found for SObjectType ' + sObjectType); |
| 160 | + |
| 161 | + // Construct Selector class and query by Id for the records |
| 162 | + return (fflib_ISObjectSelector) selectorClass.newInstance(); |
| 163 | + } |
| 164 | + |
| 165 | + /** |
| 166 | + * Helper method to query the given SObject records |
| 167 | + * Internally creates an instance of the registered Selector and calls its |
| 168 | + * selectSObjectById method |
| 169 | + * |
| 170 | + * @param recordIds The SObject record Ids, must be all the same SObjectType |
| 171 | + * @exception Is thrown if the record Ids are not all the same or the SObjectType is not registered |
| 172 | + **/ |
| 173 | + public List<SObject> selectById(Set<Id> recordIds) |
| 174 | + { |
| 175 | + // No point creating an empty Domain class, nor can we determine the SObjectType anyway |
| 176 | + if(recordIds==null || recordIds.size()==0) |
| 177 | + throw new DeveloperException('Invalid record Id\'s set'); |
| 178 | + |
| 179 | + // Determine SObjectType |
| 180 | + SObjectType domainSObjectType = new List<Id>(recordIds)[0].getSObjectType(); |
| 181 | + for(Id recordId : recordIds) |
| 182 | + if(recordId.getSobjectType()!=domainSObjectType) |
| 183 | + throw new DeveloperException('Unable to determine SObjectType, Set contains Id\'s from different SObject types'); |
| 184 | + |
| 185 | + // Construct Selector class and query by Id for the records |
| 186 | + return newInstance(domainSObjectType).selectSObjectsById(recordIds); |
| 187 | + } |
| 188 | + |
| 189 | + /** |
| 190 | + * Helper method to query related records to those provided, for example |
| 191 | + * if passed a list of Opportunity records and the Account Id field will |
| 192 | + * construct internally a list of Account Ids and call the registered |
| 193 | + * Account selector to query the related Account records, e.g. |
| 194 | + * |
| 195 | + * List<Account> accounts = |
| 196 | + * (List<Account>) Applicaiton.Selector.selectByRelationship(myOpps, Opportunity.AccountId); |
| 197 | + * |
| 198 | + * @param relatedRecords used to extract the related record Ids, e.g. Opportunty records |
| 199 | + * @param relationshipField field in the passed records that contains the relationship records to query, e.g. Opportunity.AccountId |
| 200 | + **/ |
| 201 | + public List<SObject> selectByRelationship(List<SObject> relatedRecords, SObjectField relationshipField) |
| 202 | + { |
| 203 | + Set<Id> relatedIds = new Set<Id>(); |
| 204 | + for(SObject relatedRecord : relatedRecords) |
| 205 | + { |
| 206 | + Id relatedId = (Id) relatedRecord.get(relationshipField); |
| 207 | + if(relatedId!=null) |
| 208 | + relatedIds.add(relatedId); |
| 209 | + } |
| 210 | + return selectById(relatedIds); |
| 211 | + } |
| 212 | + |
| 213 | + @TestVisible |
| 214 | + private void setMock(fflib_ISObjectSelector selectorInstance) |
| 215 | + { |
| 216 | + m_sObjectByMockSelector.put(selectorInstance.sObjectType(), selectorInstance); |
| 217 | + } |
| 218 | + } |
| 219 | + |
| 220 | + /** |
| 221 | + * Class implements a Domain class factory |
| 222 | + **/ |
| 223 | + public class DomainFactory |
| 224 | + { |
| 225 | + private fflib_Application.SelectorFactory m_selectorFactory; |
| 226 | + |
| 227 | + private Map<SObjectType, Type> m_sObjectByDomainConstructorType; |
| 228 | + |
| 229 | + private Map<SObjectType, fflib_ISObjectDomain> m_sObjectByMockDomain; |
| 230 | + |
| 231 | + /** |
| 232 | + * Consturcts a Domain factory, using an instance of the Selector Factory |
| 233 | + * and a map of Apex classes implementing fflib_ISObjectDomain by SObjectType |
| 234 | + * Note this will not check the Apex classes provided actually implement the interfaces |
| 235 | + * since this is not possible in the Apex runtime at present |
| 236 | + * |
| 237 | + * @param selectorFactory, e.g. Application.Selector |
| 238 | + * @param sObjectByDomainConstructorType Map of Apex classes by SObjectType |
| 239 | + **/ |
| 240 | + public DomainFactory(fflib_Application.SelectorFactory selectorFactory, |
| 241 | + Map<SObjectType, Type> sObjectByDomainConstructorType) |
| 242 | + { |
| 243 | + m_selectorFactory = selectorFactory; |
| 244 | + m_sObjectByDomainConstructorType = sObjectByDomainConstructorType; |
| 245 | + m_sObjectByMockDomain = new Map<SObjectType, fflib_ISObjectDomain>(); |
| 246 | + } |
| 247 | + |
| 248 | + /** |
| 249 | + * Dynamically constructs an instance of a Domain class for the given record Ids |
| 250 | + * Internally uses the Selector Factory to query the records before passing to a |
| 251 | + * dynamically constructed instance of the application Apex Domain class |
| 252 | + * |
| 253 | + * @param recordIds A list of Id's of the same type |
| 254 | + * @exception Throws an exception via the Selector Factory if the Ids are not all of the same SObjectType |
| 255 | + **/ |
| 256 | + public fflib_ISObjectDomain newInstance(Set<Id> recordIds) |
| 257 | + { |
| 258 | + return newInstance(m_selectorFactory.selectById(recordIds)); |
| 259 | + |
| 260 | + } |
| 261 | + |
| 262 | + /** |
| 263 | + * Dynamically constructs an instace of the Domain class for the given records |
| 264 | + * Will return a Mock implementation if one has been provided via setMock |
| 265 | + * |
| 266 | + * @param records A concreate list (e.g. List<Account> vs List<SObject>) of records |
| 267 | + * @exception Throws an exception if the SObjectType cannot be determined from the list |
| 268 | + * or the constructor for Domain class was not registered for the SOBjectType |
| 269 | + **/ |
| 270 | + public fflib_ISObjectDomain newInstance(List<SObject> records) |
| 271 | + { |
| 272 | + SObjectType domainSObjectType = records.getSObjectType(); |
| 273 | + if(domainSObjectType==null) |
| 274 | + throw new DeveloperException('Unable to determine SObjectType'); |
| 275 | + |
| 276 | + // Mock implementation? |
| 277 | + if(m_sObjectByMockDomain.containsKey(domainSObjectType)) |
| 278 | + return m_sObjectByMockDomain.get(domainSObjectType); |
| 279 | + |
| 280 | + // Determine SObjectType and Apex classes for Domain class |
| 281 | + Type domainConstructorClass = m_sObjectByDomainConstructorType.get(domainSObjectType); |
| 282 | + if(domainConstructorClass==null) |
| 283 | + throw new DeveloperException('Domain constructor class not found for SObjectType ' + domainSObjectType); |
| 284 | + |
| 285 | + // Construct Domain class passing in the queried records |
| 286 | + fflib_SObjectDomain.IConstructable domainConstructor = |
| 287 | + (fflib_SObjectDomain.IConstructable) domainConstructorClass.newInstance(); |
| 288 | + return (fflib_ISObjectDomain) domainConstructor.construct(records); |
| 289 | + } |
| 290 | + |
| 291 | + @TestVisible |
| 292 | + private void setMock(fflib_ISObjectDomain mockDomain) |
| 293 | + { |
| 294 | + m_sObjectByMockDomain.put(mockDomain.sObjectType(), mockDomain); |
| 295 | + } |
| 296 | + } |
| 297 | + |
| 298 | + public class ApplicationException extends Exception { } |
| 299 | + |
| 300 | + /** |
| 301 | + * Exception representing a developer coding error, not intended for end user eyes |
| 302 | + **/ |
| 303 | + public class DeveloperException extends Exception { } |
| 304 | +} |
0 commit comments