Quais são as boas maneiras de equilibrar exceções informativas e código limpo?

11

Com nosso SDK público, tendemos a enviar mensagens muito informativas sobre o motivo de uma exceção. Por exemplo:

if (interfaceInstance == null)
{
     string errMsg = string.Format(
          "Construction of Action Argument: {0}, via the empty constructor worked, but type: {1} could not be cast to type {2}.",
          ParameterInfo.Name,
          ParameterInfo.ParameterType,
          typeof(IParameter)
    );

    throw new InvalidOperationException(errMsg);
}

No entanto, isso tende a atrapalhar o fluxo do código, pois tende a colocar muito foco nas mensagens de erro, e não no que o código está fazendo.

Um colega começou a refatorar algumas das exceções lançando algo assim:

if (interfaceInstance == null)
    throw EmptyConstructor();

...

private Exception EmptyConstructor()
{
    string errMsg = string.Format(
          "Construction of Action Argument: {0}, via the empty constructor worked, but type: {1} could not be cast to type {2}.",
          ParameterInfo.Name,
          ParameterInfo.ParameterType,
          typeof(IParameter)
    );

    return new InvalidOperationException(errMsg);
}

O que facilita a compreensão da lógica do código, mas adiciona muitos métodos extras para o tratamento de erros.

Quais são as outras maneiras de evitar o problema da "lógica de confusão de mensagens de exceção longa"? Estou perguntando principalmente sobre C # /. NET idiomático, mas como outros idiomas o gerenciam também são úteis.

[Editar]

Seria bom ter os prós e contras de cada abordagem também.

FriendlyGuy
fonte
4
Na IMHO, a solução do seu colega é muito boa, e eu acho que se você tiver muitos métodos extras desse tipo, poderá reutilizar pelo menos alguns deles. Ter muitos métodos pequenos é bom quando seus métodos são bem nomeados, desde que eles criem blocos de construção fáceis de entender do seu programa - esse parece ser o caso aqui.
Doc Brown
@DocBrown - Sim, eu gosto da ideia (adicionada como resposta abaixo, juntamente com os prós / contras), mas, tanto para o intellisense quanto para o número potencial de métodos, ele também começa a parecer desorganizado.
FriendlyGuy
11
Pensamento: não faça da mensagem a transportadora de todos os detalhes da exceção. Uma combinação de usar a Exception.Datapropriedade, captura "exigente" de exceção, captura de código e adição de seu próprio contexto, juntamente com a pilha de chamadas capturadas, contribuem com informações que devem permitir mensagens muito menos detalhadas. Finalmente, System.Reflection.MethodBaseparece promissor fornecer detalhes para passar ao seu método de "construção de exceção".
Radarbob
@radarbob você está sugerindo que talvez as exceções sejam muito detalhadas? Talvez estejamos parecendo demais com o log.
FriendlyGuy
11
@MackleChan, li que o paradigma aqui é colocar informações em uma mensagem que tenta dizer 'zactly o que aconteceu' é necessariamente gramaticalmente correta e uma pretensão de AI: ".. via o construtor vazio funcionou, mas ..." Realmente? Meu codificador forense interno vê isso como uma conseqüência evolutiva do erro comum de repetições de perda de rastreamento de pilha e desconhecimento de Exception.Data. A ênfase deve ser a captura de telemetria. Refatorar aqui é bom, mas perde o problema.
Radarbob 18/12/2013

Respostas:

10

Por que não ter classes de exceção especializadas?

if (interfaceInstance == null)
{
    throw new ThisParticularEmptyConstructorException(<maybe a couple parameters>);
}

Isso leva a formatação e os detalhes à exceção em si e deixa a classe principal organizada.

ptyx
fonte
11
Prós: Muito limpo e organizado, fornece nomes significativos para cada exceção. Contras: potencialmente muitas classes de exceções extras .
FriendlyGuy
Se isso importa, você pode ter um número limitado de classes de exceção, passar a classe de onde a exceção se originou como parâmetro e ter uma opção gigante dentro de classes de exceção mais genéricas. Mas isso tem um leve cheiro - não tenho certeza se eu iria lá.
Ptyx
isso cheira muito mal (exceções fortemente associadas às classes). Eu acho que seria difícil equilibrar as mensagens de exceção personalizáveis ​​versus o número de exceções.
FriendlyGuy
7

A Microsoft parece (via olhando a fonte .NET) às vezes usar cadeias de recursos / ambiente. Por exemplo ParseDecimal:

throw new OverflowException(Environment.GetResourceString("Overflow_Decimal"));

Prós:

  • Centralizando mensagens de exceção, permitindo a reutilização
  • Manter a mensagem de exceção (sem dúvida, que não importa codificar) longe da lógica dos métodos
  • O tipo de exceção lançada é claro
  • As mensagens podem ser localizadas

