Consistência e Exceptions

Publicado em 19/04/2011

10


Olá pessoal, como estamos?

Exceptions são muito importantes. Elas funcionam como alertas de que algo não está bem. Lançamos exceptions, por exemplo, quando:

  • um método é chamado com um parâmetro inválido;
  • há uma tentativa de colocar um objeto em estado inválido.

Há muitos posts e artigos que mostram como deve ser a captura e tratamento eficiente de Exceptions (código cliente). Infelizmente, há bem pouco material disponível sobre formas eficientes de lançar exceptions. O post de hoje é dedicado a esse tema.

O problema

Lançar Exceptions é a opção natural para alertar um comportamento não-OK de parte do sistema.

É obrigação de todo objeto garantir que seu estado permaneça válido (consistente). Mais que isso, é obrigação de todo objeto denunciar “objetos irresponsáveis” que tentem o colocar em estado inválido.

Lançar exceptions também é a caminho mais comum para tornar evidente que um método foi evocado com um parâmetro inadequado. Sendo radical, um método deve rejeitar qualquer chamada inválida e tornar evidente que fez seu trabalho devido a entradas inválidas.

public void DoSomething(object a)
{
    if (a == null)
        throw new ArgumentNullException("a");
    // ...
}

Ao lançar Exceptions, estamos dando a oportunidade para que os programadores de códigos clientes possam fazer correções rapidamente e evitar comportamentos inesperados ou indesejados.

Entretanto, lançar um exception não é uma decisão fácil. Sempre precisamos considerar:

  1. Que tipo de exception devo lançar?
  2. Que mensagem fornecer?
  3. Como tratar sua localização (idioma e cultura)?
  4. Como garantir uma escolha coerente de tipos Exceptions e mensagens em todo o sistema?

O código que apresentei acima mostra a opção de um programador para lançar um Exception. Ela foi uma escolha adequada? Considere o seguinte bloco de código:

public void DoSomething(string a)
{
    if (String.IsNullOrEmpty(a))
        throw new ArgumentException("Parameter a cannot be Null nor Empty");
    // ...
}

Observe que uma mesma Exception é lançada para o valor null or empty. Querendo ou não, temos um caso claro de comportamento inconsistente.

As questões que apresentei são apenas algumas das dúvidas que precisam ser resolvidas claramente para todos o time antes que qualquer código seja escrito. Mas o que queremos?

  1. Garantir um padrão coerente na escolha de tipos de exceptions. Por exemplo, se um parâmetro for passado nulo, que sempre seja lançada uma ArgumentNullException (nunca ArgumentException);
  2. Garantir um padrão coerente nas mensagens associadas às instâncias das exceptions;
  3. Facilitar a localização (cultura e idioma) das mensagens associadas às exceptions;
  4. Possibilitar adequação das Exceptions para diferentes cenários de uso, possibilitando, inclusive, a alteração do tipo de Exception que está sendo lançado sem adicionar complexidade ao código origem;
  5. Permitir que o time se concentre no negócio no lugar de aspectos técnicos.

A solução (uma proposta, pelo menos)

Criar um componente que centralize o lançamento de Exceptions para todo o sistema. Esse componente deve permitir ao desenvolver se concentrar no negócio. Observe os exemplos apresentados acima reapresentados com o conceito que defendo hoje:

public void DoSomething(object a)
{
    Throw.IfArgumentNull(a, "a");
    // ...
}

Repare como o código já fica mais expressivo e mais limpo. Repare que retiramos do programdor a responsabilidade de saber que Exception será disparada. Há um foco muito mais claro no negócio. Vamos ao outro exemplo:

public void DoSomething(string a)
{
    Throw.IfArgumentNullOrEmpty(a, "a");
    // ...
}

Mais uma vez, perceba como o código ficou mais limpo. Além disso, tiramos do desenvolvedor a carga de escolher a exception que será lançada. Centralizar o lançamento de exceptions garantiu consistência. Mais que isso, retiramos do desenvolvedor a responsabilidade de ter que escrever a mensagem associada a exception.

Implementação dos métodos de verificação/consistência

Quero começar o passeio de nossa implementação mostrando o código fonte associado a IfArgumentNull. Observe:

[DebuggerStepThrough]
public static void IfArgumentNull(
    object argument, 
    string argumentName,
    Func< Exception, Exception > modifier = null
    )
{
    if (argument != null) return;
            
    Throw.Now(
        new ArgumentNullException(argumentName),
        modifier
        );
}

Primeiro, observe como transferimos a criação da Exception para o interior desse método. No lugar de lançar a exception aqui chamo ainda um outro método. Observe também que decorei o método com o atributo DebuggerStepThrough, assim faço com que o método não seja considerado pela depuração passo-a-passo.

Observe agora o método IfArgumentNullOrEmpty:

[DebuggerStepThrough]
public static void IfArgumentNullOrEmpty(
    string argument,
    string argumentName,
    Func<Exception, Exception> modifier = null
    )
{
    Throw.IfArgumentNull(argument, argumentName, modifier);

    if (!String.IsNullOrEmpty(argument)) return;

    Throw.Now(
        new ArgumentException(
            string.Format(
                CultureInfo.CurrentUICulture,
                ThrowResources.ArgumentNullOrEmpty,
                argumentName)
            ),
        modifier
        );
}

Aqui, temos um exemplo bacana. Nosso IfArgumentNullOrEmpty lança um ArgumentNullException caso o argumento esteja nulo e um ArgumentException caso o argumento esteja Empty.

Por favor, observe como também garantimos a padronização da mensagem associada a exception, uma vez que estamos construindo a mensagem no interior do método. Perceba como é prático transportar a string para um resource, facilitando a localização.

Modificando o “comportamento” do sistema com Modifiers

Nosso mecanismo para lançamento de Exceptions pode ser ajustado através da passagem de um Modifier como argumento (opcional). O que esse “Modifier” faz é capturar a Exception que seria lançada inicialmente, realizar algum processamento e devolver uma exception transformada (ou a original, conforme a regra do negócio).

Essa inteligência permite que que, por exemplo, possamos utilizar uma “infraestrutura” de exceptions conforme contexto. Por exemplo, gerando uma “FaultException” no WCF.

Observe a implementação do método central Now que “opera” essa computação:

public static Func<Exception, Exception> GlobalModifier { get; set; }

[DebuggerStepThrough]
public static void Now(
    Exception exception,
    Func<Exception, Exception> modifier = null
    )
{

    if (exception == null)
        throw new InvalidOperationException(ThrowResources.ExceptionCannotBeNull);

    Exception e = exception;

    if (modifier != null)
        e = modifier(exception) ?? exception;

    if (GlobalModifier != null)
        e = GlobalModifier(e) ?? exception;

    throw e;
}

O que fizemos? Definimos uma propriedade para um “modifier global” e, antes de finalmente realizar o throw, submetemos a exception as duas modificações. Com alguma criatividade, podemos incorporar, inclusive, um mecanismo de log para as exceptions.

Bacana!

Por hoje, era isso.

Smiley piscando

Etiquetado:,
Publicado em: Post