Blog SAPienteSAPiente
ABAP Cloud

Design de métodos no ABAP: parâmetros, estrutura ou objeto?

SAPiente2 de ago. de 2026· 18 min read

Quantos parâmetros tem o constructor mais movimentado do seu sistema? Se a resposta for "uns sete, mas já foram quatro", este guia é para você. Assinatura de método é contrato: cada parâmetro solto a mais multiplica o custo de toda mudança futura — e é exatamente aí que a maioria das classes ABAP começa a apodrecer. Este post parte do artigo OO Design — Method Interface, publicado por Björn S. Software Heroes — OO Design: Method Interface, e vai bem além dele, aprofundando cada decisão com as regras do guia de estilo mantido pela SAP no GitHub.

O plano: começar de uma classe que todo mundo já escreveu (e que compila, funciona e passa em code review), entender por baixo como o ABAP passa parâmetros — e por que isso muda a decisão —, evoluir a classe em duas variantes (estrutura e objeto de configuração) e então subir os degraus que o artigo original não sobe: fail fast com fábrica e CREATE PRIVATE, o padrão builder para construções com muitos opcionais, o desenho do lado do erro da assinatura (exceção ou coletor?), os princípios SOLID que tudo isso realiza na prática, e os testes unitários que esse desenho destrava.

Por que a assinatura do método é uma decisão de arquitetura?

Porque ela define o custo de mudança do seu código. O Clean ABAP resume em uma regra: mantenha métodos enxutos, com poucos parâmetros, agrupando elementos relacionados em unidades coesas. Uma assinatura inchada não é só feia: ela acopla quem chama a cada detalhe interno de quem é chamado.

Pense no ciclo de vida real de uma classe de integração ou de processamento. Ela nasce com três parâmetros. Seis meses depois, alguém precisa de um timeout configurável. Depois, de um flag de simulação. Depois, de um segundo destino. Cada adição parece inofensiva — afinal, é "só mais um parâmetro". Mas cada uma edita o constructor, os atributos, os métodos internos e todos os chamadores.

O resultado você conhece: métodos com 8 IMPORTING (metade OPTIONAL), chamadas com 15 linhas de EXPORTING/IMPORTING, e ninguém mais sabe quais combinações de parâmetros são válidas. Não é falta de capricho — é falta de uma decisão de design que deveria ter sido tomada no começo. E é uma decisão barata de tomar cedo e cara de tomar tarde.

Por baixo do capô: referência, valor e o bug clássico do EXPORTING

Antes de discutir estilo, vale entender a mecânica — porque parte das recomendações do Clean ABAP só faz sentido completo quando você sabe o que o runtime faz com cada tipo de parâmetro. O comportamento por categoria:

Categoria

Passagem padrão

Estado na entrada do método

Consequência prática

IMPORTING

Por referência (read-only)

Valor do chamador

Sem cópia; escrita é bloqueada em tempo de compilação

IMPORTING VALUE(...)

Por valor

Cópia local

Método pode alterar a cópia sem afetar o chamador

EXPORTING

Por referência

Não inicializado — carrega o conteúdo atual da variável do chamador

A origem do bug do "valor da chamada anterior"

CHANGING

Por referência

Valor do chamador (leitura e escrita)

Modificação in-place intencional

RETURNING VALUE(...)

Por valor (obrigatório)

Sempre inicial

Nasce limpo em toda chamada; habilita estilo funcional

A linha que merece sua atenção é a do EXPORTING. Como a passagem padrão é por referência, o parâmetro formal não é limpo na entrada do método — ele enxerga o que estiver na variável do chamador naquele momento. Dois bugs clássicos nascem daí:

" Bug 1 — caminho que não preenche a saída
DATA message TYPE string VALUE `resto da chamada anterior`.

processor->run( IMPORTING message = message ).
" se run( ) tiver um caminho (um RETURN antecipado, por exemplo)
" que não preenche message, o `resto` sobrevive — e segue o fluxo
" como se fosse resultado desta chamada.

" Bug 2 — acumulação sobre lixo
METHOD collect_messages.
  " sem CLEAR result aqui...
  APPEND `nova mensagem` TO result.
  " ...os APPENDs somam em cima do que o chamador já tinha.
ENDMETHOD.

A defesa clássica é disciplina: CLEAR em todo parâmetro EXPORTING na primeira linha do método. Mas disciplina que depende de memória falha — e é por isso que a recomendação do Clean ABAP de preferir RETURNING não é preferência estética (Clean ABAP — Prefer RETURNING to EXPORTING): por ser passagem por valor obrigatória, o RETURNING nasce inicial em toda chamada. A classe inteira de bug simplesmente deixa de existir.

E o medo de performance — "por valor copia tudo"? Para tabelas internas, o kernel usa table sharing: a cópia física só acontece se alguém modificar a tabela depois. O próprio Clean ABAP tem uma regra com nome autoexplicativo: RETURNING large tables is usually okay. Meça antes de otimizar — o gargalo quase nunca está aí.

Sobra o CHANGING: ele tem lugar legítimo quando o propósito do método é modificar algo que já existe — ordenar uma tabela, enriquecer campos de uma estrutura recebida (Clean ABAP — Use CHANGING sparingly). O problema não é existir; é virar hábito onde um RETURNING expressaria melhor a intenção.

O ponto de partida: a classe que todo mundo já escreveu

O artigo da Software Heroes parte de uma classe deliberadamente "normal" — não é um espantalho, é o padrão que a gente encontra em qualquer sistema. Um constructor que recebe a configuração em parâmetros soltos, um método público run, uma validação privada e um método interno de processamento:

