Pattern Matching in codice OO

2

Ho una situazione in cui ho bisogno di modellare oggetti che non condividono attributi comuni ma rappresentano la stessa entità logica. Ora, in base al loro tipo avranno attributi diversi (proprietà). Per mantenere un codice di esempio semplice & facile da capire, diciamo che abbiamo Valid & Invalid risultati di convalida. Quando il risultato della convalida non è valido espone un messaggio di errore, altrimenti non espone nulla.

Questi sono i modelli di oggetti dei risultati di convalida:

public interface IValidationResult
{ }

public sealed class Valid : IValidationResult
{
    private Valid()
    { }

    public static Valid Instance { get; } = new Valid();
}

public sealed class Invalid : IValidationResult
{
    public string ErrorMessage { get; }

    public Invalid(string errorMessage)
    {
        ErrorMessage = errorMessage;
    }
}

In un certo senso ho bisogno di un unione discriminata che non è supportato in C # ancora .

L'utilizzo:

static IValidationResult Validate(string name)
{
    if(string.IsNullOrWhiteSpace(name))
        return new Invalid("Name can't be null or empty string");

    return Valid.Instance;
}

static async Task Main(string[] args)
{
    var validationResult = Validate("Michael");
    switch (validationResult)
    {
        case Valid _:
        {
            break;
        }
        case Invalid invalidResult:
        {
            Console.WriteLine($"Invalid name: {invalidResult.ErrorMessage}");
            break;
        }
    }
}

Come si può vedere, sto facendo uso della corrispondenza dei tipi per prendere una decisione sul flusso del programma in base al tipo di validationResult .

Anche se apprezzo che si tratti di un codice leggibile, non mi sembra il codice OO. Quindi, sono andato avanti e ho creato la seconda versione di questo, che è più OO per me, ma è più codice e non così elegante (IMO).

public interface IValidationResult2
{
    void UseResult(Action<string> useValidationResultAction);
}

public sealed class Valid2 : IValidationResult2
{
    private Valid2()
    { }

    public static Valid2 Instance { get; } = new Valid2();
    public void UseResult(Action<string> useValidationResultAction)
    { }
}

public sealed class Invalid2 : IValidationResult2
{
    public string ErrorMessage { get; }

    public Invalid2(string errorMessage)
    {
        ErrorMessage = errorMessage;
    }

    public void UseResult(Action<string> useValidationResultAction)
    {
        useValidationResultAction(ErrorMessage);
    }
}

E l'utilizzo:

static IValidationResult2 Validate2(string name)
{
    if (string.IsNullOrWhiteSpace(name))
        return new Invalid2("Name can't be null or empty string");

    return Valid2.Instance;
}

static async Task Main(string[] args)
{
    var validationResult2 = Validate2("Michael");
    validationResult2.UseResult(errMsg => Console.WriteLine($"Invalid name: {errMsg}"));
}

Come ho detto mentre questo secondo approccio sembra più OO per me, tuttavia, mi sembra che sia meno leggibile / elegante.

Posso pensare a una terza opzione, in cui validationResult consentirebbe di configurare il flusso del programma in base al risultato (sia esso valido o non valido), ma anche quello del secondo approccio. Senza la piena implementazione, questo è il modo in cui l'utilizzo della terza opzione sarà:

static async Task Main(string[] args)
{
    var validationResult3 = Validate3("Michael");
    validationResult3
        .WhenValid(_ => { })
        .WhenInvalid(errMsg => { Console.WriteLine($"Invalid name: {errMsg}"); })
        .Execute();
}

Quale dovrebbe essere preferito e amp; perché ? Lo considereresti un odore di codice quando vedrai pattern matching nel codice OO?

    
posta Michael 21.07.2018 - 13:53
fonte

2 risposte

2

Non tutto deve essere perfettamente orientato agli oggetti, anche se devi chiamare quasi tutto a class in C #.

Il tuo primo progetto è un'approssimazione ragionevole di un'unione in C #. Potresti renderlo leggermente più sicuro impedendo altre implementazioni dell'interfaccia dei risultati di convalida, ad es. utilizzando una classe base con un costruttore interno.

Il tuo secondo progetto utilizza un'implementazione radicalmente più complessa utilizzando tecniche funzionali. Questa complessità può ripagare se ciò consente di utilizzare combinatori o modelli comuni o quando abilita un intero ecosistema di applicazioni - si veda ad es. LINQ. Qui, ti salva solo un'istruzione if.

Potresti notare che il tuo tipo di risultato è OK o una stringa di errore, che potrebbe anche essere rappresentata (leggermente meno chiaramente) con un tipo nullable come string o un valore nullo personalizzato Error . Ovviamente è possibile evitare i null e restituire invece un'istanza speciale. Tuttavia, non è necessario il matching completo di pattern con un interruttore se ci sono solo due casi (per uno dei quali non si farà nulla).

es. se definiamo

public sealed class Result {
  private Result(string msg) { ErrorMessage = msg; }
  public static Result Valid => new Result(null);
  public static Result Invalid(string msg) => new Result(msg);

  public string ErrorMessage { get; }
  public bool IsValid => ErrorMessage == null;
}

Possiamo usare un validationResult come

if (!validationResult.IsValid)
  Console.WriteLine($"Invalid name: {validationResult.ErrorMessage}");

o

if (validationResult.ErrorMessage is string msg)
  Console.WriteLine($"Invalid name: {msg}");
    
risposta data 21.07.2018 - 14:37
fonte
1

Nel tuo esempio concreto, la corrispondenza del modello può essere implementata con funzionalità polimorfiche di OO.

public interface IValidationResult
{
    void Print();
}

public class FailedValidation : IValidationResult
{
    private readonly string _errorMessage;

    public FailedValidation(string errorMessage) => _errorMessage = errorMessage;

    public void Print() => Console.WriteLine(_errorMessage);
}

public class SuccessValidation : IValidationResult
{
    public void Print() => {}; // do nothing
}

// Validation
public class Validator
{
    public IValidationResult Validate(string message)
    {
        if (string.IsNullOrEmpty(message))
        {
            return new FailedValidation("Invalid name");
        }

        return new SuccessValidation();
    }
}

Quindi il tuo codice utente apparirà semplice e senza logica di condizioni aggiuntive.

static void Main(string[] args)
{
    var name = args[0];
    var result = new Validator().Validate(name)
    result.Print();
}
    
risposta data 22.07.2018 - 08:14
fonte

Leggi altre domande sui tag