Contras:

  • Se uma mensagem de exceção for alterada, todas elas serão alteradas.
  • A mensagem de exceção não está tão facilmente disponível para o código que lança a exceção.
  • A mensagem é estática e não contém informações sobre quais valores estão errados. Se você quiser formatá-lo, é mais desorganizado no código.
FriendlyGuy
fonte
2
Você deixou um grande benefício: Localização do texto da exceção
17 de 26
@ 17of26 - Bom ponto, acrescentou.
FriendlyGuy
Votei na sua resposta, mas as mensagens de erro assim são "estáticas"; você não pode adicionar modificadores a eles, como o OP está fazendo no código dele. Então você efetivamente deixou de lado algumas das funcionalidades dele.
Robert Harvey
@RobertHarvey - eu adicionei como um golpe. Isso explica por que as exceções internas nunca fornecem informações locais. Além disso, para sua informação, sou OP (conhecia essa solução, mas queria saber se outras pessoas têm soluções melhores).
FriendlyGuy
6
@ 17of26 como desenvolvedor, odeio exceções localizadas com paixão. Eu tenho que deslocalizá-los todas as vezes antes que eu possa, por exemplo. google para soluções.
111313 Konrad Morawski
2

Para o cenário público do SDK, eu consideraria fortemente o uso de contratos de código da Microsoft, pois eles fornecem erros informativos, verificações estáticas e você também pode gerar documentação para adicionar aos documentos XML e aos arquivos de ajuda gerados pelo Sandcastle . É suportado em todas as versões pagas do Visual Studio.

Uma vantagem adicional é que, se seus clientes estiverem usando C #, eles poderão aproveitar seus assemblies de referência de contrato de código para detectar possíveis problemas antes mesmo de executarem seu código.

A documentação completa dos contratos de código está aqui .

James World
fonte
2

A técnica que eu uso é combinar e terceirizar a validação e lançar completamente para uma função de utilidade.

O benefício mais importante é que ele é reduzido a uma linha na lógica de negócios .

Aposto que você não pode fazer melhor a menos que possa reduzi-lo ainda mais - para eliminar todas as validações de argumento e proteções de estado do objeto da lógica de negócios, mantendo apenas as condições operacionais excepcionais.

Obviamente, existem maneiras de fazer isso - linguagem fortemente tipada, "nenhum objeto inválido é permitido a qualquer momento", design por contrato , etc.

Exemplo:

internal static class ValidationUtil
{
    internal static void ThrowIfRectNullOrInvalid(int imageWidth, int imageHeight, Rect rect)
    {
        if (rect == null)
        {
            throw new ArgumentNullException("rect");
        }
        if (rect.Right > imageWidth || rect.Bottom > imageHeight || MoonPhase.Now == MoonPhase.Invisible)
        {
            throw new ArgumentException(
                message: "This is uselessly informative",
                paramName: "rect");
        }
    }
}

public class Thing
{
    public void DoSomething(Rect rect)
    {
        ValidationUtil.ThrowIfRectNullOrInvalid(_imageWidth, _imageHeight, rect);
        // rest of your code
    }
}
rwong
fonte
1

[note] Copiei isso da pergunta para uma resposta, caso haja comentários sobre ela.

Mova cada exceção para um método da classe, aceitando os argumentos que precisam da formatação.

private Exception EmptyConstructor()
{
    string errMsg = string.Format(
          "Construction of Action Argument: {0}, via the empty constructor worked, but type: {1} could not be cast to type {2}.",
          ParameterInfo.Name,
          ParameterInfo.ParameterType,
          typeof(IParameter)
    );

    return new InvalidOperationException(errMsg);
}

Coloque todos os métodos de exceção na região e coloque-os no final da classe.

Prós:

  • Mantém a mensagem fora da lógica central do método
  • Permite adicionar informações lógicas a cada mensagem (você pode passar argumentos para o método)

Contras:

  • Desordem de método. Potencialmente, você pode ter vários métodos que retornam exceções e não estão realmente relacionados à lógica de negócios.
  • Não é possível reutilizar mensagens em outras classes
FriendlyGuy
fonte
Eu acho que os contras que você citou superam as vantagens.
neontapir
@neontapir os contras são todos facilmente abordados. Os métodos auxiliares devem receber IntentionRevealingNames e agrupados em alguns #region #endregion(o que faz com que sejam ocultos do IDE por padrão) e, se aplicáveis ​​a diferentes classes, coloque-os em uma internal static ValidationUtilityclasse. Aliás, nunca reclame nomes longos de identificadores na frente de um programador de C #.
Rwong
Eu reclamaria de regiões. Na minha opinião, se você quiser recorrer a regiões, a classe provavelmente terá muitas responsabilidades.
31415 ne nepirpir
0

Se você conseguir se livrar de erros um pouco mais gerais, poderá escrever uma função de conversão genérica estática pública para você, que infere o tipo de fonte:

public static I CastOrThrow<I,T>(T t, string source)
{
    if (t is I)
        return (I)t;

    string errMsg = string.Format(
          "Failed to complete {0}, because type: {1} could not be cast to type {2}.",
          source,
          typeof(T),
          typeof(I)
        );

    throw new InvalidOperationException(errMsg);
}


/// and then:

var interfaceInstance = SdkHelper.CastTo<IParameter>(passedObject, "Action constructor");

Existem variações possíveis (pense SdkHelper.RequireNotNull()), que apenas verificam os requisitos das entradas e lançam se elas falharem, mas neste exemplo, combinar o elenco com a produção do resultado é autodocumentado e compacto.

Se você estiver no .net 4.5, há maneiras de o compilador inserir o nome do método / arquivo atual como um parâmetro do método (consulte CallerMemberAttibute ). Mas para um SDK, você provavelmente não pode exigir que seus clientes mudem para o 4.5.

jdv-Jan de Vaan
fonte
É mais sobre exceções lançadas em geral (e gerenciando as informações em uma exceção versus o quanto isso atrapalha o código), não sobre esse exemplo específico de transmissão.
FriendlyGuy
0

O que gostamos de fazer para erros de lógica de negócios (não necessariamente erros de argumento etc.) é ter uma enumeração única que define todos os tipos possíveis de erros:

/// <summary>
/// This enum is used to identify each business rule uniquely.
/// </summary>
public enum BusinessRuleId {

    /// <summary>
    /// Indicates that a valid body weight value of a patient is missing for dose calculation.
    /// </summary>
    [Display(Name = @"DoseCalculation_PatientBodyWeightMissing")]
    PatientBodyWeightMissingForDoseCalculation = 1,

    /// <summary>
    /// Indicates that a valid body height value of a patient is missing for dose calculation.
    /// </summary>
    [Display(Name = @"DoseCalculation_PatientBodyHeightMissing")]
    PatientBodyHeightMissingForDoseCalculation = 2,

    // ...
}

Os [Display(Name = "...")]atributos definem a chave nos arquivos de recursos a serem usados ​​para converter as mensagens de erro.

Além disso, esse arquivo pode ser usado como ponto de partida para encontrar todas as ocorrências em que um certo tipo de erro é gerado no seu código.

A verificação de regras de negócios pode ser delegada a classes especializadas do Validator que produzem listas de regras de negócios violadas.

Em seguida, usamos um tipo de exceção personalizado para transportar as regras violadas:

[Serializable]
public class BusinessLogicException : Exception {

    /// <summary>
    /// The Business Rule that was violated.
    /// </summary>
    public BusinessRuleId ViolatedBusinessRule { get; set; }

    /// <summary>
    /// Optional: additional parameters to be used to during generation of the error message.
    /// </summary>
    public string[] MessageParameters { get; set; }

    /// <summary>
    /// This exception indicates that a Business Rule has been violated. 
    /// </summary>
    public BusinessLogicException(BusinessRuleId violatedBusinessRule, params string[] messageParameters) {
        ViolatedBusinessRule = violatedBusinessRule;
        MessageParameters = messageParameters;
    }
}

As chamadas de serviço de back-end são agrupadas no código genérico de tratamento de erros, que traduz a regra de busines violada em uma mensagem de erro legível pelo usuário:

public object TryExecuteServiceAction(Action a) {
    try {
        return a();
    }
    catch (BusinessLogicException bex) {
        _logger.Error(GenerateErrorMessage(bex));
    }
}

public string GenerateErrorMessage(BusinessLogicException bex) {
    var translatedError = bex.ViolatedBusinessRule.ToTranslatedString();
    if (bex.MessageParameters != null) {
        translatedError = string.Format(translatedError, bex.MessageParameters);
    }
    return translatedError;
}

Aqui ToTranslatedString()está um método de extensão para enumler as chaves de recursos dos [Display]atributos e usar o ResourceManagerpara converter essas chaves. O valor da respectiva chave de recurso pode conter espaços reservados para string.Format, que correspondem ao fornecido MessageParameters. Exemplo de uma entrada no arquivo resx:

<data name="DoseCalculation_PatientBodyWeightMissing" xml:space="preserve">
    <value>The dose can not be calculated because the body weight observation for patient {0} is missing or not up to date.</value>
    <comment>{0} ... Patient name</comment>
</data>

Exemplo de uso:

throw new BusinessLogicException(BusinessRuleId.PatientBodyWeightMissingForDoseCalculation, patient.Name);

Com essa abordagem, você pode separar a geração da mensagem de erro da geração do erro, sem a necessidade de introduzir uma nova classe de exceção para cada novo tipo de erro. Útil se diferentes frontends exibirem mensagens diferentes, se a mensagem exibida depender do idioma e / ou função do usuário etc.

Georg Patscheider
fonte
0

Eu usaria cláusulas de guarda como neste exemplo para que as verificações de parâmetros possam ser reutilizadas.

Além disso, você pode usar o padrão do construtor para que mais de uma validação possa ser encadeada. Será um pouco parecido com afirmações fluentes .

Martin K
fonte