CLASS zcl_bs_demo_oo_intf1_start DEFINITION
  PUBLIC FINAL
  CREATE PUBLIC.

  PUBLIC SECTION.
    METHODS constructor
      IMPORTING config_id        TYPE char10
                !destination     TYPE string
                number_of_errors TYPE i
                !log             TYPE REF TO zif_aml_log.

    METHODS run
      EXPORTING system_id TYPE char3
                !message  TYPE string.

  PRIVATE SECTION.
    DATA config_id        TYPE char10.
    DATA destination      TYPE string.
    DATA number_of_errors TYPE i.
    DATA log              TYPE REF TO zif_aml_log.

    METHODS is_config_valid
      RETURNING VALUE(result) TYPE abap_boolean.

    METHODS internal_run
      IMPORTING config_id        TYPE char10
                !destination     TYPE string
                number_of_errors TYPE i
      EXPORTING system_id        TYPE char3
                !message         TYPE string.
ENDCLASS.


CLASS zcl_bs_demo_oo_intf1_start IMPLEMENTATION.
  METHOD constructor.
    me->config_id        = config_id.
    me->destination      = destination.
    me->number_of_errors = number_of_errors.
    me->log              = log.
  ENDMETHOD.

  METHOD run.
    IF NOT is_config_valid( ).
      RETURN.
    ENDIF.

    internal_run( EXPORTING config_id        = config_id
                            destination      = destination
                            number_of_errors = number_of_errors
                  IMPORTING system_id        = system_id
                            message          = message ).
  ENDMETHOD.

  METHOD is_config_valid.
    IF config_id IS INITIAL.
      log->add_message_text( `Config is initial` ).
    ENDIF.

    IF destination IS INITIAL.
      log->add_message_text( `Destination is mandatory` ).
    ENDIF.

    IF number_of_errors <= 0.
      log->add_message_text( `Insert a number bigger than 0` ).
    ENDIF.

    RETURN xsdbool( log->has_error( ) = abap_false ).
  ENDMETHOD.

  METHOD internal_run.
    " Faz o processamento
  ENDMETHOD.
ENDCLASS.

Funciona? Funciona. Passa no ATC? Passa. Então qual é o problema? O artigo aponta quatro, e todos são de manutenção, não de funcionamento:

  1. Constructor com interface larga: quatro parâmetros soltos que precisam ser tratados um a um — e que só tendem a crescer.

  2. Duplicação de parâmetros entre métodos: o run repassa os mesmos três valores para internal_run com a sintaxe verbosa de EXPORTING/IMPORTING. A mesma lista aparece em três assinaturas diferentes.

  3. Mudança em cascata: um único campo novo exige editar o constructor, o atributo privado, a assinatura de internal_run, a chamada dentro de run — e todos os pontos que instanciam a classe.

  4. Validação presa na classe: a lógica de is_config_valid vive dentro da classe de processamento. Não dá para reusá-la em outra classe que receba a mesma configuração, nem testá-la isolada.

Eu adicionaria um quinto, sutil: o run devolve system_id e message por EXPORTING — exatamente a categoria de parâmetro que acabamos de ver carregar lixo entre chamadas. E repare num detalhe que já aponta o caminho certo: is_config_valid usa RETURNING e por isso pode ser chamado direto no IF NOT is_config_valid( ), sem variável auxiliar. É esse estilo funcional que a gente quer para o resto da classe.

O que o Clean ABAP recomenda para parâmetros de método?

As regras são objetivas — e quase todas apontam para a mesma direção: menos parâmetros, uma única saída, estilo funcional . O quadro abaixo resume as que interessam para o nosso caso:

Regra (título original)

O que significa na prática

Aim for few IMPORTING parameters, at best less than three

Menos de 3 entradas por método. Passou disso, agrupe em estrutura ou objeto.

Prefer RETURNING to EXPORTING

Uma saída via RETURNING VALUE(...) habilita chamadas funcionais, encadeamento e DATA(...) inline.

Use either RETURNING or EXPORTING or CHANGING, but not a combination

Um único mecanismo de saída por método. Misturar confunde quem chama.

Split methods instead of adding OPTIONAL parameters

ABAP não tem overloading. Em vez de acumular OPTIONAL, crie métodos com nomes específicos.

Split method instead of Boolean input parameter

run( simulate = abap_true ) esconde dois comportamentos num método. Prefira run( ) e simulate( ).

RETURNING large tables is usually okay

O medo de "copiar tabela grande" raramente se justifica — meça antes de otimizar.

Do one thing, do it well, do it only

Método com várias saídas desconexas está fazendo coisa demais.

Duas dessas regras merecem um comentário a mais, porque são as mais desobedecidas na prática.

OPTIONAL é dívida de design

Cada parâmetro OPTIONAL cria combinações implícitas: com 3 opcionais, o método tem 8 modos de ser chamado — e nenhum deles está documentado na assinatura (Clean ABAP — Split methods instead of adding OPTIONAL parameters). Existe até o PREFERRED PARAMETER na linguagem para "aliviar" a ambiguidade — se você está precisando dele, o método já passou do ponto de dividir. A pergunta certa não é "esse parâmetro é opcional?", e sim "isso são dois métodos com nomes melhores?".

Flag booleano na entrada é dois métodos disfarçados

Quando o chamador escreve run( simulate = abap_true ), quem lê a chamada precisa abrir o método para descobrir o que o flag muda. Dois métodos com nomes honestos (run( ) e simulate( )) delegando para um privado comum documentam a intenção no ponto de chamada — de graça.

