Skip to content

Commit 7359fb5

Browse files
committed
Merge pull request apex-enterprise-patterns#83 from jondavis9898/issue82-support-generic-lists-in-SObjectDomain
Issue apex-enterprise-patterns#82-Add support for creating SObjectDomain classes from generic lists
2 parents 7da892b + 77aabb7 commit 7359fb5

File tree

4 files changed

+185
-5
lines changed

4 files changed

+185
-5
lines changed

fflib/src/classes/fflib_Application.cls

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,37 @@ public class fflib_Application
304304
return (fflib_ISObjectDomain) domainConstructor.construct(records);
305305
}
306306

307+
/**
308+
* Dynamically constructs an instace of the Domain class for the given records and SObjectType
309+
* Will return a Mock implementation if one has been provided via setMock
310+
*
311+
* @param records A list records
312+
* @param domainSObjectType SObjectType for list of records
313+
* @exception Throws an exception if the SObjectType is not specified or if constructor for Domain class was not registered for the SObjectType
314+
*
315+
* @remark Will support List<SObject> but all records in the list will be assumed to be of
316+
* the type specified in sObjectType
317+
**/
318+
public fflib_ISObjectDomain newInstance(List<SObject> records, SObjectType domainSObjectType)
319+
{
320+
if(domainSObjectType==null)
321+
throw new DeveloperException('Must specify sObjectType');
322+
323+
// Mock implementation?
324+
if(m_sObjectByMockDomain.containsKey(domainSObjectType))
325+
return m_sObjectByMockDomain.get(domainSObjectType);
326+
327+
// Determine SObjectType and Apex classes for Domain class
328+
Type domainConstructorClass = m_sObjectByDomainConstructorType.get(domainSObjectType);
329+
if(domainConstructorClass==null)
330+
throw new DeveloperException('Domain constructor class not found for SObjectType ' + domainSObjectType);
331+
332+
// Construct Domain class passing in the queried records
333+
fflib_SObjectDomain.IConstructable2 domainConstructor =
334+
(fflib_SObjectDomain.IConstructable2) domainConstructorClass.newInstance();
335+
return (fflib_ISObjectDomain) domainConstructor.construct(records, domainSObjectType);
336+
}
337+
307338
@TestVisible
308339
private void setMock(fflib_ISObjectDomain mockDomain)
309340
{

fflib/src/classes/fflib_ApplicationTest.cls

Lines changed: 117 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@ private class fflib_ApplicationTest
4242
System.assert(domainObjectAcct instanceof AccountsDomain);
4343
System.assertEquals(testAccountId, domainObjectAcct.getRecords()[0].Id);
4444

45+
// Registered Accounts domain class by SObject List
46+
testAccountId = fflib_IDGenerator.generate(Account.SObjectType);
47+
domainObjectAcct =
48+
Domain.newInstance(
49+
new List<SObject>
50+
{ new Account(
51+
Id = testAccountId,
52+
Name = 'Test Account') }
53+
, Account.SObjectType);
54+
System.assert(domainObjectAcct instanceof AccountsDomain);
55+
System.assertEquals(testAccountId, domainObjectAcct.getRecords()[0].Id);
56+
4557
// Registered Opportunities domain class by SObject List
4658
Id testOpportunityId = fflib_IDGenerator.generate(Opportunity.SObjectType);
4759
fflib_ISObjectDomain domainObjectOpp =
@@ -53,6 +65,19 @@ private class fflib_ApplicationTest
5365
System.assertEquals(testOpportunityId, domainObjectOpp.getRecords()[0].Id);
5466
System.assert(domainObjectOpp instanceof OpportuntiesDomain);
5567

68+
// Test failure for creating new instance using IConstructable2
69+
// for domain class that does not support it
70+
testOpportunityId = fflib_IDGenerator.generate(Opportunity.SObjectType);
71+
domainObjectOpp =
72+
Domain.newInstance(
73+
new List<SObject>
74+
{ new Opportunity(
75+
Id = testOpportunityId,
76+
Name = 'Test Opportunity') }
77+
, Opportunity.SObjectType);
78+
System.assertEquals(testOpportunityId, domainObjectOpp.getRecords()[0].Id);
79+
System.assert(domainObjectOpp instanceof OpportuntiesDomain);
80+
5681
// Given
5782
fflib_ApexMocks mocks = new fflib_ApexMocks();
5883
mocks.startStubbing();
@@ -71,6 +96,18 @@ private class fflib_ApplicationTest
7196

7297
// Then
7398
System.assert(domainObjectAcct instanceof fflib_SObjectMocks.SObjectDomain);
99+
100+
// When
101+
domainObjectAcct =
102+
Domain.newInstance(
103+
new List<SObject>
104+
{ new Account(
105+
Id = testAccountId,
106+
Name = 'Test Account') }
107+
, Account.SObjectType);
108+
109+
// Then
110+
System.assert(domainObjectAcct instanceof fflib_SObjectMocks.SObjectDomain);
74111
}
75112

