質問

I read/watch about Uncle Bob's clean code and when he talks about exceptions he suggested to have a well named exception over a description which make perfect sense for the programmer, for decoupling the main application form the business logic and for the tests.

But what about the user? I have an application that take several parameters and each of them can be wrong. I tough of those solutions but each of them has pros and cons:

  1. Explicit message

    pros: The message is with the exception, thus it should be accurate.

    cons: It couples the business logic with the UI

  2. Mega switch in the catch

    pros: The message is decoupled from the business logic, thus the message can have a custom formatting, be updated without touching the busines rules etc.

    cons: Add a new exception will need to modify the switch, thus it is likely I will miss an exception

  3. Combinaison of 1 and 2

    pros and cons: It will have the best and the worst of the previous solutions.

Am I missing the good 4th options?

役に立ちましたか?

解決

  1. Mega switch in the catch

You lost me at "mega switch". Mega switches are common bad practice. They're textbook OCP violations, on top of generating near monolithic code, and often indicative of a design that's not promoting reusability to the degree that it should.

  1. Explicit message
    pros: The message is with the exception, thus it should be accurate.
    cons: It couples the business logic with the UI

It depends on your requirements, concerns, and what precisely you mean by "explicit message". Do you mean a single string property?

Quite often, when two desired features conflict and you still want both, it can be resolved using a more in-depth solution. You can (usually) have your two features at the same time, but it comes at the cost of extra effort. Is that extra effort worth it? That's something you have to decide.

The main takeaway here is that you need to balance features, complexity and time spent. While having the business generate a message for your UI may feel less clean, if it covers all your needs, it's probably going to be the easiest to implement.

As an example, let's say your business layer writes a descriptive string into your exception's Message property.

throw new InsufficientFundsException(
    $"Cannot withdraw {withdrawalAmount}, there is only {availableBalance} available"
);

The first issue here is one of reusability. Since your exception targets a specific error, the message is likely going to be the same for all exceptions of this type, so you wouldn't want to copy/paste this message all over the codebase. So let's put the message (format) in the exception itself:

public class InsufficientFundsException : Exception
{
    public InsufficientFundsException(int withdrawalAmount, int availableBalance) 
      : base(GenerateMessage(withdrawalAmount, availableBalance))
    {
        // Using the base constructor to pass the message.
    }

    private static string GenerateMessage(int withdrawalAmount, int availableBalance)
    {
        return $"Cannot withdraw {withdrawalAmount}, there is only {availableBalance} available";
    }
}

// In business

throw new InsufficientFundsException(command.WithdrawalAmount, account.CurrentBalance);

That's a decent but simple approach. The business is the most aware of what the actual issue is, so it's most capable of describing the issue.

Is this bad design because the business is deciding the message? Well, that depends on whether your UI actually needs to interfere in the generation of this message. Before we delve into examples of UI concerns, I just want to point out that if there is no UI interference needed, then there's no reason to continue adding complexity to this design. Once the design covers your needs, it doesn't need to be expanded any further.

As an example of why the UI needs to interfere, the language of a user (in case of multiple languages) is a UI-driven consideration, and it's a valid reason to not let the (user language agnostic) business logic generate the message.

The solution here is to further work out how to pass this data from the business to the UI without forcing the business to be multilingual.
If you're happy making the UI responsible for deciding the entire message format; instead of using the Message property you can extend your custom exception type with the data (which is not language-specific):

public class InsufficientFundsException : Exception
{
    public int WithdrawalAmount { get; }
    public int AvailableBalance { get; }

            public InsufficientFundsException(int withdrawalAmount, int availableBalance) 
      : base()
    {
        this.WithdrawalAmount = withdrawalAmount;
        this.AvailableBalance = availableBalance;
    }
}    

// in Business

throw new InsufficientFundsException(command.WithdrawalAmount, account.CurrentBalance);

// in UI

catch(InsufficientFundsException ife)
{
    // use ife.WithdrawalAmount and ife.AvailableBalance to generate a language-specific message
}

But you can make a reasonable argument that while the decision of the language is a UI-driven concern, the messages themselves (in all possible languages) should be stored somewhere in the database. There's different camps here. Some people put the error messages in the UI, others put them in the database (only to be accessed/changed by an admin user).

If you're storing them in the database, then you're going to have to further extend your exception logic to fetch the correct message at the right time.

// in Business

throw new InsufficientFundsException(command.WithdrawalAmount, account.CurrentBalance);

// in UI

catch(InsufficientFundsException ife)
{
    ife.GenerateMessageForLanguage(currentUser.Language);
}

I will leave the implementation of GenerateMessageForLanguage up to you. At this point, it's become clear that you need a data store for error messages, where you can retrieve a specific message based on a language and error type. There are many ways to implement this, and this answer can't contain an entire working out of this.

The main takeaway here is that your design is based on your needs. Different needs, different design. And on top of that, different companies might prioritize their development differently. Some will favor minimizing development time, whereas others will favor extending the projected lifespan of the application (which tends to take more time to develop). There is no one-size-fits-all answer that everyone agrees with.

他のヒント

Why do Exceptions have Names/Types?
Because we have code constructs to catch Exceptions based on their Type.

Why do Exceptions have Custom Properties?
So that the code catching the Exception has all the information it needs to handle the Exception delivered to it in a consistent format.

Why do Exceptions have Messages?
Possibly because Exceptions "grew" out of Errors and they used to have [Codes and] Messages.

Should the User ever see an Exception?
Not the whole thing, no.
They're big, scary screenfuls of technical "gibberish" that Users don't have any interest in, whatsoever.

Should the User ever see an Exception Message?
Maybe but, as you quite rightly say, you get into all sorts of trouble with translation and where the message needs to be generated.
Catching and translating the Message on the fly (your "case" statement) is one alternative but remember that you can't just look for exact words in the Message because, to misquote Leonard McCoy:

I know Developers; they love to change things.

Of course, your "case" statement should always have the fallback position, if no translation/mapping is appropriate, of passing the given Message verbatim, or with an additional "I don't recognise this one!" moniker. That way you don't "miss" any.

Option 1 mixes up the application logic and UI, as you've noted. It also fouls up internationalization of the application, as the message is hard-coded into the application code.

Option 2 just moves the problem to another bit of the application logic.

If an exception needs to be displayed to the user, and not all will be, then pass the exception up to the UI layer. It can then look at the exception type, and any data passed within the exception, and then determine what message to display to the user. The UI is probably better written using a look-up-table, rather than a switch statement, to handle all the different exception types.

ライセンス: CC-BY-SA帰属
所属していません softwareengineering.stackexchange
scroll top