E por que tanta insistência no RETURNING? Porque um método com exatamente um valor de retorno vira um método funcional: pode ser usado como operando em expressões, encadeado e combinado com declarações inline. Compare o antes e depois de uma mesma chamada:

" Com EXPORTING — verboso, exige variáveis declaradas antes
internal_run( EXPORTING config_id = config_id
                        destination = destination
              IMPORTING system_id = system_id
                        message   = message ).

" Com RETURNING — funcional, inline, encadeável
DATA(result) = internal_run( config ).
DATA(message) = internal_run( config )-message.

Regra: se você se pegar escrevendo EXPORTING num método novo, pare e pergunte: "essas saídas são um resultado ou vários?" Se são um resultado lógico, agrupe numa estrutura e devolva com RETURNING. Se são vários resultados desconexos, o método provavelmente está fazendo mais de uma coisa.

Variante A — estrutura: quando agrupar campos já resolve?

A primeira evolução proposta pelo artigo é agrupar os parâmetros relacionados em tipos estruturados locais — uma estrutura para a configuração de entrada, outra para o resultado. É a solução de menor custo e resolve três dos quatro problemas:

TYPES:
  BEGIN OF configuration,
    config_id        TYPE char10,
    destination      TYPE string,
    number_of_errors TYPE i,
    log              TYPE REF TO zif_aml_log,
  END OF configuration.

TYPES:
  BEGIN OF run_result,
    system_id TYPE char3,
    message   TYPE string,
  END OF run_result.

Com os tipos definidos, as assinaturas colapsam para um parâmetro de entrada e um de saída — e olha o que acontece com o run:

METHODS constructor
  IMPORTING config TYPE configuration.

METHODS run
  RETURNING VALUE(result) TYPE run_result.

METHODS internal_run
  IMPORTING config        TYPE configuration
  RETURNING VALUE(result) TYPE run_result.

" ...

METHOD run.
  IF NOT is_config_valid( ).
    RETURN.
  ENDIF.

  RETURN internal_run( config ).
ENDMETHOD.

A chamada de internal_run caiu de seis linhas para uma. O constructor virou uma atribuição (me->config = config). E o principal: campo novo agora muda um único lugar — a definição do tipo. A assinatura de todos os métodos permanece estável, e os chamadores que não usam o campo novo nem percebem a mudança.

Diagrama comparando o efeito cascata de adicionar um campo: com parâmetros soltos a mudança atinge constructor, atributos, métodos internos e chamadores; com estrutura ou objeto, atinge apenas a definição do tipo.

O mesmo campo novo: com parâmetros soltos, 4+ pontos de edição; com estrutura ou objeto, 1.

Detalhe que pouca gente usa: o RETURN internal_run( config ). — devolver o resultado de outra chamada direto no RETURN — só existe desde o ABAP 7.58 (e no ABAP Cloud). Em releases anteriores, escreva result = internal_run( config ). seguido de RETURN. — o efeito é o mesmo.

Onde declarar o TYPE? Na classe, na interface ou no dicionário?

A regra prática: o tipo mora onde mora o contrato. Três opções, em ordem crescente de alcance:

  • Na classe (PUBLIC SECTION): o padrão para tipos que só existem por causa daquela classe. Consumidores referenciam zcl_processor=>configuration — o acoplamento é explícito e rastreável.

  • Na interface: se a classe tem uma interface (zif_processor), o tipo pertence a ela — quem programa contra o contrato não deveria precisar conhecer a implementação nem para declarar variáveis. Sinal claro de tipo no lugar errado: TYPE REF TO zif_processor na variável, zcl_processor=>configuration no tipo.

  • No dicionário (DDIC): obrigatório quando a estrutura cruza a fronteira do sistema — RFC, released APIs, persistência. O custo é ciclo de vida mais pesado (transporte, where-used gigante, extensão governada). Não pague esse custo para um tipo que só duas classes usam.

O anti-padrão: a estrutura-lixão

Estrutura também apodrece. O sintoma: metade dos consumidores preenche metade dos campos, e ninguém sabe mais quais combinações são válidas — a "estrutura de contexto" que só cresce é o equivalente estrutural do método com 8 OPTIONAL. Quando dois grupos de campos têm ciclos de vida diferentes (um é configuração de conexão, outro é parametrização de negócio), são duas estruturas. Agrupar não é amontoar: é reunir o que muda junto.

E os limites da variante? A estrutura continua sendo transporte de dados, nada mais. A validação segue presa na classe de processamento (problema 4 da lista). Não há como garantir invariantes — qualquer código com acesso à estrutura pode alterar seus campos a qualquer momento. E tem um cheiro sutil aqui: o campo log TYPE REF TO zif_aml_log mistura dados de configuração com uma dependência de infraestrutura no mesmo saco. Funciona, mas embaralha responsabilidades — e é exatamente isso que a próxima variante organiza.

Variante B — objeto de configuração: o que muda de verdade?

A segunda evolução extrai a configuração para uma classe própria, com atributos READ-ONLY populados só pelo constructor e — a mudança decisiva — a validação dentro do próprio objeto. É o padrão clássico de parameter object aplicado ao ABAP:

CLASS zcl_bs_demo_oo_intf_config DEFINITION
  PUBLIC FINAL
  CREATE PUBLIC.

  PUBLIC SECTION.
    DATA config_id        TYPE char10             READ-ONLY.
    DATA destination      TYPE string             READ-ONLY.
    DATA number_of_errors TYPE i                  READ-ONLY.
    DATA log              TYPE REF TO zif_aml_log READ-ONLY.

    METHODS constructor
      IMPORTING config_id        TYPE char10
                !destination     TYPE string
                number_of_errors TYPE i
                !log             TYPE REF TO zif_aml_log.

    METHODS is_valid
      RETURNING VALUE(result) TYPE abap_boolean.
