Appliquer les principes de la couche Domaine dans Apex
Objectifs de formation
Une fois cette unité terminée, vous pourrez :
- Créer une classe Domaine Apex
- Intégrer votre code de retour aux valeurs par défaut et votre code de validation dans une classe Domaine
- Définir des méthodes de la classe Domaine sur les événements de déclencheur Apex
- Contrôler la mise en œuvre de l’application de la sécurité lors de l’exécution
Vidéo de démonstration Trail Together
Vous souhaitez être guidé pas à pas par un expert pendant que vous travaillez sur cette étape ? Regardez cette vidéo qui fait partie de la série Trail Together.
(Ce clip commence à 17 min 30 s, au cas où vous voudriez revenir en arrière et regarder à nouveau le début de l’étape.)
Code de référence
Ce module fait référence aux classes Apex suivantes du projet Exemple de code commun Apex FFLIB. Nous vous conseillons de les ouvrir avant de commencer.
Création de classes Domaine
La classe fflib_SObjectDomain utilisée dans le code de déclencheur dans l’unité précédente étend une classe de base prenant en charge la fonctionnalité de gestionnaire de déclencheur et fournit des fonctionnalités utiles, telles que la sécurité des objets.
La classe de base emploie le modèle de méthode pour fournir des points d’ancrage standard permettant d’implémenter une logique de domaine commune pour la validation des enregistrements via la méthode onValidate()
et le retour aux valeurs de champ par défaut via la méthode onApplyDefaults()
.
Il existe également des méthodes permettant d’intégrer la logique relative à des événements de déclencheur Apex spécifiques. Enfin, le constructeur (pour lequel toutes les classes qui ont étendu la classe fflib_SObjectDomain
doivent également procéder à une exposition) établit une liste des sObjects conformément à l’objectif de conception du traitement en masse décrit dans l’unité précédente.
Voici une implémentation simple de la classe de domaine Opportunities.
public class Opportunities extends fflib_SObjectDomain {
public Opportunities(List<Opportunity> sObjectList) {
super(sObjectList);
}
public class Constructor implements fflib_SObjectDomain.IConstructable {
public fflib_SObjectDomain construct(List<SObject> sObjectList) {
return new Opportunities(sObjectList);
}
}
}
Vous remarquerez que la classe interne de constructeur autorise la méthode de classe de base fflib_SObjectDomain.triggerHandler
(utilisée dans l’exemple de déclencheur Apex présenté dans l’unité précédente) à créer une nouvelle instance de la logique de gestionnaire de déclencheur relative à une classe de domaine. Elle transmet également la liste sObject, qui est généralement Trigger.new
.
Implémentation de la logique de retour des champs aux valeurs par défaut
Pour fournir un emplacement à la logique de retour des champs aux valeurs par défaut, la classe de base fflib_SObjectDomain
expose la méthode onApplyDefaults()
. Cette méthode est appelée depuis la méthode handleBeforeInsert()
dans la classe de base fflib_SObjectDomain
lors d’une invocation de déclencheur.
En plaçant la logique ici, vous garantissez que le retour aux valeurs par défaut est systématiquement appliqué dans toute l’application lors de l’ajout d’enregistrements. Si nécessaire, vous pouvez également l’appeler explicitement à partir d’un service permettant de présenter les valeurs d’enregistrement par défaut à un utilisateur, lorsque celui-ci accède par exemple à une interface utilisateur personnalisée via une page Visualforce ou un composant Lightning.
La classe de base expose à toutes les méthodes, via la propriété records
, la liste sObject fournie lors de l’appel du constructeur. Bien que nous ne soyons pas exactement dans un scénario de déclenchement ici, nous vous recommandons fortement d’envisager un traitement en masse, conformément aux objectifs de conception de domaine présentés dans l’unité précédente.
public override void onApplyDefaults() {
// Apply defaults to Opportunities
for(Opportunity opportunity :(List<Opportunity>) Records) {
if(opportunity.DiscountType__c == null) {
opportunity.DiscountType__c = OpportunitySettings__c.getInstance().DiscountType__c;
}
}
}
Implémentation de la logique de validation
Bien que vous puissiez remplacer toutes les méthodes de déclencheur ci-dessus pour implémenter la logique de validation, il est recommandé de ne le faire que pendant la phase postérieure à l’invocation du déclencheur Apex. En remplaçant l’une des deux méthodes onValidate()
de la classe de base fflib_SObjectDomain
, vous pouvez implémenter cette logique dans un endroit clairement défini.
public override void onValidate() {
// Validate Opportunities
for(Opportunity opp :(List<Opportunity>) this.records) {
if(opp.Type.startsWith('Existing') && opp.AccountId == null) {
opp.AccountId.addError('You must provide an Account when ' +
'creating Opportunities for existing Customers.');
}
}
}
La méthode onValidate()
ci-dessus est appelée à partir de la classe de base lorsque des enregistrements sont insérés dans l’objet. Si vous avez besoin d’une logique de validation sensible à la modification des données lors des mises à jour d’enregistrements, vous pouvez remplacer la variante suivante.
public override void onValidate(Map<Id,SObject> existingRecords) {
// Validate changes to Opportunities
for(Opportunity opp :(List<Opportunity>) Records) {
Opportunity existingOpp = (Opportunity) existingRecords.get(opp.Id);
if(opp.Type != existingOpp.Type) {
opp.Type.addError('You cannot change the Opportunity type once it has been created');
}
}
}
Vous remarquerez que le code des méthodes de classe de base handleAfterInsert()
et handleAfterUpdate()
garantit l’application de cette bonne pratique de sécurité en appelant cette méthode uniquement pendant la phase postérieure du déclencheur Apex (une fois que tous les déclencheurs Apex de cet objet se sont exécutés). Ce comportement est particulièrement important pour les développeurs de packages AppExchange (voir la section Ressources).
Implémentation de la logique de déclenchement Apex
La logique de domaine que vous implémentez ne correspond pas nécessairement aux méthodes ci-dessus. En effet, d’après les consignes relatives à la séparation des préoccupations, il n’est pas strictement obligatoire d’implémenter la logique de retour aux valeurs par défaut ou de validation dans l’ensemble de ces méthodes. Il s’agit là uniquement d’une possibilité. Si vous préférez, vous pourriez la placer entièrement dans les méthodes ci-dessous.
Pour implémenter du code associé à un déclencheur Apex qui invoque des comportements via d’autres objets de domaine, voici un exemple (quelque peu artificiel, certes) du remplacement de la méthode onAfterInsert()
pour mettre à jour le champ Description sur les comptes associés à chaque fois que de nouvelles opportunités sont insérées.
public override void onAfterInsert() {
// Related Accounts
List<Id> accountIds = new List<Id>();
for(Opportunity opp :(List<Opportunity>) Records) {
if(opp.AccountId!=null) {
accountIds.add(opp.AccountId);
}
}
// Update last Opportunity activity on related Accounts via the Accounts domain class
fflib_SObjectUnitOfWork uow =
new fflib_SObjectUnitOfWork(
new Schema.SObjectType[] { Account.SObjectType });
Accounts accounts = new Accounts([select Id from Account
where id in :accountIds]);
accounts.updateOpportunityActivity(uow);
uow.commitWork();
}
Voici quelques points à noter à propos de cet exemple :
- Une instance de la classe Domaine nommée Accounts est initialisée à l’aide d’une requête SOQL en ligne. L’unité suivante de ce module présente un modèle qui aide à encapsuler la logique de requête pour assurer une meilleure réutilisation et une meilleure cohérence autour des données résultantes, un aspect important pour la logique de classe Domaine.
- L’instance
fflib_SObjectUnitOfWork
est utilisée dans le contexte des déclencheurs Apex plutôt que dans un contexte de service, conformément au module de séparation des préoccupations. Dans ce cas, elle porte sur un événement ou une méthode de déclenchement. Ces méthodes sont directement appelées par la plate-forme et non par la couche de service. Ainsi, une unité de travail est créée et attribuée à la méthode Accounts afin qu’elle enregistre les mises à jour des enregistrements Account. Bien que cela ne soit pas présenté ici, il est généralement judicieux d’initialiser l’unité de travail dans un seul endroit afin d’éviter les doublons. - Ici, il est pertinent de procéder à une délégation à la classe de domaine Accounts car la mise à jour de l’activité fondée sur l’objet Accounts est davantage un comportement appartenant à l’objet Account qu’à Opportunity. Ce type de séparation des préoccupations entre les classes Domaine est également illustré dans la section suivante.
Pour référence, voici la méthode Accounts.updateOpportunityActivity
.
public class Accounts extends fflib_SObjectDomain {
public Accounts(List<Account> sObjectList) {
super(sObjectList);
}
public void updateOpportunityActivity(fflib_SObjectUnitOfWork uow) {
for(Account account :(List<Account>) Records) {
account.Description = 'Last Opportunity Raised ' + System.today();
uow.registerDirty(account);
}
}
}
Implémentation d’une logique personnalisée
Vous n’êtes pas obligé(e) d’implémenter exclusivement des méthodes qui peuvent être remplacées à partir de la classe de base. Rappelez-vous de la couche Service révisée présentée dans l’unité précédente.
public static void applyDiscounts(Set<Id> opportunityIds, Decimal discountPercentage) {
// Unit of Work
// ... // Validate parameters
// ... // Construct Opportunities domain class
Opportunities opportunities = new Opportunities( ...);
// Apply discount via domain class behavior
opportunities.applyDiscount(discountPercentage, uow);
// Commit updates to opportunities
uow.commitWork();
}
Ce code s’appuie sur une méthode de classe Domaine pouvant appliquer une remise à une opportunité, ce qui encapsule davantage cette logique dans un emplacement associé à l’objet Domaine.
Lorsque cela est nécessaire, le code délègue l’application des remises au niveau des lignes à la classe de domaine OpportunityLineItems
. Pour appuyer notre propos, supposons que la logique diffère pour les opportunités qui exploitent des lignes de produits.
vous pouvez consulter le code de la classe de domaine OpportunityLineItems ici.
L’instance fflib_ISObjectUnitOfWork
est prise comme argument afin que l’appelant (dans ce cas précis, la méthode OpportunitiesService.applyDiscount
) puisse la transmettre au code de domaine pour enregistrer le travail sur celui-ci, puis le transmettre à la méthode applyDiscount()
de la classe de domaine OpportunityLineItems
.
Logique métier dans la classe Domaine et dans la classe Service
Parfois, placer du code peut s’avérer tout sauf évident. Revenons au principe de séparation des préoccupations et intéressons-nous à celles relatives aux couches Service et Domaine.
Type de préoccupation d’application | Service ou Domaine | Exemple |
---|---|---|
S’assurer que les champs sont validés et que les valeurs par défaut leur sont appliquées de manière cohérente lors de la manipulation des données d’enregistrement. |
Domaine | Appliquer aux produits la politique de remise par défaut au fur et à mesure de leur ajout. |
Répondre à une action utilisateur ou système impliquant de rassembler plusieurs informations ou de mettre à jour plusieurs objets. Cela consiste principalement à fournir à un groupe d’enregistrements les actions pouvant survenir et à coordonner tout ce qui est nécessaire pour mener à bien cette action (avec éventuellement d’autres services de support). |
Service | Créer et calculer des factures à partir d’ordres d’exécution. Susceptible de récupérer des informations du catalogue. |
Gérer les modifications apportées aux enregistrements de l’application dans le cadre d’autres modifications d’enregistrement connexes ou lors de l’exécution d’une action utilisateur ou système. Cela consiste par exemple à assurer le retour aux valeurs par défaut lorsque cela est nécessaire. Si la modification d’un champ en affecte un autre, ce dernier est également mis à jour. |
Domaine | La manière dont réagit un objet Account lors de la création d’une opportunité ou la manière dont une remise est appliquée lors de l’exécution du processus de remise pour une opportunité. Remarque : ce type de logique peut débuter dans la couche Service, mais la gestion de la taille et de la complexité de la méthode de service ou l’amélioration de sa réutilisation peut être réalisée plus efficacement dans la couche Domaine. |
Traiter un comportement commun qui s’applique à un certain nombre d’objets différents. |
Domaine | Calcul du prix sur les lignes de produit d’un produit d’opportunité ou d’un ordre d’exécution Remarque : vous pouvez placer cela dans une classe de base Domaine partagée, en remplaçant la méthode fflib_SObjectDomain afin de l’intégrer aux événements du déclencheur Apex, les classes Domaine concrètes étendant à leur tour cette classe avec leurs comportements. |
Contrôle de l’application de la sécurité
Par défaut, la classe de base fflib_SObjectDomain
applique la sécurité CRUD des objets Salesforce. Cependant, celle-ci est appelée pour tous les types d’accès à l’objet, qu’il s’effectue via un contrôleur ou un service. La logique de service peut tenter d’accéder à un objet pour le compte de l’utilisateur sans exiger d’autorisations sur l’objet.
Si vous préférez désactiver ce comportement par défaut et l’appliquer vous-même dans votre code de service, vous pouvez utiliser une des fonctionnalités de configuration de la classe de base. L’exemple suivant montre comment procéder dans chaque constructeur. Vous pouvez également créer votre propre classe de base à l’aide de ce code, puis l’étendre à toutes les classes de domaine.
public Opportunities(List<Opportunity> sObjectList) {
super(sObjectList);
// Disable default Object Security checking
Configuration.disableTriggerCRUDSecurity();
}
Test des classes Domaine
La factorisation de la logique en blocs plus petits et plus encapsulés est avantageuse dans le cadre du développement piloté par les tests (TDD), car vous pouvez plus facilement intégrer les classes de domaine dans vos tests et invoquer directement les méthodes. Cela ne signifie pas que vous ne devez pas tester votre couche de service, mais cela vous permet d’adopter une approche de test et de développement plus progressive.
Préparation au défi pratique
Pour relever ce défi, lancez le Trailhead Playground que vous avez utilisé dans le module Apex Enterprise Patterns : Couche Service. Vous aurez besoin des bibliothèques open source que vous avez déjà installées. Si vous utilisez un autre Trailhead Playground, lancez-le et installez d’abord la bibliothèque ApexMocks, puis la bibliothèque Apex Commons à l’aide des boutons Déployer vers Salesforce ci-dessous. Vous pouvez en apprendre plus sur ces bibliothèques et leurs contrats de licence open source respectifs dans leurs dépôts.
Déployer la bibliothèque open source ApexMocks.
Déployer la bibliothèque open source Apex Common.
Ressources
- GitHub : Apex Enterprise Patterns
- Wikipedia : Séparation des préoccupations
- Publication sur le blog : Placement du code de validation dans un déclencheur Apex
- Publication sur le blog : Tests unitaires, Apex Enterprise Patterns et Apex Mocks - Partie 1
- Publication sur le blog : Tests unitaires, Apex Enterprise Patterns et Apex Mocks - Partie 2
- Publication sur le blog : Martin Fowler : catalogue de modèles d’architecture d’applications d’entreprise