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?