ENDCLASS.


CLASS zcl_bs_demo_oo_intf_config IMPLEMENTATION.
  METHOD constructor.
    me->config_id        = config_id.
    me->destination      = destination.
    me->number_of_errors = number_of_errors.
    me->log              = log.
  ENDMETHOD.

  METHOD is_valid.
    IF config_id IS INITIAL.
      log->add_message_text( `Config is initial` ).
    ENDIF.

    IF destination IS INITIAL.
      log->add_message_text( `Destination is mandatory` ).
    ENDIF.

    IF number_of_errors <= 0.
      log->add_message_text( `Insert a number bigger than 0` ).
    ENDIF.

    RETURN xsdbool( log->has_error( ) = abap_false ).
  ENDMETHOD.
ENDCLASS.

Na classe de processamento, o constructor agora recebe uma única referência, e o run pergunta ao próprio objeto se ele é válido:

METHOD run.
  IF NOT config->is_valid( ).
    RETURN.
  ENDIF.

  RETURN internal_run( config ).
ENDMETHOD.

O que esse desenho compra que a estrutura não compra?

  • Validação desacoplada e reusável: is_valid agora viaja junto com a configuração. Dez classes que consomem a mesma configuração validam do mesmo jeito — a regra vive num lugar só.

  • Imutabilidade: com READ-ONLY, ninguém altera a configuração depois de criada. O Clean ABAP recomenda exatamente isso no lugar de getters para objetos que não mudam após a construção (Clean ABAP — Consider using immutable instead of getter). Imutabilidade não é purismo: um objeto que não muda pode ser passado para qualquer método, guardado em qualquer atributo, compartilhado entre quantos consumidores for — sem que ninguém precise se perguntar "quem mexeu nisso no meio do caminho?".

  • Espaço para crescer com comportamento: precisa carregar a configuração de uma tabela? Derivar valores default? Formatar para log? São métodos novos no objeto de configuração — sem tocar em nenhuma assinatura existente.

  • Coesão: dados e as regras sobre esses dados moram juntos. É a definição de encapsulamento que a gente aprende no primeiro dia de OO e esquece no primeiro projeto.

Na prática: se preferir getters/setters em vez de atributos READ-ONLY (por exemplo, para ajustar ou normalizar valores na entrada), o ADT gera os dois com um quick fix: Ctrl+1 sobre o atributo → Generate getter/setter. Mas comece pelo READ-ONLY — getter sem lógica é ruído. Adicione o getter quando (e se) a lógica aparecer.

O custo? Mais um artefato no pacote e um pouco mais de cerimônia para instanciar. O artigo original é honesto nesse ponto: nenhuma das variantes "ganha" em todos os cenários, e a comunidade não tem consenso fechado — a escolha depende do contexto e do custo de implementação. A régua que uso: se a configuração tem regras (validação, derivação, defaults), objeto. Se é só um punhado de campos que viajam juntos, estrutura.

Fail fast: com fábrica e CREATE PRIVATE, objeto inválido não nasce

O desenho da Variante B tem uma fresta: nada impede alguém de instanciar a configuração e usá-la sem nunca chamar is_valid. A validação existe, mas é opcional — e validação opcional, em sistema grande, é validação pulada. O passo seguinte fecha essa fresta invertendo a lógica: em vez de "crie e depois valide", "só nasce se for válido".

A receita tem três passos: CREATE PRIVATE (ninguém de fora instancia com NEW), um método de fábrica estático que valida antes de devolver, e uma exceção para o caso inválido:

CLASS zcl_bs_demo_oo_intf_config DEFINITION
  PUBLIC FINAL
  CREATE PRIVATE.

  PUBLIC SECTION.
    CLASS-METHODS create
      IMPORTING config_id        TYPE char10
                !destination     TYPE string
                number_of_errors TYPE i
                !log             TYPE REF TO zif_aml_log
      RETURNING VALUE(result)    TYPE REF TO zcl_bs_demo_oo_intf_config
      RAISING   zcx_invalid_config.

    " atributos READ-ONLY e constructor como antes...
ENDCLASS.


CLASS zcl_bs_demo_oo_intf_config IMPLEMENTATION.
  METHOD create.
    DATA(instance) = NEW zcl_bs_demo_oo_intf_config(
        config_id        = config_id
        destination      = destination
        number_of_errors = number_of_errors
        log              = log ).

    IF NOT instance->is_valid( ).
      RAISE EXCEPTION NEW zcx_invalid_config( ).
    ENDIF.

    RETURN instance.
  ENDMETHOD.
ENDCLASS.

A partir daqui, o invariante vale para o sistema inteiro: se existe uma referência a zcl_bs_demo_oo_intf_config, ela é válida. A classe de processamento joga fora o IF NOT config->is_valid( ) do run — não porque a validação sumiu, mas porque ela virou pré-condição garantida pelo tipo. Cada consumidor que deixava de validar era um bug em potencial; agora a categoria inteira desapareceu.

Diagrama do padrão fail fast: o chamador só consegue criar a configuração pelo método de fábrica create, que valida antes de devolver; se a validação passa, nasce uma instância imutável; se falha, a exceção zcx_invalid_config é lançada e o objeto nunca existe.

Fail fast: com CREATE PRIVATE + fábrica, referência que circula é referência válida.

