Aplicar principios de la Capa de dominios en Apex
Objetivos de aprendizaje
Después de completar esta unidad, podrá:
- Crear una clase de Apex de dominio.
- Reflejar sus valores predeterminados y su código de validación en una clase de dominio.
- Asignar métodos de la clase de dominio a eventos de desencadenadores de Apex.
- Controlar la aplicación de seguridad en el tiempo de ejecución.
Siga el proceso con Trail Together
¿Desea seguir el proceso con un experto a medida que realiza este paso? Mire este video que forma parte de la serie Trail Together.
(Este video comienza en el minuto 17:30, en caso de que desee rebobinar y mirar el comienzo del paso nuevamente).
Código de referencia
Este módulo hace referencia a estas clases de Apex del proyecto Código de muestra de FFLIB Apex Common. Es conveniente que las abra antes de empezar.
Creación de clases de dominio
La clase fflib_SObjectDomain que se usa en el código del desencadenador en la unidad anterior amplía una clase base que admite la función de controlador de desencadenador y ofrece funciones útiles, como la seguridad de objetos.
La clase base usa el patrón de método de la plantilla para proporcionar enlaces estándares con el fin de implementar una lógica de dominio común para la validación de registros mediante el método onValidate()
y valores de campos predeterminados mediante el método onApplyDefaults()
.
Además, hay métodos para colocar lógica en relación con eventos específicos de desencadenadores de Apex. Por último, el constructor (para el que todas las clases que ampliaron la clase fflib_SObjectDomain
también deben exponer) toma una lista de sObjects de acuerdo con el objetivo de diseño de masificación descrito en la unidad anterior.
Esta es una implementación básica de la clase de dominio Oportunidades.
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);
}
}
}
Tenga en cuenta que la clase interna del constructor permite que el método de clase base SObjectDomain.triggerHandler
, usado en la muestra de desencadenador de Apex que se mostró en la unidad anterior, cree una nueva instancia de lógica del controlador de desencadenador para una clase de dominio. Además, pasa la lista de sObjects, generalmente Trigger.new
.
Implementación de lógica de valores predeterminados de campos
Para ofrecer un lugar para la lógica de valores predeterminados de campos, la clase base fflib_SObjectDomain
expone el método onApplyDefaults()
. Se llama a este método desde el método handleBeforeInsert()
en la clase base fflib_SObjectDomain
durante una invocación del desencadenador.
Al colocar lógica aquí se garantiza que los valores predeterminados sean coherentes en toda la aplicación cuando se agreguen registros. También puede llamarla explícitamente, si es necesario, desde un servicio que ayude a presentar valores de registros predeterminados a un usuario que acceda a una interfaz de usuario personalizada a través de una página de Visualforce o un componente Lightning, por ejemplo.
La clase base expone la lista de sObjects proporcionada durante la llamada del constructor a todos los métodos mediante la propiedad records
(registros). Aunque rigurosamente esta no es una situación hipotética de desencadenador, se sigue recomendando encarecidamente la masificación, de acuerdo con los objetivos de diseño de dominios de la unidad anterior.
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;
}
}
}
Implementación de lógica de validación
Aunque puede reemplazar cualquiera de los métodos de desencadenador anteriores para implementar la lógica de validación, la mejor práctica es hacerlo únicamente en la fase posterior de la invocación del desencadenador de Apex. Al reemplazar uno de los dos métodos onValidate()
de la clase base fflib_SObjectDomain
, puede implementar esta lógica en un lugar claramente definido.
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.');
}
}
}
Se llama al método onValidate()
anterior desde la clase base cuando se insertan registros en el objeto. Si requiere lógica de validación que distinga el cambio de datos durante actualizaciones de registros, puede sustituir la variante siguiente.
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');
}
}
}
Tenga en cuenta que el código de los métodos de clase base handleAfterInsert()
y handleAfterUpdate()
garantiza que se aplique la mejor práctica de seguridad al llamar a este método únicamente durante la fase posterior del desencadenador de Apex (después de que todos los desencadenadores de Apex de este objeto se completen). Este comportamiento es extremadamente importante para los desarrolladores de paquetes de AppExchange (consulte la sección Recursos).
Implementación de lógica de desencadenadores de Apex
La lógica de dominio que implemente no siempre se incluirá en alguno de los métodos anteriores. De hecho, la implementación de toda la lógica de valores predeterminados o de validación en estos métodos no es un requisito estricto, de acuerdo con las directrices de la separación de intereses. Se trata de una mera consideración. Si lo prefiere, puede colocarlo todo en los métodos que se muestran a continuación.
Para implementar código relacionado con los desencadenadores de Apex que invoque comportamientos mediante otros objetos de dominio, a continuación se muestra un ejemplo ligeramente forzado del reemplazo del método onAfterInsert()
con el fin de actualizar el campo Descripción en Cuentas relacionadas cada vez que se insertan nuevas Oportunidades.
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();
}
Hay que comentar varios aspectos del ejemplo anterior.
- Una instancia de la clase de dominio Cuentas se inicializa mediante el uso de una consulta SOQL en línea. La siguiente unidad de este módulo presenta un patrón que ayuda a encapsular lógica de consulta para mejorar su reutilización y su coherencia en los datos resultantes, lo cual es importante para la lógica de clase de dominio.
- La instancia
fflib_SObjectUnitOfWork
se usa más en un contexto de desencadenador de Apex que en un contexto de servicio de acuerdo con el módulo de SOC. En este caso, su ámbito es un método o evento de desencadenador. Estos métodos son llamados directamente por la plataforma, no la capa de servicios. Como tal, se crea una unidad de trabajo, la cual se proporciona al método Cuentas para que registre actualizaciones en los registros de Cuenta. Aunque no se muestra aquí, suele ser una mejor práctica contar con un único lugar para inicializar la unidad de trabajo con el fin de evitar la duplicación. - La delegación a la clase de dominio Cuentas es apropiada aquí dado que la actualización de actividad basándose en Cuentas es más un comportamiento del objeto Cuenta que de Oportunidad. Este tipo de SOC entre clases de dominio también se ilustrará más adelante en la sección siguiente.
Como referencia, aquí tiene el método 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);
}
}
}
Implementación de lógica personalizada
No está restringido a implementar únicamente métodos que puedan sustituirse desde la clase base. Recuerde la capa de servicios revisada que se mostró en la unidad anterior.
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();
}
Este código usa un método de clase de dominio que puede aplicar un descuento a una Oportunidad, y, así, encapsular más esta lógica en un lugar que está asociado al objeto Dominio.
Cuando es necesario, el código delega en la clase de dominio OpportunityLineItems
para aplicar descuentos de nivel de línea. Para el asunto que estamos tratando, supongamos que la lógica es diferente para las oportunidades que aprovechan líneas de productos.
Puede ver el código de la clase de dominio OpportunityLineItems aquí.
La instancia fflib_ISObjectUnitOfWork
se toma como argumento para que el emisor de la llamada (en este caso, el método OpportunitiesService.applyDiscount
) pueda pasarlo para que el código de dominio registre trabajo frente a él y, luego, se pasa al método applyDiscount()
de la clase de dominio OpportunityLineItems
.
Lógica de negocio en la clase de dominio frente a la clase de servicio
A veces no es tan obvio dónde se debe colocar el código. Volvamos al principio de SOC y pensemos en cuáles son las preocupaciones de las capas de servicios y dominios.
Tipo de preocupación de la aplicación | Servicio o dominio | Ejemplo |
---|---|---|
Asegurarse de que los campos se validan y tienen valores predeterminados de manera coherente cuando se manipulan datos de registros. |
Dominio | Aplicar una política de descuento predeterminada a productos a medida que se agregan. |
Responder a una acción del usuario o del sistema que implique reunir varias informaciones o actualizar varios objetos. Principalmente proporciona acciones que pueden producirse en un conjunto de registros y coordina todo lo necesario para completar dicha acción (posiblemente con otros servicios de asistencia). |
Servicio | Crear y calcular facturas a partir de órdenes de trabajo. Puede que incorpore información de la lista de precios. |
Manejar cambios en registros que se produzcan en la aplicación como parte del cambio de otros registros relacionados o mediante la ejecución de una acción del usuario o del sistema. Por ejemplo, aplicar los valores predeterminados necesarios. Si el cambio de un campo afecta a otro, también se actualiza. |
Dominio | El modo en el que el objeto Cuenta reacciona cuando se crea una Oportunidad o cómo se aplica el Descuento cuando se ejecuta el proceso de descuento de Oportunidad. Nota: Este tipo de lógica puede comenzar en la capa de servicios, pero se proporciona mejor en la capa de dominios para manejar el tamaño y la complejidad del método de servicio o mejorar la reutilización. |
Manejar un comportamiento común que se aplica en un número determinado de objetos diferentes. |
Dominio | Calcular el precio del producto de oportunidad o las líneas de productos de la orden de trabajo. Nota: Podría colocar esto en una clase base de dominio compartida, sustituyendo el método fflib_SObjectDomain para enlazar con los eventos de desencadenadores de Apex, con clases de dominio concretas que ampliaran esta clase a su vez con sus comportamientos. |
Control de la aplicación de seguridad
De manera predeterminada, la clase base fflib_SObjectDomain
aplica la seguridad CRUD de objeto de Salesforce. Sin embargo, se invoca para todo tipo de accesos al objeto, ya sea a través de un controlador o de un servicio. Puede que la lógica de servicio quiera acceder a un objeto en nombre del usuario sin requerir permisos al objeto.
Si prefiere desactivar este comportamiento predeterminado y aplicarlo por sí mismo en su código de servicio, puede usar una funcionalidad de configuración de la clase base. El siguiente ejemplo muestra cómo hacerlo en cada constructor. Otra forma es crear su propia clase base con este código y, a continuación, ampliar dicha clase para todas las clases de dominio.
public Opportunities(List<Opportunity> sObjectList) {
super(sObjectList);
// Disable default Object Security checking
Configuration.disableTriggerCRUDSecurity();
}
Prueba de clases de dominio
La factorización de la lógica en fragmentos más pequeños y más encapsulados es beneficiosa para el desarrollo basado en pruebas (test-driven development, TDD), dado que puede construir las clases de dominio en sus pruebas más fácilmente e invocar los métodos directamente. Esto no quiere decir que no deba probar su capa de servicios, sino que le permite aplicar un enfoque de prueba y desarrollo más incremental.
Preparación para el reto práctico
Para completar este reto, inicie el Trailhead Playground que usó en el módulo Patrones de negocio de Apex: Capa de servicios. Necesitará las bibliotecas de código abierto que ya instaló. Si está usando un Trailhead Playground diferente, inícielo e instale la biblioteca ApexMocks primero y, luego, la biblioteca Apex Commons ; para ello, use los botones Implementar en Salesforce que aparecen a continuación. Puede leer más acerca de ambas bibliotecas y sus acuerdos de licencia de código abierto respectivos en sus repositorios.
Implemente la biblioteca de código abierto ApexMocks.
Implemente la biblioteca de código abierto Apex Commons.
Recursos
- GitHub: Patrones de negocio de Apex
- Wikipedia: Separación de intereses
- Publicación de blog: Dónde colocar un código de validación en un desencadenador de Apex
- Publicación de blog: Prueba de unidades, simulaciones de patrones de negocio de Apex y Apex – Parte 1
- Publicación de blog: Prueba de unidades, simulaciones de patrones de negocio de Apex y Apex – Parte 2
- Publicación de blog: Martin Fowler: Catálogo de patrones de arquitectura de aplicaciones empresariales