Open reignman5 opened 3 years ago
TriggerHandler Like your method naming here, but how would you handle methods with multiple conditions. For example: EmployeeNumber changed and employee number above 3000. Would you iterate twice over the records?
Yes, iterating multiple times over records is not something to be afraid of, if it helps improving code reuse and readability. What you can do if you know that there are multiple methods needing the same criteria/condition then you could gather them in one method like so;
public override void onAfterUpdate(...)
{
onChangedStatus();
}
private void onChangedStatus()
{
List<Case> changedRecords = getChangedRecords(new List<SObjectField>{ Case.Status });
if (changedRecords.isEmpty()) return;
ICases cases = Cases.newInstance(changedRecords);
forHighPrioritySendEmailNotification(cases);
forEmeaRegionReAssignQueue(cases);
}
private void forHighPrioritySendEmailNotification(ICases cases)
{
ICases highPriorityCases = cases.selectByPriority('High');
if (highPriorityCases.isEmpty()) return;
CasesService.sendHighPriorityNotificationEmail(highPriorityCases);
}
Selector Layer This layer is only called by Service Layer and TriggerHandlers and NOT by the domain layer? Even if I only need a few fields from the parent object?
Yes, the only purpose of the domain is to be a wrapper around a list of (S)Objects. It only contains getters, setters and filter methods. Nothing more. If you need something outside that scope it should be on a service method.
Service Layer You say in the service layer we have cross domain logic. But your example is an AccountsService and you are only calling the domain layer. Answer to question 2 will probably clear that up, but why do you need an AccountsService if object specific logic is in the domain layer?
If you need records that are outside the scope of the domain (the records which are part of the domain) the you need another domain with those records. This pattern clearly separates the high logic from the lower logic. Service methods pass around domains, and if you need todo an operation on those records contained in a domain, then you call from the service a method on the domain. This also avoids iterations in service methods, and keeps the logic clearly structured and readable. This structure keeps it very clear where what logic should go.
Domain Layer 4.1 How would you handle selectors with multiple conditions. Iterate twice with two or methods or put the logic in one method?
Yes, you would iterate multiple times over the same list. This increases the code reuse as one method is only doing one thing. Method re-use is exponentially smaller when you start to combine more things. Remember the goal is not to create fast execution code, but code that is easy to read, maintain and extend.
new Accounts(records)
.selectByRegion('EMEA') // does the first iteration
.selectByEmployeeNumbersGreaterThan(500); // does the second iteration
You would want to add some guard clauses at some point to prevent it from iterating over empty lists.
Domain Layer 4.2 Why do your selectors return classes? Any other reason than chaining?
One main reason is chaining, as you usually want to do multiple things like the example below. And we are dealing with domains, that a domain is a wrapper around the list of (S)Objects is a complete other thing. This also prevents developers from digging in too deep at a too low level of abstraction.
new Accounts(records)
.selectByRegion('EMEA') // First filter
.setRating('Warm'); // then apply logic
Domain Layer 4.3 returning classes becomes a problem when you have extending classes that use common selector logic in the parent class
In your example chaining indeed becomes difficult. You can try to see if you are able to use one common interface for the domain containing all the method (using e.g. overriding), but if you need to add extra method you probably want to do something like:
public class Contracts1 extends Contracts implements IContracts1
{
public override void calculateRent()
{
selectContractsWithManualRent(); // Method on IContacts
setAmountManual(); // Method on IContacts1
}
}
Domain Layer 4.4 Scenario: I have a contracts object with different record types. Those contract records have fields that get calculated onBeforeUpdate. Which fields and how they are calculated is depending on the record type. So I would like your opinion on if this code is true to apex commons.
I would do something very similar with a few small changes:
public with sharing class ContractsTriggerHandler extends fflib_SObjectDomain
{
public override void onBeforeUpdate(Map<Id, SObject> existingRecords)
{
calculateRent();
}
private void calculateRent()
{
Map<String, IContacts> contactsByRecordType =
Contracts.newInstance(getRecords())
// you use a getter to return a map where records are wrapped in a domain grouped by recordType
.getByRecordType();
for (String recordType : contactsByRecordType.keySet)
{
((IContracts) Application.contractsByRecordType.newInstanceByRecordType(
contactsByRecordType.get(recordType),
recordType)
.calculateRent();
}
}
}
@wimvelzeboer Thanks for your quick answers! And great that I wasnt too far off with my code.
Just regarding 4.3
public class Contracts1 extends Contracts implements IContracts1
{
public override void calculateRent()
{
selectContractsWithManualRent(); // Method on IContacts
setAmountManual(); // Method on IContacts1
}
}
setAmountManual would not use the records selected by selectContractsWithManualRent right? So would you instantiate a new IContracts1 Class on the Contracts1 domain?
Thats correct, I was too quick. It should have been:
public class Contracts1 extends Contracts implements IContracts1
{
public override void calculateRent()
{
IContacts withManualRent = selectContractsWithManualRent(); // Method on IContacts
newInstance(withManualRent).setAmountManual(); // Method on IContacts1
}
}
or you can do something like:
public class Contracts1 extends Contracts implements IContracts1
{
public override void calculateRent()
{
selectContracts1WithManualRent()
.setAmountManual(); // Method on IContacts1
}
private Contacts1 selectContracts1WithManualRent()
{
return newInstance(
selectContractsWithManualRent()
);
}
}
@wimvelzeboer Hey, Im making good progess in implementing apex commons. Just one more question. Imagine I have a business logic that if a case has status = Closed, needs to create a different object. Would I put that logic in the case service layer or in the service layer of the oter object?
Hi @reignman5
When developing this logic, you always identify the business logic and what it is triggered by. In your case its very clear that the trigger is an onAfterUpdate Trigger Handler event invoking a method on the trigger handler named something like onChangedStatusToClosedCreateDifferentObject()
.
All this method does is validating the condition 'onChangedStatusToClosed' and then invoking the business logic createDifferentObject
and passing the applicable data.
The question you should ask yourself at this point is, what input requires the method createDifferentObject
. Does it only needs the record Ids e.g. to relate the child objects to its parent, or does it require more information from the Case object that had its status changed to closed.
If you know this answer you know in which service layer you should put the logic.
In the first case where it only requires the record Ids, the Trigger Handler method onChangedStatusToClosedCreateDifferentObject
would extract the record Ids (via getRecordIds()
) and call the service layer of the DifferentObject type passing along the recordIds.
Something like this:
private void onChangedStatusToClosedCreateDifferentObject()
{
List<Case> changedRecords =
getChangedRecords(new List<Schema.SObjectField>{ Case.Status } );
if (changedRecords.isEmpty()) return;
Cases casesChangedToClosed = Cases.newInstance(changedRecords).selectByStatus('Closed');
if (casesChangedToClosed.isEmpty()) return;
DifferentObjectService.createObjectsByParentId(
casesChangedToClosed.getRecordIds()
);
}
Or you need more information from the source object then you would call the CasesService method first. Trigger handler
private void onChangedStatusToClosedCreateDifferentObject()
{
... // same filter and select as previous example
CasesService.createDifferentObjects(casesChangedToClosed)
);
}
CasesService Implementation
public void createDifferentObjects(Cases cases)
{
fflib_ISObjectUnitOfWork unitOfWork = Application.UnitOfWork.newInstance();
createDifferentObjects(unitOfWork, cases);
unitOfWork.commitWork();
}
public void createDifferentObjects(fflib_ISObjectUnitOfWork unitOfWork, Cases cases)
{
// start with gathering all the information from the source object and translate that into primitive variables.
Map<Id, status> caseStatusByCaseId = cases.getStatusById();
// perform any data sanitising / validation stuff
....
// Call the DifferentObject service with the primitive variables
DifferentObjectService.createObjectByCaseStatusAndId(unitOfWork, caseStatusByCaseId);
}
Here you see a number of things happening: 1.) The Cases Service method is first service method invoked, it is the responsibility of that service to create a UnitOfWork to capture all DML work and at the end of the entire process commits the work to the database. 2.) The Case Service is translating the "complex" case object into primitives, which it passes on to the DifferentObjectService. Always try to only pass data to services in its most primitive form. That will increate the chances that service methods can be reused by different parts of the application. Do be careful not to over do this and end up in a situation where you have e.g. extracted the Id of the record and then later on that you need to query it again.. 3.) The condition of the record that has its status changed to closed, it not present in the service method, which allows the service method to be used also for other source records.
The basic rule here is, if business logic is cross domains then you usually start on the domain where the logic starts. And then the business logic starts right after the point where it is initiated, e.g. from a trigger handler, WebService, Controller, etc. and where the records/data has been identified, filtered and translated/casted/converted. So, the condition (case status changed to Closed) is usually not(!) part of the business logic.
I hope this helps you identifying where the service logic should go.
@wimvelzeboer Your answer was a really big help. We are making great progress and we already seeing benefits in code reuse. Really happy about it. One thing that occurs sometimes that we have different logic on the same field change. So lets say on case status = 'done' Description should be set to 'done Case', but also on case Status 'done' all cases with Accounts should have checkbox 'account case' set to true. Would you put that in two different methods in the triggerhandler or in one. Example 2 methods: onStatusChangedToDoneSetDescription, onStatusChangedToDoneAndWithAccountSetAccountCase 1 method: onStatusChangedToDoneSetDescriptionAndSetAccountCase
Hi @reignman5, Nice to hear that you already starting to see the benefits of the design pattern! That is indeed one of the major drivers of using the framework, write less code. :-)
To your question, if there are many things happening with the same condition, I would use a separate method. Something like:
public override void onBeforeUpdate(..)
{
onStatusChangedToDone();
}
private void onStatusChangedToDone()
{
List<...> changedRecords = ...
if (changedRecords.isEmpty()) return;
Cases domain = Cases.newInstance(changedRecords);
// Do all logic
domain.setDescription('Hello World!');
domain.selectWithAccounts()
.setSomeOtherField();
}
If the 'Do all logic` part is too complex, then you would prefer to have just a list of other (private) method calls, similar to the onBeforeUpdate method.
@wimvelzeboer We are making great progress, having cut down our monstrous 5000 line god class to around 1500 lines. Now we even split the new class in a setter, getter and selector class as in your documentation. And before asking my questions, I just wanted to thank you again for all your help. I really appreciate that and if im getting on your nerves dont hesitate to say that!
public class Constructor implements fflib_IDomainConstructor
@reignman5 Wow 5000 lines, that is indeed a huge class. Unfortunately that is quite common in Salesforce development. Its so easy to develop things that sometimes the architecture layer is skipped, resulting into things like this.
To your first question, there are four things a domain can contain; getters, setters, selectors and domain level business logic. So, the last one is the part that still stays on the domain if you already have separate classes for the first three. These methods are small methods typically grouping some of the other domain methods into small domain scale business logic Something like:
public void recalculateRating()
{
selectByNumberOfEmployeesGreaterThan(500)
.setRating('Hot');
selectByNumberOfEmployeesLessThan(500)
.setRating('Warm');
}
This method stays within the context of the domain, it doesn't need any external references, it just makes service methods a bit smaller so that they can focus on higher level business logic.
And indeed that abstract domain class 'Accounts' is indeed incorrect. I think I was a little bit too enthusiastic while doing some copy-pasting at the time I wrote that. I just fixed that! Thanks!
About the excessive public count, that is indeed something that is not really possible to solve if you already split it. I mainly see that happing with large objects with many fields. That the number of getters and settters gets to high. The only way to solve that is looking at your table structure and see if you can simplify that and extract some data into separate tables. Sometimes working with skinny tables and have separate domains for those, can resolve some of that complexity.
Another thing I usually see is that project/sdeveloper that start using fflib have a high volume of selector methods at the start. Overtime you will learn that when you make smaller methods, you mainly focus more on selecting by an Id or by a single field. Also do not over complicate selector method in domains, try to select by a single field. The more complex the filter condition is the lower the chances are that that selector can be re-used.
I also have this PR open to the fflib repo to introduce a more fluent way of building these selector methods, and that you can re-use the filter conditions for the domain and the selector. It has been sitting thee for a while now, but I hope they will merge it someday.
@wimvelzeboer Thanks for the quick answer again. Yes that was an abomination of a class, but also had a lot of unused code. Just to be clear here, with selectors I meant the domainSelectors as in selectByField and then iterating over the records. Maybe that sould be named filterByField to not have this confusion with the (soql)selector class
Using "selector" for both the domain and selector layer might be confusing at the start, but since it's both filtering data I think its ok.
@wimvelzeboer Probably, we were ok with it as of now. And from a pure naming standpoint it makes sense.
I was just scrolling through the code again. I just thought about where to put two things I have in my domain.
Hi @reignman5
If you want to create new objects, then I would suggest to have a factory class, that class can then use a domain to populate the records with the required values.
public class AccountFactory()
{
public IAccounts generateCustomerAccount()
{
return generateAccounts(1)
.setType('Customer')
.setRating('Cold');
}
public IAccounts generateAccounts(Integer amount)
{
List<Account> result = new List<Account>();
for (Integer i = 0; i < amount; i++)
{
result.add(new Account());
}
return Accounts.newInstance(result);
}
About the days calculation. If its the same logic I would extract that into its own private method that you place in the highest level. Another way could be to have every plain getter and setter method, that doesn't do any calculation, but extract that into the service layer.
@wimvelzeboer Right now we are starting to refactor our tests. We are starting with unit tests for our domain classes as it is the easiest. What we are still discussing is when to use integration tests, as in using dml. For now our perspective is, service and domain methods will be mocked, for controller and triggerhandler methods we will write integration tests.
In your doc you mock your controller methods and I cant find an example for triggerhandlers. Would love your opinion on that topic.
Best regards
@reignman5 Indeed unit-test for domains are the easiest. No mocking or DML statements should be required for those. Just only creating some data in memory and running that through the Domain class.
I always advise to work with feature tests, having one test method that runs through an entire feature from front to end, trying to do as many assertions as possible, on the same data set.
A way to start moving that that structure is to take the TriggerHandler events (e.g. onBeforeInsert) and insert one set of records with as many different possibilities, one DML statement (it becomes usually a large bulk one of over 200 records), and again many assertions on the same data set. You would want to try to avoid having many test methods doing DML statements.
I recently added this code snippet with an example about this structure. It's currently only taking one scenerio but the structure has room for an endless list.
I am still working on the documentation, but usually try to start with the code snippets first. :-P
@wimvelzeboer Ah thanks for the code. That makes sense. So would you write mock tests for triggerhandler methods (Example: onChangedStatusSetRating)? Why are you writing mock tests for controllers? Wouldnt a feature test make sense there?
@wimvelzeboer Also we are running into a problem that we cant use the mocking framework, if we try to verify that a method called another method in the same class. So for example I have a method that groups methods together.
calculate(){
firstcalc();
secondcalc();
}
We would like to unit test the calculate method by just verifying that it called firstcalc() and secondcalc(). But it is not working as it is showing the exception that the methods did not run. Is that really not possible?
So would you write mock tests for triggerhandler methods (Example: onChangedStatusSetRating)?
Yeah, you will use standard unit-test (that use mocking) on any other level than the 'onBeforeInsert'.
Why are you writing mock tests for controllers? Wouldnt a feature test make sense there?
I think you have to make a decision, either you do the integrated tests based on; feature, Database Events (Triggers) or Controllers. Do not try to mix them in one application.
... if we try to verify that a method called another method in the same class. ...
yeah, well. That is indeed a problem the Salesforce Stub API cannot handle. Alternatively you could create new instances and call those, something like:
calculate(){
newInstance(this).firstcalc();
newInstance(this).secondcalc();
}
But I would only do that if there was a real high need for it. In normal life I would never do this.
@wimvelzeboer Thanks for your answers. Huge help as always!
But I would only do that if there was a real high need for it. In normal life I would never do this.
I hope you mean regarding using newInstance(this), not regarding grouping methods in another method in the same class. Otherwise my whole developer life would have been a lie :D
@reignman5 yeah, I was referring to adding that newInstance stuff 🤣
@wimvelzeboer Sorry for bothering again, but we just ran into a problem regarding testing, we cant seem to find a solution for: We are testing this service method:
public void resetApartmentStatus(Set<Id> contractIds) {
List<Contract__c> relevantContracts = ContractsSelector.newInstance()
.selectApartmentAndAccountById(contractIds);
IContracts contracts = Contracts.newInstance(relevantContracts)
.selectByAccountEqualsApartmentCurrentTenant();
if (contracts.isEmpty()) {
return;
}
Set<Id> apartmentIds = contracts.getIds(Schema.Contract__c.ApartmentRef__c);
ApartmentsService.resetStatus(apartmentIds);
}
This is the unit test:
static void resetApartmentStatusTest() {
Id accountId = fflib_IDGenerator.generate(Account.SObjectType);
Apartment__c apartment = new Apartment__c(
Id = fflib_IDGenerator.generate(Apartment__c.SObjectType),
CurrentTenant__c = accountId
);
Set<Id> apartmentIds = new Set<Id>{ apartment.Id };
List<Contract__c> records = new List<Contract__c>{
new Contract__c(
ID = fflib_IDGenerator.generate(Contract__c.SObjectType),
ApartmentRef__r = apartment,
Account__c = accountId
),
new Contract__c(
ID = fflib_IDGenerator.generate(Contract__c.SObjectType),
ApartmentRef__r = apartment,
Account__c = fflib_IDGenerator.generate(Account.SObjectType)
)
};
Set<Id> contractIds = new Set<Id>{
fflib_IDGenerator.generate(Contract__c.SObjectType),
fflib_IDGenerator.generate(Contract__c.SObjectType)
};
IContracts domain = Contracts.newInstance(
new List<Contract__c>{ records[0] }
);
fflib_ApexMocks mocks = new fflib_ApexMocks();
IContracts contractsDomainMock = (IContracts) mocks.mock(IContracts.class);
IContracts contractsDomainMock = (IContracts) mocks.;
IContractsSelector contractsSelectorMock = (IContractsSelector) mocks.mock(
IContractsSelector.class
);
IApartmentsService apartmentsServiceMock = (IApartmentsService) mocks.mock(
IApartmentsService.class
);
mocks.startStubbing();
mocks.when(contractsSelectorMock.sObjectType())
.thenReturn(Contract__c.SObjectType);
mocks.when(contractsSelectorMock.selectApartmentAndAccountById(contractIds))
.thenReturn(records);
mocks.when(contractsDomainMock.getType())
.thenReturn(Contract__c.SObjectType);
mocks.when(
contractsDomainMock.selectByAccountEqualsApartmentCurrentTenant()
)
.thenReturn(domain);
mocks.when(contractsDomainMock.getIds(Schema.Contract__c.ApartmentRef__c))
.thenReturn(apartmentIds);
mocks.stopStubbing();
Application.Domain.setMock(contractsDomainMock);
Application.Selector.setMock(contractsSelectorMock);
Application.Service.setMock(
IApartmentsService.class,
apartmentsServiceMock
);
System.Test.startTest();
new ContractsServiceImpl().resetApartmentStatus(contractIds);
System.Test.stopTest();
((IContracts) mocks.verify(contractsDomainMock))
.selectByAccountEqualsApartmentCurrentTenant();
((IContracts) mocks.verify(contractsDomainMock))
.getIds(Schema.Contract__c.ApartmentRef__c);
((IContractsSelector) mocks.verify(contractsSelectorMock))
.selectApartmentAndAccountById(contractIds);
((IApartmentsService) mocks.verify(apartmentsServiceMock))
.resetStatus(apartmentIds);
}
Problem is ((IContracts) mocks.verify(contractsDomainMock)).getIds(Schema.Contract__c.ApartmentRef__c);
is throwing an exception that it did not run.
It is working if we change Set<Id> apartmentIds = contracts.getIds(Schema.Contract__c.ApartmentRef__c);
in the service method to Set<Id> apartmentIds = Contracts.newInstance(records).getIds(Schema.Contract__c.ApartmentRef__c);
.
So the problem must be that in the test getIds() is not using the mock.
But we dont know how we can force that. We tried to return the mock through this line
mocks.when(
contractsDomainMock.selectByAccountEqualsApartmentCurrentTenant()
)
.thenReturn(domain);
But then we get a null exception on the guard clause.
@wimvelzeboer Forget it, we just figured it out. We just had to stub the isEmpty() method. A lot to learn, but should be worth it.
Have a great weekend!
@reignman5 Good for you! Mocking can indeed be tricky sometimes.
I am also working on an extension pack on top of apex-common with a lot of extra features that might interest you. You can have a look at them here: fflib-apex-extensions .
@wimvelzeboer we successfully refactored a big part of our code base. Big thanks to you, would not have been possible without your help.
But one problem we are facing is doing mass updates on our biggest custom object. I tried to update 200 records through anonymous apex, but Im getting apex cpu time limit error. When i analyze the debug log it seems all those getChangedRecords loops really add up, but also seem to take way too long. Example here: Why is it taking 3 seconds to loop through 200 records. Is there anything I am missing?
@reignman5 Good to hear that you have got quite far with refactoring. As with many frameworks we sacrifice a bit of the CPU time to have a better manageable code base. It is therefore not surprising that you suddenly hit CPU limits while the non-refactored code did not.
You typically solve these issues by looking at another approach, like using changeEvents for after Insert/Update operations instead of realtime execution.
The problem that you are currently facing with the getChangedRecords method, might have something todo that this methods does an iteration inside another iteration. If you check for a high number of records e.g. 200 and you also check if any of the given 10 fields are changed then you can quickly have an issue. When none of those records seem to be changed on those fields, then it had todo 2,000 iterations for this single method call. If you increase the number of fields then that will only become more. How many fields are you checking in that onChangedFieldsForRentCalculationCalculateRent method?
@wimvelzeboer Yes you are right this particular getChangedRecordLoop has 4 different fields to check. But I also had another one with a Single field, which took 1800ms. Could it be that the Performance was so slow because I did that in a Developer Org?
@reignman5 4 fields to check isn't that many, could you share the code snippet of the methods that takes 1800ms? It will run slower on a Developer Org but this method should not take that long.
@wimvelzeboer It was just this. the getChangedRecords method in this method lasted 1800ms according to the debug log. I will try it in our partial Sandbox and tell you if that changed anything on the performance
private void onChangedTECHTriggerRegenerateContractMessagetoTrueSentReCheckContractMail() {
List<Contract__c> changedRecords = getChangedRecords(
new Set<Schema.SObjectField>{
Schema.Contract__c.TECH_Trigger_Regenerate_Contract_Message__c
}
);
if (changedRecords.isEmpty()) {
return;
}
@reignman5 That does look just fine, very odd it takes that much time
@wimvelzeboer I also looked at your feature test example again. https://github.com/wimvelzeboer/fflib-training-sessions/blob/main/SoC/force-app/main/tests/classes/featureTests/AccountsTriggerHandlerTest.cls
Did you also do that for an update dml feature test? I dont really like my pattern. Example:
testData.addAll(generateDataOnChangedRecordTypeSetHandlingFeeUpdate());
testData.addAll(generateDataOnChangedContractDateCalculateDurationUpdate());
insert testData;
for(Contract__c contract:testData){
changeDataOnChangedRecordTypeSetHandlingFeeUpdate());
changeDataOnChangedContractDateCalculateDurationUpdate());
}
update testData;
Really stressfull to find the right records for the changeDataMethods
@wimvelzeboer in the partial sandbox the perfomance got way better. so seems to be a salesforce problem. But thanks for helping me!
It's interesting discussion going on here. I would like to ask about the the approach for working with results of the query with related records. Let's say I have the following simplistic code with a selector query in my service implementation class
List<Account> accounts = accountsSelector.selectWithPersonTasksByIds(accountIds);
PersonTasks
) What is the best approach here assuming that the domain is not aware of other (S)ObjectTypes than its own
Without overthinking the most straightforward solution is to create a getter on the domain let's say
public Map<Id,List<Task>> getAccountIdToPersonTasks(){
Map<Id,List<Task>> result = new Map<Id,List<Task>>()
for(Account account: (List<Account>) getRecords()){
result.put(account.Id, account.PersonTasks);
}
return result;
}
then in the service we can do the following
Map<Id,List<Task>> accountIdToPersonTasks = Accounts.newInstance(accountIds).getAccountIdToPersonTasks();
for(Account account: accounts.getRecords){
Tasks tasksDomain = Tasks.newInstance(accountIdToPersonTasks.get(account.Id));
tasksDomain.setChildrenValues();
}
I don't like the fact that the account.PersonTasks
is being called in the domain since it's breaking the cleanliness of the domain layer. Second thing is that the loop in the service is antipattern since fflib calls SOQLs in the background to instantiate the domain.
I would say that the remedy for the loop is to simply create a method that acts based on the parent Id setChildrenValuesByParentId
but I am not sure how to solve the first problem
public void setLastCustomerActivitySubject() {
for (Account account : (List<Account>) getRecords()) {
account.LastCustomerActivitySubject__c = account.PersonTasks.get(0).Subject;
}
}
Here I also have doubts since the account.PersonTasks
is being called in the domain and references the Task field.
Any guidelines how to approach these kind of problems?
Best, Szymon
@szymon-halik the last years I am mainly working with large Salesforce instances where objects have around or more than 1 million records per table. Indexing and selective queries then becomes very important. In those cases I would only run queries on one table at a time, and avoid relational or non-indexed queries.
In those cases I would write in my service class something like:
IAccounts accounts = Accounts.newInstance(accountIds);
ITasks tasks = Tasks.newInstance(TasksSelector.newinstance().selectByAccountId(accountIds));
List<Id, Id> taskIdByAccountId = tasks.getMostRecentIdByAccountId();
accounts.setLastCustomerActivityById(taskIdByAccountId);
This structure leaves the domain classes clean, with methods that can easily be re-used, and both domains are not aware of the other. Only primitive data types are exchanged.
You might even want to change the selector so that it only returning one record/Id per accountId if there are many tasks per account.
Down side of this approach is that you need to run two queries against the database, but with proper designed selector classes those two queries will run faster as they can utilize indexing.
Side Note: In my example you might notice that I re-use the name of the domain class as variable names. Some people find that a bad practice, as you can not call any static method on that domain class after the variable declaration. But I find it very useful, as it helps me to write shorter methods, that only do one thing.
@szymon-halik I see that I still have to write a Wiki page for it, but if you have multi layered relationships like; parent -> child -> grand-child or child -> parent -> grand-parent. Then you might want to have a look at these domain methods: getChildIdsById
and getParentIdById
, (maybe combined with the fflib_ArrayUtils.replaceValue method)
The two methods can link the record Id of the parent to the Id of the grand-child or child to the grand-parent.
You can then use those two methods to extract the right data from one domain and use it for the top level domain, or visa-versa.
These two methods have ApexDocs with examples.
@wimvelzeboer thanks for your help here. I really like the idea of exchanging only the primitive data types in between layers but I still have a doubt how to construct the final map/list properly. I review the selector layer guidelines and this problem is covered there by I can't find any reference how to construct mentioned method getRecordsByRaceId()
So far I can easily to the following:
//this scans the whole Task table to get only those related to particular Accounts
TasksInterface tasksDomain = Tasks.newInstance(TasksSelector.newInstance().selectByAccountIds(accountsDomain.getRecordIds()));
//creates a map of all related tasks => it's preparation for sorting
Map<Id,Date> accountIdByTaskCreatedDate = tasksDomain.getAccountIdToTaskCreatedDate();
//some kind of sort/compare if needed here to be able to determine which CreatedDate is the most recent one for each account
//once sorted then create map accountIdByLastCustomerActivity and pass to domain setter
accountsDomain.setLastCustomerActivityDateById(accountIdByLastCustomerActivity);
Would you recommend to create some custom sort to be able to determine the most recent created child?
I think it must be quite common scenario to fetch most recent child records and set some parent attributes based on that but I can find good example for implementing that using split domain structure
@szymon-halik In your case that you can do a small hack, to avoid querying to many records.
You can do a selector with the following query:
SELECT AccountId, MAX(Id) Id FROM Task WHERE AccountId IN :accountId GROUP BY AccountId
That will return the record that you need per AccountId. Id does however not return all the task fields. But you can take the recordIds from this query, and run another query (selectById
) to fetch only the latest records.
Then you can use a Tasks domain method to return you the map getCreatedDateByAccountId()
Set<Id> taskIds =
Tasks.newInstance(
TasksSelector.newInstance().selectMaxIdByAccountIds(accountsDomain.getRecordIds()))
.getRecord(Ids);
ITasks tasks = Tasks.newInstance(taskIds);
Map<Id, Datetime> createdDateByAccountId = tasks.getCreatedDateByAccountId();
accounts.setLastCustomerActivityById(createdDateByAccountId);
NOTE: The selector method selectMaxIdByAccountIds
does need to do a conversion from AggregatedResult to Task
This is only something that would work if you are looking for the CreatedDate. Something like LastModifiedDate or another custom date(time) field would not work.
In those cases I would have a method like Map<Id, Datetime> getMaxCreatedDateByAccountId()
, which is just an iteration over the records, checking the map and storing the value if the createdDate is later than the one in the map.
The above works like a charm - I had to abstract the AggregateResult to a separate class to make it scalable but other than that the small hack works
@wimvelzeboer I have started to work recently on the testing policy for our org and I have noticed that we must write separate unit test for each overloaded entry point in the Service
layer. Let's say that we have the following AccountsService
public with sharing class AccountsService {
public static void closeCallReminders(Set<Id> accountIds){
service().closeCallReminders(accountIds);
}
public static void closeCallReminders(AccountsInterface accountsDomain){
service().closeCallReminders(accountsDomain);
}
public static void closeCallReminders(AccountsInterface accountsDomain, fflib_ISObjectUnitOfWork unitOfWork) {
service().closeCallReminders(accountsDomain, unitOfWork);
}
private static AccountsServiceInterface service() {
return (AccountsServiceInterface) SalesApplication.service.newInstance(AccountsServiceInterface.class);
}
}
Implementation class
public with sharing class AccountsServiceImp implements AccountsServiceInterface {
public void closeCallReminders(Set<Id> accountIds) {
closeCallReminders(Accounts.newInstance(accountIds));
}
public void closeCallReminders(AccountsInterface accountsDomain) {
fflib_ISObjectUnitOfWork unitOfWork = SalesApplication.unitOfWork.newInstance();
closeCallReminders(accountsDomain, unitOfWork);
unitOfWork.commitWork();
}
public void closeCallReminders(AccountsInterface accountsDomain, fflib_ISObjectUnitOfWork unitOfWork) {
//some irrelevant logic here
unitOfWork.registerDirty(openCallReminders.getRecords());
}
}
Test class
@IsTest
private class AccountsServiceImpTest {
@IsTest
static void callingServiceShouldCloseOpenCallReminders() {
//given
//data and mocks setup
//when
AccountsService.closeCallReminders(accountIds);
//then
//mocks verify
}
}
The callingServiceShouldCloseOpenCallReminders
method covers only this method signature -> closeCallReminders(Set<Id> accountIds)
from AccountsService
. AccountsServiceImp
is covered fully. My question is what is the best practise here? I saw in a few scenarios that it might be beneficial to limit the when call in unit test to a new instance of AccountsServiceImp
but then you don't get any coverage for AccountsService
.
So far, we have tests that are firing based on the entry scenario to fully coverAccountsService
:
@IsTest
private class AccountsServiceImpTest {
@IsTest
static void callingServiceWithIdsShouldCloseOpenCallReminders() {
}
@IsTest
static void callingServiceWithInterfacesShouldCloseOpenCallReminders() {
}
@IsTest
static void callingServiceWithUnitOfWorkShouldCloseOpenCallReminders() {
}
}
It's not a lot code that is duplicated but I don't like the fact that it's duplicated. Code static analysis will scream when we push this via quality gates
Thanks for any insight here!
@wimvelzeboer Big thanks for this helpful resource. Just have a few questions:
Domain Layer 4.1 How would you handle selectors with multiple conditions. Iterate twice with two or methods or put the logic in one method? 4.2 Why do your selectors return classes? Any other reason than chaining? 4.3 returning classes becomes a problem when you have extending classes that use common selector logic in the parent class In this code I have a selector (selectContractsWithManualRent) that is common logic between all extending classes and returned an instance of the parent class. There are two options I think:
public override void calculateRent() { selectContractsWithManualRent().setAmountManual(); }
public IContracts1 setAmountManual() { //calculate amount } }
public virtual inherited sharing class Contracts extends fflib_SObjects implements IContracts {
public IContracts selectContractsWithManualRent() { List contractsWithManualRent = new List();
for (Contractc contract : getContracts()) {
if (contract.ManualMonthlyRentc != null) {
contractsWithManualRent.add(contract);
}
}
return newInstance(contractsWithManualRent);
}
}
Sorry for the many questions, but Im confident I can implent apex commons if those are cleared up. If you dont have time to answer no worries!