Quando usar cada estilo de validação? Os dois têm lugar:

  • is_valid adiado (Variante B pura): quando o objeto representa entrada do usuário ou de sistema externo que pode legitimamente estar incompleta, e você quer coletar todas as mensagens de erro para devolver de uma vez — o padrão do exemplo, com o coletor de log.

  • Fail fast na fábrica: quando o objeto é infraestrutura interna do seu design — configuração, parâmetros de serviço, value objects. Aqui, inválido significa bug de programação, e a exceção no ponto de criação aponta o culpado exato, em vez de deixar o objeto quebrado viajar até explodir longe da causa.

Insight: fábrica estática tem um bônus escondido: ela pode ter nome. create_from_customizing( ), create_for_test( ), create_default( ) — três formas de construção com contratos explícitos, coisa que um único constructor (ABAP não tem overloading) nunca vai expressar. É o idioma que compensa a limitação da linguagem.

E quando são muitos parâmetros opcionais? O padrão builder no ABAP

Estrutura e objeto resolvem o agrupamento — mas não resolvem a construção quando há muitos campos opcionais com defaults. Um create( ) com 9 IMPORTING (6 OPTIONAL) só mudou o problema de lugar. Para esse cenário específico existe o builder: um objeto intermediário que acumula os valores passo a passo, com método encadeável por campo, e valida tudo no final:

CLASS zcl_config_builder DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    METHODS with_config_id
      IMPORTING value       TYPE char10
      RETURNING VALUE(self) TYPE REF TO zcl_config_builder.

    METHODS with_destination
      IMPORTING value       TYPE string
      RETURNING VALUE(self) TYPE REF TO zcl_config_builder.

    METHODS with_number_of_errors
      IMPORTING value       TYPE i
      RETURNING VALUE(self) TYPE REF TO zcl_config_builder.

    METHODS build
      RETURNING VALUE(result) TYPE REF TO zcl_bs_demo_oo_intf_config
      RAISING   zcx_invalid_config.

  PRIVATE SECTION.
    DATA config_id        TYPE char10.
    DATA destination      TYPE string.
    DATA number_of_errors TYPE i VALUE 1.  " default explícito
ENDCLASS.


CLASS zcl_config_builder IMPLEMENTATION.
  METHOD with_config_id.
    me->config_id = value.
    self = me.
  ENDMETHOD.

  " with_destination e with_number_of_errors: mesmo padrão

  METHOD build.
    RETURN zcl_bs_demo_oo_intf_config=>create(
        config_id        = config_id
        destination      = destination
        number_of_errors = number_of_errors
        log              = log ).
  ENDMETHOD.
ENDCLASS.

Cada with_* devolve self = me — e é isso que habilita o encadeamento no ponto de uso:

DATA(config) = NEW zcl_config_builder(
    )->with_config_id( 'DEMO01'
    )->with_destination( `DEST_MIRROR`
    )->with_number_of_errors( 3
    )->build( ).

O chamador informa só o que difere do default, em qualquer ordem, com nome de método autodocumentado — e o build( ) delega para a fábrica fail-fast, então o invariante de validade continua de pé. Repare que o builder é a aplicação direta de duas coisas já vistas: métodos funcionais com RETURNING (sem isso não há encadeamento) e a fábrica com validação.

Regra: builder se paga a partir de ~5 campos com maioria opcional e defaults não triviais. Para 3 campos obrigatórios, é cerimônia pura — fique na fábrica. E cuidado com o builder sem validação no build( ): ele só transforma um objeto inválido difícil de criar num objeto inválido agradável de criar.

Exceção, resultado ou coletor: como desenhar o lado do erro da assinatura?

Até aqui falamos do caminho feliz da assinatura — entradas e saída. Mas o lado do erro também é contrato, e o ABAP oferece três estilos com consequências bem diferentes. Escolher errado aqui é o que produz aqueles TRY/CATCH vazios e validações que param no primeiro erro quando deviam listar todos.

Estilo

Como aparece na assinatura

Quando usar

Armadilha

Flag / código de retorno

RETURNING result TYPE abap_boolean

Perguntas genuínas (is_valid, exists)

Como sinalização de falha de operação, é legado: o chamador pode simplesmente ignorar

Exceção baseada em classe

RAISING zcx_...

Contrato violado, pré-condição quebrada, situação que impede continuar

Para no primeiro erro — péssimo para validação de formulário/planilha

Coletor / objeto de resultado

log injetado, ou RETURNING result com mensagens dentro

Validação em massa: o chamador quer todas as mensagens, não a primeira

Exige disciplina do chamador de consultar o coletor no final

O exemplo deste post usa o coletor (zif_aml_log) — e faz sentido: is_valid quer reportar as três violações de uma vez, não abortar na primeira. Já a fábrica fail-fast usa exceção — e também faz sentido: configuração inválida ali é bug de programação, e o RAISING zcx_invalid_config na assinatura força o chamador a se posicionar. Os dois estilos convivem no mesmo design, cada um no seu papel.

Se esse desenho parece familiar, é porque a própria SAP o adotou em escala: no EML do RAP, um MODIFY ENTITIES não lança exceção quando uma entidade falha — ele devolve as estruturas FAILED e REPORTED, coletores tipados com todas as falhas e mensagens da operação em massa. É o estilo coletor institucionalizado no modelo de programação, exatamente porque operações em massa não podem parar no primeiro erro.

Detalhe que pouca gente usa: exceção também comunica na assinatura. Um método com RAISING zcx_config_invalid zcx_destination_unreachable documenta seus modos de falha melhor que qualquer comentário — e o compilador cobra o tratamento de quem chama (para exceções de checagem estática). Assinatura sem RAISING nenhum também comunica: "eu não falho — ou falho de um jeito que você não trata".

Como interfaces e injeção de dependência destravam os testes?

Repare que o exemplo inteiro depende de zif_aml_log — uma interface de log, não uma classe concreta. Isso não é acaso: é o que permite testar a validação sem gravar log de verdade. O Clean ABAP formaliza os dois lados dessa moeda: métodos públicos deveriam fazer parte de uma interface, e dependências deveriam ser injetáveis para permitir test doubles.

A interface do exemplo é mínima — dois métodos:

INTERFACE zif_aml_log PUBLIC.
  METHODS add_message_text
    IMPORTING text TYPE string.

  METHODS has_error
    RETURNING VALUE(result) TYPE abap_boolean.
ENDINTERFACE.
Diagrama de injeção de dependência: a classe de negócio depende da interface zif_aml_log, que é implementada pela classe real de log em produção e por um log spy local no teste unitário.

Inversão de dependência: a classe depende da interface; produção e teste plugam implementações diferentes.

Como a classe de configuração recebe o log pelo constructor (injeção de dependência), o teste unitário entrega um dublê no lugar da implementação real. Um log spy local, que só coleciona as mensagens, resolve:

CLASS ltd_log_spy DEFINITION FOR TESTING.
  PUBLIC SECTION.
    INTERFACES zif_aml_log.
    DATA messages TYPE string_table.
ENDCLASS.

CLASS ltd_log_spy IMPLEMENTATION.
  METHOD zif_aml_log~add_message_text.
    APPEND text TO messages.
  ENDMETHOD.

  METHOD zif_aml_log~has_error.
    result = xsdbool( messages IS NOT INITIAL ).
  ENDMETHOD.
ENDCLASS.


CLASS ltc_config DEFINITION FOR TESTING
  RISK LEVEL HARMLESS DURATION SHORT.

  PRIVATE SECTION.
    METHODS rejeita_config_vazia FOR TESTING.
ENDCLASS.

CLASS ltc_config IMPLEMENTATION.
  METHOD rejeita_config_vazia.
    DATA(log_spy) = NEW ltd_log_spy( ).

    DATA(config) = NEW zcl_bs_demo_oo_intf_config(
                       config_id        = ''
                       destination      = ``
                       number_of_errors = 0
                       log              = log_spy ).

    cl_abap_unit_assert=>assert_false( config->is_valid( ) ).
    cl_abap_unit_assert=>assert_equals(
        act = lines( log_spy->messages )
        exp = 3 ).
  ENDMETHOD.
ENDCLASS.

O teste roda em milissegundos, não toca banco, não grava log — e ainda verifica o comportamento: configuração vazia tem que gerar exatamente três mensagens. Sem a interface e sem a injeção, esse teste seria impossível ou exigiria gambiarras. Repare também no que o spy testa que um mock "de retorno fixo" não testaria: a interação — quantas mensagens, com qual conteúdo se você quiser afirmar sobre elas.

Para dependências que você não quer dublar à mão, o ABAP OO Test Double Framework gera o dublê dinamicamente a partir da interface — liberado inclusive para ABAP Cloud:

" dublê dinâmico a partir da interface — sem escrever classe
DATA(log_double) = CAST zif_aml_log(
    cl_abap_testdouble=>create( 'zif_aml_log' ) ).

" grava o comportamento: has_error( ) deve devolver abap_false
cl_abap_testdouble=>configure_call( log_double )->returning( abap_false ).
log_double->has_error( ).

" injeta o dublê no objeto sob teste
DATA(config) = NEW zcl_bs_demo_oo_intf_config(
    config_id        = 'DEMO01'
    destination      = `DEST_MIRROR`
    number_of_errors = 1
    log              = log_double ).

Detalhe que pouca gente usa: o cl_abap_testdouble só gera dublês de interfaces ou classes não-FINAL sem constructor obrigatório. Como o Clean ABAP manda marcar classes como FINAL por padrão, na prática o framework pressupõe o desenho baseado em interface. As duas regras se fecham: FINAL na classe + interface para o contrato + injeção no constructor é o trio que torna tudo testável.

Vale criar interface para toda classe, então? Não mecanicamente. A regra do Clean ABAP mira classes com consumidores externos ou que precisam de dublê; para um helper privado de um único pacote, a interface pode ser cerimônia pura. O critério honesto: alguém precisa substituir essa implementação — em teste ou em runtime? Se sim, interface. Se não, adie. Um exemplo real desse desenho no standard: a interface IF_ABAP_PARALLEL do CL_ABAP_PARALLEL — um único método do, e o framework aceita qualquer classe que a implemente.

SOLID na prática: o que esse redesign realiza

Sem querer, o percurso do post implementou os cinco princípios SOLID — e vale explicitar onde, porque "SOLID" costuma ser citado como mantra e raramente mostrado em código ABAP concreto:

Princípio

Onde apareceu neste post

S — Responsabilidade única

A classe de configuração tem um motivo para mudar: as regras da configuração. Validação saiu da classe de processamento e foi morar com os dados que valida.

O — Aberto/fechado

Assinaturas estáveis (estrutura/objeto) absorvem campos novos sem modificar consumidores: extensão sem modificação, literalmente.

L — Substituição de Liskov

ltd_log_spy e a implementação real de zif_aml_log são intercambiáveis sem quebrar quem as usa — é o que faz o teste unitário sequer funcionar.

I — Segregação de interfaces

zif_aml_log tem 2 métodos, não 20. O mesmo raciocínio condena a estrutura-lixão: contratos pequenos e focados, sempre.

D — Inversão de dependência

A classe de negócio depende da abstração (zif_aml_log) e recebe a implementação pelo constructor — nunca instancia a dependência concreta por conta própria.