76113
@IsTest
@@ -115,6 +152,17 @@ private class fflib_ApplicationTest
115152
}
116153
}
117154

155+
@IsTest
156+
private static void callingDomainFactoryWithNoSObjectTypeShouldGiveException()
157+
{
158+
try {
159+
Domain.newInstance(new List<SObject>(), null);
160+
System.assert(false, 'Expected exception');
161+
} catch (fflib_Application.DeveloperException e) {
162+
System.assertEquals('Must specify sObjectType', e.getMessage());
163+
}
164+
}
165+
118166
@IsTest
119167
private static void callingDomainFactoryWithInAccessableConstructorShouldGiveException()
120168
{
@@ -124,8 +172,33 @@ private class fflib_ApplicationTest
124172
} catch (fflib_Application.DeveloperException e) {
125173
System.assertEquals('Domain constructor class not found for SObjectType Product2', e.getMessage());
126174
}
175+
176+
try {
177+
Domain.newInstance(new List<SObject>{ new Product2(Name = 'Test Product') }, Product2.SObjectType);
178+
System.assert(false, 'Expected exception');
179+
} catch (fflib_Application.DeveloperException e) {
180+
System.assertEquals('Domain constructor class not found for SObjectType Product2', e.getMessage());
181+
}
127182
}
128183

184+
@IsTest
185+
private static void callingDomainFactoryWithContructorClassThatDoesNotSupportIConstructableShouldGiveException()
186+
{
187+
try {
188+
Domain.newInstance(new List<Contact>{ new Contact(LastName = 'TestContactLName') });
189+
System.assert(false, 'Expected exception');
190+
} catch (System.TypeException e) {
191+
System.assertEquals('Invalid conversion from runtime type fflib_ApplicationTest.ContactsConstructor to fflib_SObjectDomain.IConstructable', e.getMessage());
192+
}
193+
194+
try {
195+
Domain.newInstance(new List<SObject>{ new Contact(LastName = 'TestContactLName') }, Contact.SObjectType);
196+
System.assert(false, 'Expected exception');
197+
} catch (System.TypeException e) {
198+
System.assertEquals('Invalid conversion from runtime type fflib_ApplicationTest.ContactsConstructor to fflib_SObjectDomain.IConstructable2', e.getMessage());
199+
}
200+
}
201+
129202
@IsTest
130203
private static void callingUnitOfWorkFactoryShouldGivenStandardImplsAndMockImpls()
131204
{
@@ -302,22 +375,33 @@ private class fflib_ApplicationTest
302375
fflib_ApplicationTest.Selector,
303376
new Map<SObjectType, Type> {
304377
Account.SObjectType => AccountsConstructor.class,
305-
Opportunity.SObjectType => OpportuntiesConstructor.class });
378+
Opportunity.SObjectType => OpportuntiesConstructor.class,
379+
Contact.SObjectType => ContactsConstructor.class });
306380

307381
public class AccountsDomain extends fflib_SObjectDomain
308382
{
309383
public AccountsDomain(List<Opportunity> sObjectList)
310384
{
311385
super(sObjectList);
312386
}
387+
388+
public AccountsDomain(List<SObject> sObjectList, SObjectType sObjectType)
389+
{
390+
super(sObjectList, sObjectType);
391+
}
313392
}
314393

315-
public class AccountsConstructor implements fflib_SObjectDomain.IConstructable
394+
public class AccountsConstructor implements fflib_SObjectDomain.IConstructable2
316395
{
317396
public fflib_SObjectDomain construct(List<SObject> sObjectList)
318397
{
319398
return new AccountsDomain(sObjectList);
320399
}
400+
401+
public fflib_SObjectDomain construct(List<SObject> sObjectList, SObjectType sObjectType)
402+
{
403+
return new AccountsDomain(sObjectList, sObjectType);
404+
}
321405
}
322406

323407
public class OpportuntiesDomain extends fflib_SObjectDomain
@@ -326,15 +410,44 @@ private class fflib_ApplicationTest
326410
{
327411
super(sObjectList);
328412
}
413+
414+
public OpportuntiesDomain(List<SObject> sObjectList, SObjectType sObjectType)
415+
{
416+
super(sObjectList, sObjectType);
417+
}
329418
}
330419

