Skip to main content

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.
Nota

Nota

Deseja aprender em português (Brasil)? Nesse emblema, as validações dos desafios práticos do Trailhead funcionam em inglês. As traduções são fornecidas entre parênteses como referência No Trailhead Playground, (1) mude a localidade para Estados Unidos, (2) mude o idioma para inglês e (3) copie e cole apenas os valores em inglês. Siga as instruções aqui.

Consulte o emblema Trailhead no seu idioma para saber mais sobre como aproveitar a experiência de Trailhead em outros idiomas.

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.

Classe fflib_SObjectDomain.

Nota

Nota: A maioria dos métodos existe como métodos virtuais. Os métodos handle*XXXX* nessa classe base são responsáveis por fazer com que os métodos on*XXXX* sejam chamados nos momentos certos. Para casos mais avançados em que você precisa de manipulação direta, você pode substituir os métodos de manipulador, mas esteja ciente de que, a menos que você chame a versão de superclasse do método handle*XXX, os métodos de superclasse on*XXXX* não serão chamados. Por exemplo, consulte os métodos da classe OpportunitiesTriggerHandler para obter vários exemplos.

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;
        }               
    }
}
Nota

Nota: o exemplo acima também é possibilitado pelo uso de uma fórmula expressa como parte da definição de campo DiscountType__c. É claro que a lógica de predefinição poderia abarcar vários campos ou outros registros. Nesse caso, você deve usar codificação em Apex.

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.

Codifique usando um método de classe de domínio que possa aplicar um desconto a uma oportunidade.

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();
}  
Nota

Nota: a classe base não dá suporte genérico para impor segurança em nível de campo nas atualizações. Essa imposição ainda é responsabilidade sua.

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 para Salesforce

Implantar a biblioteca de código aberto Apex Common.

Botão Implantar no Salesforce.

Recursos

Continue a aprender de graça!
Inscreva-se em uma conta para continuar.
O que você ganha com isso?
  • Receba recomendações personalizadas para suas metas de carreira
  • Pratique suas habilidades com desafios práticos e testes
  • Monitore e compartilhe seu progresso com os empregadores
  • Conecte-se a orientação e oportunidades de carreira