Note a direção: ninguém partiu de "vamos aplicar SOLID". Partimos de um problema concreto — assinatura cara de manter — e os princípios emergiram das soluções. É o sentido saudável da coisa: SOLID como consequência de boas decisões locais, não como checklist a priori. E o Clean ABAP completa com a recomendação estrutural que amarra o L e o D: prefira composição a herança — hierarquias são difíceis de acertar e caras de refatorar.

Parâmetros, estrutura ou objeto: como decidir?

A resposta curta: pela natureza dos dados e pelo número de pontos de edição que uma mudança futura vai exigir. O artigo original conclui que ambas as variantes têm prós e contras e que a escolha depende do cenário — mas dá para transformar essa ponderação em critérios objetivos:

Critério

Parâmetros soltos

Estrutura

Objeto de configuração

Nº de entradas na assinatura

1 por campo

1

1

Campo novo exige editar

4+ pontos

1 ponto (o tipo)

1–2 pontos (classe de config)

Validação / invariantes

Espalhada nos consumidores

Espalhada nos consumidores

Dentro do objeto; com fábrica, garantida

Imutabilidade

Não (campos livres)

Sim (READ-ONLY)

Defaults e construção flexível

OPTIONAL (frágil)

VALUE #( ) parcial

Fábricas nomeadas / builder

Reuso entre classes

Copia e cola

Bom (tipo compartilhado)

Ótimo (dados + regras juntos)

Test double via interface

Possível (extraindo interface)

Custo de implementação

Zero

Baixo

Médio (artefato novo)

Em forma de árvore de decisão:

Árvore de decisão para desenhar assinaturas de método em ABAP: até três parâmetros estáveis e sem relação ficam como parâmetros individuais; dados relacionados que são apenas transporte viram estrutura; necessidade de validação, comportamento, reuso ou test double leva ao objeto de configuração.

Árvore de decisão: comece simples e promova quando os sinais aparecerem.

Três observações para calibrar a régua:

  • Não pule etapas por precaução. Duas entradas estáveis e sem relação entre si (um ID e um flag, digamos) ficam ótimas como parâmetros individuais. Estrutura de um campo só é burocracia.

  • Sinais de que é hora de promover a estrutura: a mesma lista de parâmetros aparece em 2+ assinaturas; você está no terceiro parâmetro e já enxerga o quarto; a chamada não cabe mais numa linha.

  • Sinais de que é hora de promover a objeto: apareceu a primeira regra de validação sobre os dados; dois lugares validam a mesma coisa de jeitos diferentes; você quer testar a validação isolada; a "configuração" começou a precisar de comportamento (carregar, derivar, salvar). E se a construção tem muitos opcionais com defaults — some o builder.

Os limites do padrão: quando não aplicar

Nem toda assinatura é sua para redesenhar — e nem toda classe pequena merece a cerimônia completa. Antes de sair refatorando, cheque os limites:

  • Assinaturas impostas por framework: handlers do RAP (FOR MODIFY, FOR READ, ações), módulos de função RFC e BAdIs têm assinaturas geradas ou fixadas — você implementa o contrato que recebeu. O que dá para fazer é delegar imediatamente: o handler extrai os dados e chama a sua classe de negócio, essa sim com assinatura limpa. Foi o desenho que usei no RAP Unmanaged, onde os handlers só orquestram e a lógica vive em classes próprias.

  • RFC e integração exigem tipos flat: interfaces RFC não transportam referências a objetos. Um objeto de configuração para na fronteira do sistema — ali a estrutura (de dicionário) é o teto.

  • APIs liberadas para outros times (released APIs): estabilidade de contrato vale mais que elegância. Adicionar campo numa estrutura publicada é mudança visível para todos os consumidores; faça o design da estrutura antes de liberar, pensando em extensão futura.

  • Overengineering em helpers triviais: um método privado chamado num único lugar, com dois parâmetros, não precisa de estrutura, objeto nem interface. Clean code também é saber parar.

Regra: o padrão se aplica ao seu modelo de objetos — as classes que você desenha e controla. Na borda (framework, RFC, API pública), o contrato manda; dentro, delegue rápido para classes com assinaturas enxutas e teste lá.

Como refatorar uma classe existente sem quebrar quem chama?