331-
public class OpportuntiesConstructor implements fflib_SObjectDomain.IConstructable
420+
public class OpportuntiesConstructor implements fflib_SObjectDomain.IConstructable2
332421
{
333422
public fflib_SObjectDomain construct(List<SObject> sObjectList)
334423
{
335424
return new OpportuntiesDomain(sObjectList);
336425
}
337-
}
426+
427+
public fflib_SObjectDomain construct(List<SObject> sObjectList, SObjectType sObjectType)
428+
{
429+
return new OpportuntiesDomain(sObjectList, sObjectType);
430+
}
431+
}
432+
433+
public class ContactsDomain extends fflib_SObjectDomain
434+
{
435+
public ContactsDomain(List<Opportunity> sObjectList)
436+
{
437+
super(sObjectList);
438+
}
439+
440+
public ContactsDomain(List<SObject> sObjectList, SObjectType sObjectType)
441+
{
442+
super(sObjectList, sObjectType);
443+
}
444+
}
445+
446+
// Intentionally does not support IConstructable or IConstructable2 interfaces in order to support testing
447+
public class ContactsConstructor
448+
{
449+
450+
}
338451

339452
class OpportuntiesSelector extends fflib_SObjectSelector
340453
{

fflib/src/classes/fflib_SObjectDomain.cls

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,30 @@ public virtual with sharing class fflib_SObjectDomain
8282

8383
/**
8484
* Constructs the domain class with the data on which to apply the behaviour implemented within
85+
*
86+
* @param sObjectList A concreate list (e.g. List<Account> vs List<SObject>) of records
87+
8588
**/
8689
public fflib_SObjectDomain(List<SObject> sObjectList)
90+
{
91+
this(sObjectList, sObjectList.getSObjectType());
92+
}
93+
94+
/**
95+
* Constructs the domain class with the data and type on which to apply the behaviour implemented within
96+
*
97+
* @param sObjectList A list (e.g. List<Opportunity>, List<Account>, etc.) of records
98+
* @param sObjectType The Schema.SObjectType of the records contained in the list
99+
*
100+
* @remark Will support List<SObject> but all records in the list will be assumed to be of
101+
* the type specified in sObjectType
102+
**/
103+
public fflib_SObjectDomain(List<SObject> sObjectList, SObjectType sObjectType)
87104
{
88105
// Ensure the domain class has its own copy of the data
89106
Records = sObjectList.clone();
90107
// Capture SObjectType describe for this domain class
91-
SObjectDescribe = Records.getSObjectType().getDescribe();
108+
SObjectDescribe = sObjectType.getDescribe();
92109
// Configure the Domain object instance
93110
Configuration = new Configuration();
94111
}
@@ -255,6 +272,14 @@ public virtual with sharing class fflib_SObjectDomain
255272
{
256273
fflib_SObjectDomain construct(List<SObject> sObjectList);
257274
}
275+
276+
/**
277+
* Interface used to aid the triggerHandler in constructing instances of Domain classes
278+
**/
279+
public interface IConstructable2 extends IConstructable
280+
{
281+
fflib_SObjectDomain construct(List<SObject> sObjectList, SObjectType sObjectType);
282+
}
258283

259284
/**
260285
* For Domain classes implementing the ITriggerStateful interface returns the instance
@@ -654,6 +679,12 @@ public virtual with sharing class fflib_SObjectDomain
654679
// Domain classes are initialised with lists to enforce bulkification throughout
655680
super(sObjectList);
656681
}
682+
683+
public TestSObjectDomain(List<Opportunity> sObjectList, SObjectType sObjectType)
684+
{
685+
// Domain classes are initialised with lists to enforce bulkification throughout
686+
super(sObjectList, sObjectType);
687+
}
657688

658689
public override void onApplyDefaults()
659690
{

fflib/src/classes/fflib_SObjectDomainTest.cls

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ private with sharing class fflib_SObjectDomainTest
3535
System.assertEquals(1, fflib_SObjectDomain.Errors.getAll().size());
3636
System.assertEquals('You must provide an Account for Opportunities for existing Customers.', fflib_SObjectDomain.Errors.getAll()[0].message);
3737
System.assertEquals(Opportunity.AccountId, ((fflib_SObjectDomain.FieldError)fflib_SObjectDomain.Errors.getAll()[0]).field);
38+
39+
opps = new fflib_SObjectDomain.TestSObjectDomain(new SObject[] { new Opportunity ( Name = 'Test', Type = 'Existing Account' ) }, Opportunity.SObjectType );
40+
System.assertEquals(1, fflib_SObjectDomain.Errors.getAll().size());
41+
System.assertEquals('You must provide an Account for Opportunities for existing Customers.', fflib_SObjectDomain.Errors.getAll()[0].message);
42+
System.assertEquals(Opportunity.AccountId, ((fflib_SObjectDomain.FieldError)fflib_SObjectDomain.Errors.getAll()[0]).field);
3843
}
3944

4045
@IsTest

0 commit comments

Comments
 (0)