-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Questions #1
Comments
Yes, iterating multiple times over records is not something to be afraid of, if it helps improving code reuse and readability.
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.
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.
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.
You would want to add some guard clauses at some point to prevent it from iterating over empty lists.
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.
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:
I would do something very similar with a few small changes:
|
@wimvelzeboer Just regarding 4.3
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:
or you can do something like:
|
@wimvelzeboer |
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 The question you should ask yourself at this point is, what input requires the method In the first case where it only requires the record Ids, the Trigger Handler method
Or you need more information from the source object then you would call the CasesService method first.
CasesService Implementation
Here you see a number of things happening: The basic rule here is, if business logic is cross domains then you usually start on the domain where the logic starts. I hope this helps you identifying where the service logic should go. |
@wimvelzeboer |
Hi @reignman5, To your question, if there are many things happening with the same condition, I would use a separate method. Something like:
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
|
@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.
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. 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. |
@wimvelzeboer |
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 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.
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. |
@wimvelzeboer In your doc you mock your controller methods and I cant find an example for triggerhandlers. Best regards |
@reignman5 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. 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 |
@wimvelzeboer
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. |
Yeah, you will use standard unit-test (that use mocking) on any other level than the 'onBeforeInsert'.
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.
yeah, well. That is indeed a problem the Salesforce Stub API cannot handle.
But I would only do that if there was a real high need for it. In normal life I would never do this. |
@wimvelzeboer
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
This is the unit test:
Problem is
But then we get a null exception on the guard clause. |
@wimvelzeboer Have a great weekend! |
@reignman5 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 But one problem we are facing is doing mass updates on our biggest custom object. |
@reignman5 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. |
@wimvelzeboer |
@reignman5 |
@wimvelzeboer
|
@reignman5 |
@wimvelzeboer Did you also do that for an update dml feature test? I dont really like my pattern. Example:
Really stressfull to find the right records for the changeDataMethods |
@wimvelzeboer |
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
Without overthinking the most straightforward solution is to create a getter on the domain let's say
then in the service we can do the following
I don't like the fact that the I would say that the remedy for the loop is to simply create a method that acts based on the parent Id
Here I also have doubts since the Any guidelines how to approach these kind of problems? Best, |
@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 write in my service class something like:
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: |
@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: 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
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. 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 ( Then you can use a Tasks domain method to return you the map
NOTE: The selector method 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. |
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 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 So far, we have tests that are firing based on the entry scenario to fully cover @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:
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?
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?
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?
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:
->newInstance(selectContractsWithManualRent().getrecords())
2.Have the selector instantiate the class dynamically(by RecordType) and then cast it in the child class:
->((IContracts)selectContractsWithManualRent()).setAmountManual();
What would you do?
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.
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!
The text was updated successfully, but these errors were encountered: