Aplicar os princípios da camada de domínio no Apex
Objetivos de aprendizagem
Após concluir esta unidade, você estará apto a:
- Criar uma nova classe do Apex de domínio.
- Refletir seu código de predefinição e validação em uma classe de domínio.
- Mapear métodos na classe de domínio para os eventos de acionador do Apex.
- Controlar a aplicação de imposição de segurança no tempo de execução.
Acompanhar com o Trail Together
Deseja acompanhar um especialista enquanto trabalha nesta etapa? Veja este vídeo que faz parte da série Trail Together.
(Este clipe começa na marca dos 17:30 minutos, caso você queira retroceder e ver o início da etapa novamente.)
Código de referência
Este módulo faz referência a essas classes do Apex do projeto FFLIB Apex Common Samplecode. É melhor abri-las antes de começar.
Como criar classes de domínio
A classe fflib_SObjectDomain usada no código do disparador na unidade anterior estende uma classe base que oferece suporte à funcionalidade de manipulador de gatilho e fornece funcionalidades úteis, como segurança de objetos.
A classe base usa o padrão Template Method para fornecer ganchos padrão a fim de implementar lógica de domínio comum para a validação de registros por meio do método onValidate()
e padronização de valores de campo por meio do método onApplyDefaults()
.
Existem também métodos para determinar o local da lógica relativa a eventos do acionador do Apex específicos. Por fim, o construtor (para o qual todas as classes que estendem a classe fflib_SObjectDomain
também devem expor) usa uma lista de sObjects de acordo com a meta de design de massificação descrita na unidade anterior.
Veja uma implementação básica da classe de domínio 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);
}
}
}
Observe que a classe interna do construtor permite que o método da classe base fflib_SObjectDomain.triggerHandler
, usado no exemplo de acionador do Apex mostrado na unidade anterior, crie uma nova instância da classe de domínio transmitida na lista sObject, normalmente Trigger.new
.
Como implementar lógica de predefinição de campo
Para fornecer um local para a lógica de predefinição de campo, a classe base fflib_SObjectDomain
expõe o método onApplyDefaults()
. Esse método é chamado a partir do método handleBeforeInsert()
na classe base fflib_SObjectDomain
durante uma invocação de acionador.
Colocar a lógica aqui faz com que a predefinição ocorra consistentemente no aplicativo quando registros são adicionados. Você também pode chamá-la explicitamente, se necessário, de um serviço que ajuda a apresentar valores de registro padrão a um usuário que esteja acessando uma interface do usuário personalizada por uma página do Visualforce ou por um componente do Lightning, por exemplo.
A classe base expõe a lista de sObject fornecida durante a chamada do construtor para todos os métodos por meio da propriedade records
. Embora não estejamos estritamente em um cenário de acionador aqui, recomendamos pensar em massificação, de acordo com os objetivos de design de domínio da unidade 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;
}
}
}
Como implementar a lógica de validação
Embora você possa substituir qualquer um dos métodos de acionador acima para implementar a lógica de validação, é uma melhor prática fazer isso somente na fase posterior da invocação do acionador do Apex. Ao substituir um dos dois métodos onValidate()
da classe base fflib_SObjectDomain
, você poderá implementar essa lógica em um local 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.');
}
}
}
O método onValidate()
acima é chamado a partir da classe base quando registros são inseridos no objeto. Se você precisar de lógica de validação que seja sensível à mudança de dados durante as atualizações do registro, pode substituir a variante a seguir.
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');
}
}
}
Observe que o código nos métodos da classe base handleAfterInsert()
e handleAfterUpdate()
faz com que a melhor prática de segurança seja imposta chamando esse método somente durante a parte posterior do acionador do Apex (depois que todos os acionadores do Apex nesse objeto foram concluídos). Esse comportamento é mais importante para desenvolvedores de pacote AppExchange (consulte a seção Recursos).
Como implementar a lógica de acionador do Apex
Não é sempre que a lógica de domínio implementada se enquadra nos métodos acima. Na verdade, não é um requisito rígido implementar toda a lógica de predefinição ou validação nesses métodos, como estipulam as diretrizes de separação de preocupações. É apenas algo a se considerar. Se preferir, você poderia colocar tudo nos métodos abaixo.
Para implementar código relativo a acionadores do Apex que invoca comportamentos por meio de outros objetos de domínio, abaixo temos um exemplo ligeiramente forçado de substituição do método onAfterInsert()
para atualizar o campo Descrição em contas relacionadas sempre que novas oportunidades são inseridas.
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();
}
Algumas coisas devem ser observadas em relação ao exemplo acima.
- Uma instância da classe de domínio Accounts é iniciada usando uma consulta SOQL inline. A próxima unidade neste módulo apresenta um padrão que ajuda a encapsular lógica de consulta para reutilização e consistência melhores em relação aos dados resultantes, o que é importante para a lógica da classe de domínio.
- A instância
fflib_SObjectUnitOfWork
é usada em um contexto de acionador do Apex em vez de um contexto de serviço, de acordo com o módulo SOC. Nesse caso, seu escopo é um método ou evento de acionador. Esses métodos são chamados diretamente pela plataforma, não pela camada de serviço. Sendo assim, uma unidade de trabalho é criada e concedida ao método Accounts para que ele registre atualizações nos registros de conta. Embora não apareça aqui, é uma boa prática ter um local único para iniciar a unidade de trabalho a fim de evitar duplicação. - A delegação para a classe de domínio Accounts é apropriada porque a atualização da atividade com base nas contas é mais um comportamento do objeto de conta do que do objeto de oportunidade. Esse tipo de SOC entre classes de domínio também está ilustrada na próxima seção.
Para referência, veja o 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);
}
}
}
Como implementar lógica personalizada
Você não está restrito à implementação somente de métodos que possam ser substituídos na classe base. Lembre-se da camada de serviço revisada mostrada na unidade 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();
}
Esse código usa um método de classe de domínio que pode aplicar um desconto em uma oportunidade, encapsulando essa lógica ainda mais em um local associado com o objeto de domínio.
Quando necessário, o código delega para a classe de domínio OpportunityLineItems
a aplicação de descontos no nível da linha. Para fins de debate, assuma que a lógica é diferente para oportunidades que aproveitam linhas de produto.
você pode ver o código da classe de domínio OpportunityLineItems aqui.
A instância fflib_ISObjectUnitOfWork
é usada como um argumento, para que o chamador (neste caso, o método OpportunitiesService.applyDiscount
) possa transmiti-lo ao código de domínio para que este registre trabalho em relação a ele e, posteriormente, passe para o método applyDiscount()
da classe de domínio OpportunityLineItems
.
Lógica de negócios na classe de domínio vs. na classe de serviço
Às vezes pode parecer pouco óbvio onde o código deve entrar. Vamos voltar para o princípio de SOC e pensar sobre o que interessa às camadas de serviço e de domínio.
Tipo de preocupação de uso | Serviço ou domínio | Exemplo |
---|---|---|
Garantir que os campos são validados e predefinidos consistentemente à medida que os dados do registro são manipulados. |
Domínio | Aplicar política de desconto padrão aos produtos à medida que eles são adicionados. |
Responder a uma ação do sistema ou do usuário que envolva juntar várias informações ou atualizar muitos objetos. Basicamente, ele oferece ações que podem ocorrer em um conjunto de registros e coordena tudo o que é necessário para concluir a ação (possivelmente com outros serviços de suporte). |
Serviço | Criar e calcular faturas a partir de ordens de trabalho. Pode requisitar informações de catálogo de preços. |
Tratar de alterações em registros que ocorrem no aplicativo como parte de alterações de outros registros relacionados ou pela execução de uma ação do sistema ou do usuário. Por exemplo, valores de predefinição, conforme a necessidade. Se a alteração de um campo afetar outro, ele também será atualizado. |
Domínio | Como o objeto de conta reage quando uma oportunidade é criada ou como o desconto é aplicado quando o processo de desconto da oportunidade é executado. Nota: esse tipo de lógica pode começar na camada de serviço, mas funcionar melhor na camada de domínio para gerenciar o tamanho e a complexidade do método de serviço ou melhorar a reutilização. |
Tratamento de comportamento comum que se aplica a vários objetos diferentes. |
Domínio | Calcular o preço nas linhas de produto de oportunidade e de produto de ordem de trabalho. Nota: você pode colocar isso em uma classe base de domínio compartilhada, substituindo o método fflib_SObjectDomain para enganchar nos eventos de acionador do Apex, com classes de domínio concretas estendendo essa classe, uma de cada vez, com seus comportamentos. |
Como controlar a imposição de segurança
Por padrão, a classe base fflib_SObjectDomain
impõe segurança CRUD no objeto Salesforce. No entanto, ela é invocada para todos os tipos de acesso ao objeto, seja por um controlador ou por um serviço. A lógica de serviço pode querer acessar um objeto em nome do usuário sem exigir permissões para o objeto.
Se você preferir desativar esse comportamento padrão e o impor por conta própria no código de serviço, poderá usar um recurso de configuração da classe base. O exemplo a seguir mostra como fazer isso em cada construtor. Outra forma de fazer isso é criando sua própria classe base com esse código e estendendo essa classe para todas as classes de domínio.
public Opportunities(List<Opportunity> sObjectList) {
super(sObjectList);
// Disable default Object Security checking
Configuration.disableTriggerCRUDSecurity();
}
Como testar classes de domínio
A fatoração da lógica em pedaços menores e mais encapsulados é boa para desenvolvimento guiado por testes (TDD), pois você pode construir as classes de domínio mais facilmente em seus testes e invocar os métodos diretamente. Isso não significa que você não deve testar sua camada de serviço, mas permite que você aplique uma abordagem mais incremental de teste e desenvolvimento.
Preparar-se para o desafio prático
Para concluir este desafio, inicie o Trailhead Playground que você usou no módulo Padrões empresariais do Apex: camada de serviço. Você precisará das bibliotecas de código aberto que você já instalou. Se você estiver usando um Trailhead Playground diferente, primeiro inicie-o e instale a biblioteca ApexMocks e, em seguida, a biblioteca Apex Commons usando os botões Implantar no Salesforce abaixo. Leia mais sobre essas bibliotecas e respectivos contratos de licença de código aberto em seus repositórios.
Implantar a biblioteca de código aberto ApexMocks.
Implantar a biblioteca de código aberto Apex Common.
Recursos
- GitHub: Padrões empresariais do Apex
- Wikipédia: Separação de preocupações
- Publicação do blog: Onde colocar o código de validação em um acionador do Apex
- Publicação do blog: Teste de unidade, Padrões corporativos e simulações do Apex – Parte 1
- Publicação do blog: Teste de unidade, Padrões corporativos e simulações do Apex – Parte 2
- Publicação do blog: Martin Fowler: Catálogo de padrões de arquitetura de aplicativos corporativos