Em código novo, é só começar certo. Em código existente com N consumidores, a migração precisa ser incremental — ABAP não tem overloading, então não dá para manter duas versões do mesmo método com o mesmo nome. O caminho que funciona:

  1. Proteja tudo com ABAP Unit primeiro. Refatorar assinatura sem teste é trocar o motor com o carro andando. Se a classe não tem costura para teste (dependências instanciadas dentro), este é o momento de criar a primeira — extraia a dependência para o constructor.

  2. Crie o tipo (ou a classe de configuração), sem tocar nas assinaturas. Só isso já centraliza a definição dos campos.

  3. Converta os métodos privados (internal_run e afins) para receber a estrutura/objeto. Consumidores externos nem percebem — a mudança é interna e os testes do passo 1 seguram o comportamento.

  4. Crie o novo método público em paralelo quando a assinatura pública precisar mudar: um run_with_config( config ) ao lado do run antigo, com o antigo delegando para o novo. Marque o antigo como obsoleto no ABAP Doc ("! @deprecated).

  5. Migre os chamadores no seu ritmo — o where-used (Ctrl+Shift+G no ADT) é a lista de tarefas — e remova o método antigo quando a lista zerar. Os quick fixes do ADT (Ctrl+1) automatizam boa parte: extrair método, mudar assinatura, gerar constructor.

Repare que a ordem importa: o passo barato e invisível (métodos privados) vem antes do passo caro e visível (assinatura pública). Na maioria das classes, o passo 3 já elimina 80% da duplicação — e talvez você nem precise do 4.

Detalhe que pouca gente usa: adicionar campo numa estrutura é mudança quase sempre compatível — VALUE #( ) com componentes nomeados e MOVE-CORRESPONDING nem percebem. Os três pontos que quebram silenciosamente: comparação de estruturas inteiras (IF struct_a = struct_b passa a comparar o campo novo também), serializações (o JSON/XML de saída ganha um campo — contrato externo mudou) e código legado com acesso por offset. Vale um where-used antes de estender tipo compartilhado.

Perguntas frequentes

RETURNING copia o valor — não é lento para tabelas grandes?

Na prática, quase nunca. O próprio Clean ABAP tem uma regra chamada RETURNING large tables is usually okay: com o table sharing do kernel, a cópia física só acontece se alguém modificar a tabela depois. Se um caso concreto aparecer no trace, otimize esse caso — não o codebase inteiro por precaução.

Posso combinar EXPORTING e RETURNING no mesmo método?

A sintaxe permite, o Clean ABAP desaconselha: um único mecanismo de saída por método. Misturar obriga o chamador a decidir entre chamada funcional e EXPORTING/IMPORTING — e métodos com saídas desconexas geralmente estão fazendo mais de uma coisa. Divida o método ou agrupe as saídas numa estrutura de resultado.

Devo criar uma interface para toda classe?

Não mecanicamente. O Clean ABAP recomenda interface para métodos públicos consumidos de fora ou que precisem de test double — mas para helpers internos sem consumidor externo, a interface é custo sem retorno. Pergunte: alguém vai substituir essa implementação? Sem resposta afirmativa, adie.

Validação: exceção ou coletor de mensagens?

Depende de quem consome o erro. Entrada de usuário/sistema externo que precisa de todas as mensagens de uma vez: coletor (como o zif_aml_log do exemplo, ou failed/reported no EML). Pré-condição interna violada, bug de programação: exceção na fábrica, fail fast. Os dois convivem no mesmo design — cada um no seu papel.

Estrutura com referência a objeto dentro (como o log) é boa prática?

Funciona — referência é um campo como outro qualquer —, mas mistura dados de configuração com dependência de infraestrutura. O desenho fica mais limpo separando: dados na estrutura ou objeto de configuração; dependências (log, HTTP client, repositório) injetadas no constructor da classe que as usa. Assim cada peça pode ser trocada de forma independente no teste.

Getter e setter ou atributo público READ-ONLY?

Para objetos imutáveis, o Clean ABAP prefere atributo READ-ONLY a getter sem lógica. Getter se justifica quando há lógica na leitura (valor derivado, lazy load); setter, quando há validação na escrita — e nesse caso avalie se a validação não pertence ao constructor ou à fábrica.

Builder no ABAP não é overengineering?

Para 3 campos obrigatórios, é. O builder se paga quando a construção tem ~5+ campos com maioria opcional e defaults não triviais — o cenário em que a alternativa seria uma fábrica com 6 OPTIONAL, que é exatamente o problema que estamos fugindo. E sempre com validação no build( ); builder sem validação só embeleza a criação de objetos inválidos.

Conclusão

O artigo da Software Heroes termina com a conclusão certa: não existe variante vencedora universal — existe o compromisso de manter assinaturas enxutas que minimizem o esforço de adaptação futura. O que este post acrescenta é a escada completa, com os degraus que vêm depois do artigo:

  • Parâmetros individuais para 2–3 entradas estáveis e sem relação — com RETURNING para a saída, sempre (e agora você sabe por que: o EXPORTING por referência nem inicializado chega).

  • Estrutura quando os campos viajam juntos — 1 ponto de edição por mudança, tipo declarado onde mora o contrato.

  • Objeto de configuração quando os dados ganham regras — validação coesa, imutabilidade via READ-ONLY, reuso.

  • Fábrica + CREATE PRIVATE quando o invariante importa — instância inválida não nasce; builder quando a construção tem muitos opcionais.

  • Exceção ou coletor escolhidos pelo consumidor do erro — não por hábito.

  • Interface + injeção quando alguém (inclusive o ABAP Unit) precisa substituir a implementação.

Se quiser ver esses princípios aplicados em contextos reais do dia a dia: o guia do CL_ABAP_PARALLEL mostra o poder de uma interface de um método só; é exatamente o tipo de dependência que você vai querer atrás de uma interface como a zif_aml_log deste post; e o post de RAP Unmanaged mostra a delegação de handlers para classes de negócio com assinatura limpa.

Fontes (consultadas em 30/07/2026): Software Heroes — OO Design: Method Interface (Björn S., 28/07/2026);

SAP — Clean ABAP Styleguide (GitHub); ABAP Keyword Documentation — Functional Method Call;

ABAP Keyword Documentation — Method Chaining;

SAP Samples — ABAP Unit Tests (abap-cheat-sheets);

SAP Community — ABAP Test Double Framework: An Introduction;

Software Heroes — ABAP Unit: Test Double Framework.

TagsDesenvolvedorClean Core
Avalie este conteúdo
para avaliar
SAPiente
@sapiente

Blog sobre SAP, ABAP, BTP, Fiori e tudo que envolve o ecossistema SAP.

Ver perfil →
FacebookInstagramYouTubeTwitter

Comentários 0

Entre na conversa

Faça login para deixar seu comentário neste artigo.

Ainda sem comentários

Seja o primeiro a comentar este artigo.