Anwenden der Prinzipien der Domain-Schicht in Apex
Lernziele
Nachdem Sie diese Lektion abgeschlossen haben, sind Sie in der Lage, die folgenden Aufgaben auszuführen:
- Erstellen einer Apex Domain-Klasse
- Reflexives Ausführen Ihres Standardwertaktivierungs- und Validierungscodes in einer Domain-Klasse
- Zuordnen von Methoden in der Domain-Klasse zu Apex-Auslöserereignissen
- Steuern der Anwendung von Sicherheitsmaßnahmen zur Laufzeit
Mit Trail Together einem Dozenten folgen
Möchten Sie bei diesem Schritt einem Experten folgen? Schauen Sie sich dieses Video an, das Teil der Reihe "Trail Together" ist.
(Dieser Clip beginnt bei Minute 17:30, für den Fall, dass Sie zurückspringen und den Anfang des Schritts noch einmal sehen möchten.)
Referenzierter Code
Dieses Modul referenziert die folgenden Apex-Klassen aus dem Projekt FFLIB Apex Common Samplecode. Diese sollten Sie öffnen, bevor Sie beginnen.
Erstellen von Domain-Klassen
Die im Auslösercode der vorherigen Einheit verwendete Klasse fflib_SObjectDomain erweitert eine Basisklasse durch die Unterstützung für Auslöser-Handler-Funktionen und bietet nützliche Funktionen wie etwa Objektsicherheit.
Die Basisklasse verwendet das Schablonenmethodenmuster, um Standard-Hooks für die Implementierung allgemeiner Domain-Logik für die Validierung von Datensätzen mithilfe der onValidate()
-Methode und die Standardwertaktivierung von Feldwerten mit der onApplyDefaults()
-Methode bereitzustellen.
Es gibt auch Methoden für die Platzierung von Logik in Bezug auf spezifische Apex-Auslöserereignisse. Der Konstruktor (für den alle Klassen, mit denen die fflib_SObjectDomain
-Klasse erweitert wurde, verfügbar gemacht werden müssen,) akzeptiert eine Liste von sObjects, um das in der vorherigen Einheit beschriebene Designziel der Massenverarbeitung zu unterstützen.
Dies ist eine grundlegende Implementierung der Domain-Klasse "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);
}
}
}
Mit der inneren Klasse "Constructor" kann die Basisklassenmethode fflib_SObjectDomain.triggerHandler
, die im Apex-Auslöserbeispiel aus der vorherigen Einheit behandelt wurde, eine neue Instanz der Auslöser-Handler-Logik für eine Domain-Klasse erstellen und an die sObject-Liste übergeben (meist Trigger.new
).
Implementieren von Logik zur Aktivierung von Feldstandardwerten
Zur Platzierung von Logik zur Aktivierung von Feldstandardwerten stellt die Basisklasse fflib_SObjectDomain
die onApplyDefaults()
-Methode zur Verfügung. Diese Methode wird während eines Auslöseraufrufs von der handleBeforeInsert()
-Methode in der Basisklasse fflib_SObjectDomain
aufgerufen.
Wenn Sie Logik hier platzieren, stellen Sie sicher, dass die Standardwertaktivierung innerhalb der Anwendung einheitlich erfolgt, wenn Datensätze hinzugefügt werden. Sie können sie bei Bedarf auch explizit in einem Service aufrufen, der dazu beiträgt, Standarddatensatzwerte für einen Benutzer anzuzeigen, der beispielsweise über eine Visualforce-Seite oder Lightning-Komponente auf eine benutzerdefinierte Oberfläche zugreift.
Die Basisklasse macht die sObject-Liste verfügbar, die während des Konstruktoraufrufs über die Eigenschaft records
für alle Methoden zur Verfügung gestellt wird. Obwohl dies streng genommen kein Auslöser-Szenario ist, empfehlen wird trotzdem wärmstens, die Massenverarbeitung in Betracht zu ziehen, um die Domain-Designziele aus der vorherigen Einheit umzusetzen.
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;
}
}
}
Implementieren von Validierungslogik
Sie können die oben angegebenen Auslösermethoden zwar überschreiben, um Validierungslogik zu implementieren, die bewährten Vorgehensweisen sehen jedoch vor, dies erst in der After-Phase des Apex-Auslöseraufrufs zu tun. Durch Überschreiben einer der beiden onValidate()
-Methoden der Basisklasse fflib_SObjectDomain
können Sie diese Logik an einer klar definierten Position implementieren.
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.');
}
}
}
Die oben angegebene onValidate()
-Methode wird von der Basisklasse aus aufgerufen, wenn Datensätze im Objekt eingefügt werden. Wenn Sie Validierungslogik benötigen, die Datenänderungen während Datensatzaktualisierungen erfasst, können Sie die folgende Variante überschreiben.
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');
}
}
}
Beachten Sie, dass Code in den Basisklassenmethoden handleAfterInsert()
und handleAfterUpdate()
sicherstellt, dass bewährte Vorgehensweisen in puncto Sicherheit umgesetzt werden, indem diese Methode erst nach dem "After"-Teil des Apex-Auslösers aufgerufen wird (nachdem alle Apex-Auslöser für dieses Objekt abgeschlossen wurden). Dieses Verhalten ist für Entwickler von AppExchange-Paketen sehr wichtig (siehe Abschnitt "Ressourcen").
Implementieren von Apex-Auslöserlogik
Es ist nicht immer der Fall, dass sich die Domänenlogik, die Sie implementieren, den oben genannten Methoden zuordnen lässt. Tatsächlich ist es laut den Separation of Concerns-Richtlinien keine strikte Anforderung, die gesamte Standardwertaktivierungs- oder Validierungslogik in diesen Methoden zu implementieren. Das ist nur ein Vorschlag. Wenn es Ihnen lieber ist, können Sie auch die gesamte Logik in den nachfolgenden Methoden platzieren.
Was die Implementierung von Code im Zusammenhang mit Apex-Auslösern angeht, der Verhaltensweisen über andere Domain-Objekte aufruft, so folgt nun ein etwas konstruiertes Beispiel für das Überschreiben der onAfterInsert()
-Methode, um das Feld "Beschreibung" zugehöriger Accounts zu aktualisieren, sobald neue Opportunities eingefügt werden.
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();
}
Anmerkungen zum obigen Beispiel:
- Eine Instanz der Domain-Klasse "Accounts" wird unter Verwendung einer Inline-SOQL-Abfrage initialisiert. In der nächsten Einheit dieses Moduls wird ein Muster vorgestellt, das die Wiederverwendung und Konsistenz der Ergebnisdaten durch Kapsel der Abfragelogik verbessert, was wichtig für die Domain-Klassenlogik ist.
- Die
fflib_SObjectUnitOfWork
-Instanz wird in einem Apex-Auslöserkontext verwendet und nicht in einem Service-Kontext, wie im SOC-Modul beschrieben wird. In diesem Fall ist ihr Umfang ein Auslöserereignis oder eine Methode. Diese Methoden werden direkt von der Plattform aufgerufen, nicht von der Service-Schicht. Daher wird eine Unit of Work erstellt und an die Accounts-Methode übergeben, damit sie Änderungen an den Account-Datensätzen erfasst. Es hat sich bewährt, die Unit of Work immer an einer Position zu initialisieren, um doppelte Initialisierungen zu vermeiden (dies wird hier allerdings nicht demonstriert). - Die Delegation an die Domain-Klasse Accounts ist hier sinnvoll, da Aktualisierungen auf der Grundlage von Accounts eher ein Verhalten des Objekts "Account" als des Objekts "Opportunity" sind. Diese Art von SOC zwischen Domain-Klassen wird auch im nächsten Abschnitt noch verdeutlicht.
Hier ist die Accounts.updateOpportunityActivity
-Methode zur Bezugnahme.
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);
}
}
}
Implementieren von benutzerdefinierter Logik
Sie sind nicht auf die Implementierung von Methoden beschränkt, die von der Basisklasse aus überschrieben werden können. Erinnern Sie sich an die veränderte Service-Schicht aus der vorherigen Einheit.
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();
}
Dieser Code verwendet eine Domain-Klassenmethode, die einen Rabatt auf eine Opportunity anwenden kann, wodurch diese Logik weiter an einer Position gekapselt wird, die mit dem Domain-Objekt verknüpft ist.
Falls erforderlich, delegiert der Code an die Domain-Klasse OpportunityLineItems
, um Rabatte auf Belegpostenebene anzuwenden. Nehmen wir zum Zweck dieser Erläuterung an, dass die Logik für Opportunities, die Produktbelegposten nutzen, unterschiedlich ist.
Sie können sich den Code für die Domain-Klasse OpportunityLineItems hier ansehen.
Die fflib_SObjectUnitOfWork
-Instanz wird als Argument angegeben, damit der Aufrufer (in diesem Fall die OpportunitiesService.applyDiscount
-Methode) sie übergeben kann, damit der Domain-Code Arbeit dafür erfassen kann, und das Argument anschließend an die applyDiscount()-Methode
der Domain-Klasse OpportunityLineItems
übergeben werden kann.
Geschäftslogik in der Domain-Klasse im Vergleich zur Service-Klasse
Manchmal ist es eventuell nicht unbedingt eindeutig, wo Code platziert werden sollte. Kehren wir zurück zum SOC-Prinzip und sehen wir uns an, um welche Anforderungen es bei den Service- und Domain-Schichten geht.
Art der Anwendungsanforderung | Service oder Domain | Beispiel |
---|---|---|
Sicherstellen, dass die Validierung und Standardwertaktivierung von Feldern einheitlich abläuft, wenn Datensatzdaten geändert werden |
Domain | Anwenden von Standard-Rabattrichtlinie auf Produkte, die hinzugefügt werden |
Reagieren auf eine Benutzer- oder Systemaktion, bei der verschiedene Informationen zusammengetragen oder mehrere Objekte aktualisiert werden müssen Stellt hauptsächlich Aktionen zur Verfügung, die für eine Datensatzsammlung auftreten können, und koordiniert alle für die Durchführung dieser Aktion notwendigen Vorgänge (eventuell mit anderen, unterstützenden Services) |
Service | Erstellen und Berechnen von Rechnungen aus Arbeitsaufträgen. Eventuell müssen Informationen aus Preisbüchern extrahiert werden. |
Verarbeiten von Datensatzänderungen, die in der Anwendung auftreten, da sich andere, zugehörige Datensätze ändern oder eine Benutzer- bzw. Systemaktion ausgeführt wird. Beispiel: Aktivieren der Standardwerte für Werte, falls nötig. Wirkt sich die Änderung eines Felds auf ein anderes aus, wird auch dieses aktualisiert. |
Domain | Art und Weise, auf die das Account-Objekt reagiert, wenn eine Opportunity erstellt wird, oder, wie der Rabatt angewandt wird, wenn der Opportunity-Rabattprozess ausgeführt wird. Hinweis: Diese Art von Logik wird möglicherweise in der Service-Schicht gestartet, sollte jedoch besser in der Domain-Schicht bedient werden, um die Größe und Komplexität der Service-Methode handeln zu können bzw. ihre Wiederverwendung zu erleichtern. |
Umgang mit allgemeinem Verhalten, das über eine Reihe unterschiedlicher Objekte hinweg gilt |
Domain | Berechnen des Preises für das Opportunity-Produkt oder Produktbelegposten in Arbeitsaufträgen. Hinweis: Sie könnten dies in einer gemeinsamen Domain-Basisklasse platzieren und die fflib_SObjectDomain -Methode überschreiben, um die Apex-Auslöserereignisse zu verknüpfen, wobei konkrete Domain-Klassen wiederum diese Klasse mit ihrem Verhalten erweitern. |
Steuern der Durchsetzung von Sicherheitsmaßnahmen
Standardmäßig erzwingt die Basisklasse fflib_SObjectDomain
CRUD-Sicherheit für Salesforce-Objekte. Sie wird jedoch für alle möglichen Arten von Zugriff auf das Objekt aufgerufen, ob über ein Steuerfeld oder einen Service. Service-Logik versucht eventuell, im Namen des Benutzers auf ein Objekt zuzugreifen, ohne dass dafür Berechtigungen für das Objekt erforderlich sind.
Wenn Sie dieses Standardverhalten lieber deaktivieren und selbst in Ihrem Service-Code umsetzen möchten, können Sie eine Konfigurationsfunktion der Basisklasse verwenden. Das folgende Beispiel zeigt, wie Sie dies in jedem Konstruktor realisieren. Eine andere Möglichkeit besteht darin, eine eigene Basisklasse mit diesem Code zu erstellen und diese Klasse dann für alle Domain-Klassen zu erweitern.
public Opportunities(List<Opportunity> sObjectList) {
super(sObjectList);
// Disable default Object Security checking
Configuration.disableTriggerCRUDSecurity();
}
Testen von Domain-Klassen
Das Faktorisieren von Logik in kleinere, stärker gekapselte Blöcke ist vorteilhaft für die testgetriebene Entwicklung (Test-driven Development, TDD), da Sie die Domain-Klassen in Ihren Tests einfacher erstellen und die Methoden direkt aufrufen können. Das soll nicht heißen, dass Sie Ihre Service-Schicht nicht testen sollen, es ermöglicht Ihnen jedoch einen eher inkrementellen Test- und Entwicklungsansatz.
Vorbereiten auf die praktische Aufgabe
Zum Absolvieren dieser praktischen Aufgabe starten Sie den Trailhead Playground, den Sie im Modul Apex Enterprise Patterns: Service-Schicht verwendet haben. Sie benötigen nämlich die Open Source-Bibliotheken, die Sie dort bereits installiert haben. Falls Sie einen anderen Trailhead Playground verwenden, starten Sie diesen und installieren zuerst die ApexMocks-Bibliothek und dann die Apex Common-Bibliothek mithilfe der nachfolgend angezeigten Schaltflächen In Salesforce bereitstellen. Weitere Informationen über die beiden Bibliotheken und die zugehörigen Open Source-Lizenzvereinbarungen finden Sie in ihren Repos.
Stellen Sie die Open Source-Bibliothek ApexMocks bereit.
Stellen Sie die Open Source-Bibliothek Apex Common bereit.
Ressourcen
- GitHub: Apex Enterprise Patterns
- Wikipedia: Separation of Concerns
- Blogpost: Position für die Platzierung von Validierungscode in einem Apex-Auslöser
- Blogpost: Einheitentest, Apex Enterprise-Muster und Apex-Simulationen – Teil 1
- Blogpost: Einheitentest, Apex Enterprise-Muster und Apex-Simulationen – Teil 2
- Blogpost: Martin Fowler: Katalog der Muster in der Enterprise Application Architecture