Design de métodos no ABAP: parâmetros, estrutura ou objeto?
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 |
|---|---|---|---|
| Por referência (read-only) | Valor do chamador | Sem cópia; escrita é bloqueada em tempo de compilação |
| Por valor | Cópia local | Método pode alterar a cópia sem afetar o chamador |
| 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" |
| Por referência | Valor do chamador (leitura e escrita) | Modificação in-place intencional |
| 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:
Constructor com interface larga: quatro parâmetros soltos que precisam ser tratados um a um — e que só tendem a crescer.
Duplicação de parâmetros entre métodos: o
runrepassa os mesmos três valores parainternal_runcom a sintaxe verbosa deEXPORTING/IMPORTING. A mesma lista aparece em três assinaturas diferentes.Mudança em cascata: um único campo novo exige editar o constructor, o atributo privado, a assinatura de
internal_run, a chamada dentro derun— e todos os pontos que instanciam a classe.Validação presa na classe: a lógica de
is_config_validvive 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 |
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 |
|
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
EXPORTINGnum 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 comRETURNING. 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.
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 noRETURN— só existe desde o ABAP 7.58 (e no ABAP Cloud). Em releases anteriores, escrevaresult = internal_run( config ).seguido deRETURN.— 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_processorna variável,zcl_processor=>configurationno 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_validagora 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+1sobre o atributo → Generate getter/setter. Mas comece peloREAD-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.
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_validadiado (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 |
| Perguntas genuínas ( | Como sinalização de falha de operação, é legado: o chamador pode simplesmente ignorar |
Exceção baseada em classe |
| 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 |
| 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_unreachabledocumenta 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 semRAISINGnenhum 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.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_testdoublesó gera dublês de interfaces ou classes não-FINAL sem constructor obrigatório. Como o Clean ABAP manda marcar classes comoFINALpor 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 |
|
I — Segregação de interfaces |
|
D — Inversão de dependência | A classe de negócio depende da abstração ( |
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 ( |
Defaults e construção flexível | OPTIONAL (frágil) |
| 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: 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:
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.
Crie o tipo (ou a classe de configuração), sem tocar nas assinaturas. Só isso já centraliza a definição dos campos.
Converta os métodos privados (
internal_rune afins) para receber a estrutura/objeto. Consumidores externos nem percebem — a mudança é interna e os testes do passo 1 seguram o comportamento.Crie o novo método público em paralelo quando a assinatura pública precisar mudar: um
run_with_config( config )ao lado dorunantigo, com o antigo delegando para o novo. Marque o antigo como obsoleto no ABAP Doc ("! @deprecated).Migre os chamadores no seu ritmo — o where-used (
Ctrl+Shift+Gno 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 eMOVE-CORRESPONDINGnem percebem. Os três pontos que quebram silenciosamente: comparação de estruturas inteiras (IF struct_a = struct_bpassa 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
RETURNINGpara a saída, sempre (e agora você sabe por que: oEXPORTINGpor 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 PRIVATEquando 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;
Comentários 0
Ainda sem comentários
Seja o primeiro a comentar este artigo.
Entre na conversa
Faça login para deixar seu comentário neste artigo.