0

Using the Apex Enterprise Patterns, I would like to incorporate cross-SObject logic in my domain class. Is the following example a correct implementation?

Interaction__c records are created with references to Channel__c records. By default, certain fields of the Interaction__c record such as OwnerId are dependent on the referenced Channel__c record. Thus:

InteractionsTriggerHandler

public class InteractionsTriggerHandler extends fflib_SObjectDomain
{
    public InteractionsTriggerHandler(List<Interaction__c> sObjectList)
    {
        // Domain classes are initialised with lists to enforce bulkification throughout
        super(sObjectList);
    }

    public override void onApplyDefaults()
    {
        setChannelSpecificValues();
    }

    private void setChannelSpecificValues()
    {
        IInteractions interactions = Interactions.newInstance(this.Records);
        interactions.setChannelSpecificValues();
    }

Interactions domain class

public class Interactions extends fflib_SObjects 
    implements IInteractions
{
    // boilerplate initializers ommitted

    public void setChannelSpecificValues()
    {
        Set<Id> channelIds = getChannelIds();
        IChannels channels = Channels.newInstance(channelIds);
        channels.applyAttributesToInteractions( (List<Interaction__c>) getRecords() );
    }

    public Set<Id> getChannelIds()
    {
        Set<Id> result = new Set<Id>();
        for (Interaction__c record : (List<Interaction__c>) getRecords())
        {
            if (record.Interaction_Channel__c != null)
            {
                result.add(record.Interaction_Channel__c);
            }
        }
        return result;
    }
}

Channels domain class

public class Channels extends fflib_SObjects
    implements IChannels
{
    // boilerplate initializers ommitted

    public void applyAttributesToInteractions(List<Interaction__c> interactions) 
    {
        Map<Id, List<Interaction__c>> channelIdToInteractions = new Map<Id, List<Interaction__c>>();
        for (Interaction__c interaction : interactions)
        {
            if (interaction.Interaction_Channel__c == null) continue;

            if (!channelIdToInteractions.containsKey(interaction.Interaction_Channel__c))
                channelIdToInteractions.put(interaction.Interaction_Channel__c, new List<Interaction__c>{interaction});
            else
                channelIdToInteractions.get(interaction.Interaction_Channel__c).add(interaction);
        }

        for (Channel__c record : (List<Channel__c>) getRecords())
        {
            if (!channelIdToInteractions.containsKey(record.Id)) continue;

            for (Interaction__c interaction : channelIdToInteractions.get(record.Id))
            {
                interaction.OwnerId = record.OwnerId;
            }
        }
    }
}

Is it correct that although the interaction records are being inserted, we initialize a Channels domain class and have that handle the updates to the interaction record?

2
  • i'd avoid domain classes (Channels) calling other domain classes (Interactions) to set values - the Interactions domain is responsible for managing its data fields, especially in onApplyDefaults
    – cropredy
    Feb 28, 2023 at 19:54
  • thank you for the guidance. It helped me understand that I should encapsulate logic that alters the Interactions object within the Interactions.cls domain class itself. I would just fetch the Channel__c records I need through its selector and work through the logic from within a method in the Interactions domain class, and call that method on the TriggerHandler class. Mar 1, 2023 at 12:23

1 Answer 1

0

What goes in a domain class versus a service class is a subject of much debate but for your Interactions domain class, method onApplyDefaults you should query via the ChannelsSelector to get channels values and use Interactions-specific logic to default Interaction__c fields.

That is, Interactions is responsible for defaulting its own values, don't have Channels domain execute logic that sets Interactions' domain values

If you reference Chapter 6 of the Salesforce Platform Lightning Enterprise Architecture book by Andy Fawcett, you can see a couple of examples where service classes call the domain class to update data in a manner where the domain is responsible for its SObject field values (see RaceService.cls calling domain Contestants.cls to awardChampionshipPoints(). This approach generally requires passing a unitOfWork object

Note here, the stimulus event is originating with a Race completion and the domain classes are servants to this stimulus.

But in your OP, it is an Interaction that is being inserted, presumably from the UX so Interactions.cls needs to take sole responsibility for defaulting its values. Other domains and service classes could be used to assist Interactions.cls by wrapping selector-fetched objects that simplify a complex data model into something more easily digestible by Interactions.cls

2
  • Thanks again. I could definitely purchase that book and start reading up on it. I ended up with an approach very similar to what you described and it makes sense. Regarding your final point, I believe you describe something akin to OpportunityInfo.cls used by the OpportunitySelector.cls, and I shall keep that in mind. Mar 1, 2023 at 20:57
  • 1
    @parsa_047Fletcher - the book is seminal; get 3rd Edition (as of 2023-03-01); the chapter on Unit Testing and how apexMocks works with Enterprise Patterns was very helpful to me
    – cropredy
    Mar 1, 2023 at 22:52

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .