ava-innersource / Liquid-Application-Framework-1.0-deprecated

Liquid is a framework to speed up the development of microservices
MIT License
25 stars 13 forks source link

Problems with the ICritic interface #54

Open guilhermeluizsp opened 5 years ago

guilhermeluizsp commented 5 years ago

Analyzing the structure of the ICritic interface, I'd say that it represents an error. There's only one implementation of this class on the Framework, and it is the Critic class, which confirms my theory.

https://github.com/Avanade/Liquid-Application-Framework/blob/93f7874f2ead13d3602cc39fc1e9355206217f57/src/Liquid.Base/Interfaces/CriticHandler/ICritic.cs#L10-L31

https://github.com/Avanade/Liquid-Application-Framework/blob/93f7874f2ead13d3602cc39fc1e9355206217f57/src/Liquid.Domain/CriticHandler/Critic.cs#L9-L77

This interface has several methods, such as "AddError" or "AddWarning", but these methods don't add anything, they just change the state of the class, which makes me think that those names aren't very clear. They could be renamed to "SetAsError" or "SetAsWarning".

But let's not end it here.

An error should be immutable, right? You can't (or shouldn't) turn a warning into an info. Semantically speaking, they're different things. If you have a warning and want to replace it to an info, naturally you should remove the warning first (as it's not a warning anymore) and add a new info. It also works with messages. If you want to change the message of an error, you actually want to remove the old one and add a new error (as it points to a different thing).

It also violates the Open/Closed Principle, as if there's a need to add a new method (such as "AddRecomentation"), we would need to change the interface.

With this in mind, what's the point (or the benefit) of having an interface at all? The Critic class works on its own. There's absolutely no need for an interface to exist in this case.

Also, currently, the class doesn't protects its invariants. Is it correct to have a null message or an empty code?

Initial solution

As mentioned in #35, the word "critic" is wrong for our context, so I renamed it to Problem, as it represents a problematic result. This class becomes immutable with factory methods.

/// <summary>
/// The type of a problem
/// </summary>
public enum ProblemType
{
    /// <summary>
    /// The operation has failed.
    /// </summary>
    Error = 1,

    /// <summary>
    /// The operation has succeded with warnings.
    /// </summary>
    Warning = 2,

    /// <summary>
    /// The operation has succeded with additional information.
    /// </summary>
    Info = 3
};

/// <summary>
/// Represents a problematic result of an operation.
/// </summary>
public class Problem
{
    /// <summary>
    /// A short code that uniquelly identifies the error.
    /// </summary>
    public string Code { get; }

    /// <summary>
    /// A text describing what happened. This may be used as a display value as it's intended to be human-readable.
    /// </summary>
    public string Message { get; }

    /// <summary>
    /// The type of the problem (e.g Error, Warning).
    /// </summary>
    public ProblemType Type { get; }

    /// <summary>
    /// Creates a new instance of a <see cref="Problem"/>.
    /// </summary>
    /// <param name="code">A short code that uniquelly identifies the error.</param>
    /// <param name="message">A human-readable message describing what went wrong.</param>
    /// <param name="type">The type of the problem (e.g Error, Warning).</param>
    public Problem(string code, string message, ProblemType type)
    {
        if (string.IsNullOrWhiteSpace(code))
            throw new ArgumentException("The code must not be empty.", nameof(code));

        if (string.IsNullOrWhiteSpace(message))
            throw new ArgumentException("The message must not be empty.", nameof(message));

        Code = code;
        Message = message;
        Type = type;
    }

    /// <summary>
    /// Creates an instance of <see cref="Problem"/> as an error (<see cref="Type"/> as <see cref="ProblemType.Error"/>).
    /// </summary>
    /// <param name="code">A short code that uniquelly identifies the error.</param>
    /// <param name="message">A human-readable message describing what went wrong.</param>
    /// <returns></returns>
    public static Problem AsError(string code, string message) => new Problem(code, message, ProblemType.Error);

    /// <summary>
    /// Creates an instance of <see cref="Problem"/> as a warning (<see cref="Type"/> as <see cref="ProblemType.Warning"/>).
    /// </summary>
    /// <param name="code">A short code that uniquelly identifies the error.</param>
    /// <param name="message">A human-readable message describing what went wrong.</param>
    /// <returns></returns>
    public static Problem AsWarning(string code, string message) => new Problem(code, message, ProblemType.Warning);

    /// <summary>
    /// Creates an instance of <see cref="Problem"/> as an info (<see cref="Type"/> as <see cref="ProblemType.Info"/>).
    /// </summary>
    /// <param name="code">A short code that uniquelly identifies the error.</param>
    /// <param name="message">A human-readable message describing what went wrong.</param>
    /// <returns></returns>
    public static Problem AsInfo(string code, string message) => new Problem(code, message, ProblemType.Info);
}

Benefits

What to consider

We can work through this.

bruno-brant commented 4 years ago

We should try to implement this as soon as possible. We need a strategy to minimize impact to existing customers.