Error codes
First I have to say that I do not like very much the solution that returns the error codes. It is not totally wrong and it is better to launch exception, which many programmers would do because they follow cake recipe. I liked the creativity. But such numerical codes are bad to identify what the error really is. In a simple example and learning is ok. But in production code it’s not much.
This enumeration could indicate the type of invalidity that occurred and not in which property it occurred. For this you would need to modify the validation a little.
I repeat, it is acceptable to use it like this, but I would change to at least use an enumeration instead of meaningless numbers.
Or create classes of invalidities that are similar to exceptions but without having the exception.
Until a return string error would be better than returning a numeric code. Although not in all scenarios.
I created a question and answer pair about this, which is one of my posts that I like the most.
Modularization
The form used to validate is not wrong, but it does not follow the precepts of modularization, of object orientation, as some like. Each validation should be in its own method.
There are even those who argue in some cases that individual validations should be in their own classes and not in a single class. This may be an exaggeration or not. Doing this makes it easier to exchange validations. You can use Strategy Pattern, for example. Perhaps other patterns may be needed to compose the creation of the object properly.
But you need to ask yourself if this is necessary, as well as if it is necessary to have the ModelPessoa
. There must be reasons to increase the complexity of the application.
It’s not absurd to have it, but if you can’t justify it, you’re overengineering it. The fact that you change one class, force another, is not always a good idea. But there are cases that you can’t get away from without disorganizing the code. It seems to me that Pessoa
is already yours model.
What many people do, and it can be right or wrong depending on the context, is to create classes for property with everything it needs, each property is self-sufficient in its own kind. This is usually considered an anti-pattern that I don’t remember the name now. It makes some sense, but it can be exaggerated, especially if you’re doing it without being aware of the real need.
This form can be advantageous for leaving all that pertains to that property close by. But it would have to make a change that would make sense even if the validation is within ModelPessoa
or within Pessoa
. Validation made through a method by property could encapsulate everything that would need to be validated for that property, and would have one place to change when something in the property changes.
On the other hand, validation could be included within the property, specifically within the set
. Of course, there is a semantic difference between doing this and having a separate method. Inside the property it can never be in invalid state, and you may need it eventually. In addition to what you are restricted to report the invalidity by exception. I still think the best solution is to have validation methods. You have more control.
But not all validation can be done only on the property. There are global validations that complicate this scenario of approximating validation to its state.
Attributes
Another way to bring the validation closer to the property without creating individual classes for each property is to use attributes. For this you need a library of them.
Flawed method
This form used in the question is very bad because if the first property is invalid, it will report this problem and no other. It works, but gives a bad experience. It would have to return a list with all the errors, or a numeric code that operates as flags booleans to indicate which errors occurred at each bit, or completely change the way to handle validation.
A yield
could be a solution, but it doesn’t solve at all.
Constructor and startup
As a last parallel note, since I know you want to learn right, if you are using C#6, the builder is unnecessary since it allows initializing properties. Even in earlier versions it can be replaced by initializations within the class, but the code would get bigger because it would have to stop using the default property and initialize the private field it encapsulates. To ModelPessoa
certainly does not need the builder.
Library
Now coming to the direct subject of the question, you could create a library of validations that often happen. But it would have to work with a parameter that would be the property to be validated. It would not work with the whole object. There’s even a whim, but it wouldn’t do any good.
This can be useful for maintenance including because if there is a different way to do that specific validation, changed in the library, changed to the entire application.
This is called abstraction. Let’s say we started using IsEmpty()
to validate. Then one day discovers that the IsNullOrEmpty()
was better. Imagine having to search for every application and change it. A search global can help but would need to manually analyze each case, not everything would be validation, somewhere may have done the same kind of validation in a different way. If you’re abstracted/encapsulated in one place it’s easy to solve.
Another example: if you change the age of majority. Or if the check is bug. This information should be in only one place. And there may be different majorities for each case, all this has to be thought out. Almost no one thinks about these things. When you think about it, it’s usually too late, the software is already all disorganized. Programming is very difficult and so there are so many software full of bugs. It is common, even the best programmers, can not see the whole problem. Of course the more experienced know how to create easy escapes without generating complication.
This is to do DRY, which is one of the most important things in programming.
Will it give a very big gain in simple validations like this, as the question asks? No. But DRY, abstraction, does not exist to make the code smaller. It determines that the change is needed at only one point.
The gain can be greater in more complex validations. But these tend to be less reusable.
Frameworks
This library can be made in attributes form. But its use starts to make sense in frameworks. Because validation will not occur alone. Some mechanism will have to ensure that it is done. This can be obtained in a framework existing, or one you will create. Already exists something more or less ready in . Net. I am not saying that it is the definitive solution and that you should adopt it without thinking. Everything has its problems.
The framework can automate validation checking. You can pick up error messages, for example. Although if you change the error code to an error class, this can already be improved, then you would have something like this:
Console.WriteLine(validacao.MessageInvalidation);
This is a beautiful generalization. In this case it would have an error class that one of the error properties would be MessageInvalidation
.
It can make a loop to perform all the necessary validations and this is hidden inside it.
But make a framework is not simple task, in general inexperienced people make so many mistakes that can bring more harm than benefit.
And framewoks poorly made can plaster the application.
Each user interface may need one framework different, even when the user is indirect.
Note that this helps the consumption of validations and not their creation, which is the focus of the question.
Even if you use a library, this cannot replace an abstraction. After all you have to have a way to change the validations individually canonically.
Variant validations
One time you start to encounter more complex problems, which would even make it impossible to validate the solution within the property and even attributes. There are cases where the validation of the object creation is different from the object change, and even depends on the type of change. Think about this. Any generic solution has to consider all possible scenarios.
And the validations that are interdependent. Have you thought about this?
In general people have a lot of difficulty in creating generic software. And when they go to make a mistake, they miss not only by forgetting certain aspects, but also by abstracting unnecessary things, generating complexity for nothing. This is reflected in the use of the object-oriented paradigm. It is very difficult to get the point right, so I usually say that almost nobody knows how to program in this paradigm. Even I think about all this mistake enough. Imagine who can’t handle it.
Design Pattern
The closest pattern I can think of that might help a little bit, but that doesn’t mean I’m going to eliminate repetitions, it’s the Specification Pattern. But if you abuse it, it can become a burden. This can give another look at how validation can be done.
Another that can be used, is basically what the attributes do, is the Decorator Pattern. Another is the Visitor Pattern. They and the Strategy I think it’s better than creating this ModelPessoa
in some scenarios. But again, it may be Overkill.
Extension methods
can be used to implement these standards, or something close to them, in a simpler way.
And other patterns can be used together according to every need.
Completion
I see ways to better organize the code, better prepare it for maintenance, but I see little chance of making it less repetitive. Not least because I didn’t see repetition there. Maybe I missed something. Repetition is more in the validation consumption, not in the validation itself.
A article that can give some ideas if you need a complex solution. I don’t like everything about her, but it helps to understand what can be done.
Practical example
Note that this example was done running, without testing too much - it has several errors, and is not even close to trying to show the best way. It just shows one possible way, without thinking too much about all the consequences and needs. The example can be used to have ideas, but not as a recipe. In fact I did a lot of things that might be unnecessary, I did just to show different scenarios.
In real code I would certainly do a little different. In test code, simple, I wouldn’t do any of this. This code was meant to show a middle ground between a fictional example and a real-world example. When we do real applications, certain logics cannot be applied. Deleting repetition is great, but things are not that repetitive. When we generalize something to be aware of what we are doing, sooner or later we will have problems.
The code is a simplification. I can imagine a huge list of improvements, and I’ve found some bugs.
using System;
using static System.Console;
using System.Collections.Generic;
public class Pessoa {
public string Nome { get; set; } = "";
public string Endereco { get; set; } = "";
public int Idade { get; set; } = 0;
public string Cargo { get; set; } = "";
public decimal Salario { get; set; } = 0M;
}
public class ModelPessoa : Model<Pessoa> { //adotei a ideia que o Model seria para validar mesmo.
override public bool Validate() {
Required("Nome", Value.Nome);
ValidaEndereco();
Required("Cargo", Value.Cargo);
Custom("Salário", "SalarioValido", "O Salário não atende os requisitos", ValidaSalario);
Range("Idade", Value.Idade, Util.GetMaioridade(), 120);
return (IsValid = Errors.Count == 0);
}
public int AsIdade(string texto) { //isto não é tão necessário, mas quis mostrar esta possibilidade
var idade = 0;
if (int.TryParse(texto, out idade)) {
return idade;
} else {
Errors["IdadeInconsistente"] = new Invalidation("Idade", "A Idade foi digitada de forma inconsistente");
return 0;
}
}
public decimal AsSalario(string texto) { //isto funciona como uma abstração/encapsulamento da funcionalidade
var salario = 0M;
if (decimal.TryParse(texto, out salario)) {
return salario;
} else {
Errors["SalarioInconsistente"] = new Invalidation("Salario", "O Salário foi digitado de forma inconsistente");
return 0;
}
}
private void ValidaEndereco() { //tem a vantagem de que pode mudar a regra facilmente tendo um método isolando a funcionalidade
if (!Util.IsStandardAddress(Value.Endereco)) {
AddError("EnderecoPadrao", "Endereço", "EnderecoPadrao", "O Endereço está em formato inválido.");
}
}
private bool ValidaSalario(object[] values) { //feito para operar com o Custom, pode mudar as regras fácil aqui de forma canônica
return ((!Errors.ContainsKey("IdadeInconsistente") && Value.Idade > 21 && Value.Salario > 1000M)) ||
(Value.Cargo != "Gerente" && Value.Salario > 900M) ||
(Value.Cargo == "Gerente" && Value.Salario > 1200M);
}
}
public static class Util { //esta classe foi só para agrupar, em código real estes métodos estariam em outras classes
public static string Read(string label) {
Write(label);
return ReadLine();
}
public static int GetMaioridade() {
return 18; //aqui poderia estar pegando de um banco de dados ou arquivo de configuração
}
public static bool IsStandardAddress(string address) {
return address.Length > 2 && (address.Substring(0, 3) == "Rua" || address.Substring(0, 3) == "Av.");
}
}
public class Program {
public static void Main(string[] args) {
while (true) {
var mp = new ModelPessoa(); //o modelo é usado para trabalhar com dados temporários
mp.Messages["Idade"] = "A pessoa precisa ser maior de idade"; //personalizando uma mensagem
mp.Value.Nome = Util.Read("Nome: "); //lê o dado e guarda no modelo
mp.Value.Endereco = Util.Read("Endereço: ");
mp.Value.Idade = mp.AsIdade(Util.Read("Idade: ")); //tentando converter
mp.Value.Cargo = Util.Read("Cargo: ");
mp.Value.Salario = mp.AsSalario(Util.Read("Salário: "));
if (mp.Validate()) {
Pessoa funcionario = mp.Value; //se está válido, então pode jogar em um objeto definitivo, gravar em DB, etc
WriteLine("Cadastro efetuado!");
break;
} else {
WriteLine("Erros ocorreram!");
foreach(var erro in mp.Errors) { //tem várias formas para mostrar os erros
WriteLine(erro.Value);
}
}
}
}
}
public class Invalidation { //uma classe simplificada para guardar dados completos sobre uma invalidade, parecido com exceção, sem ser
public string Data { get; private set; }
private string message;
public string Message {
get {
return message ?? "Dado inválido";
}
}
public Invalidation(string data, string message = null) {
Data = data;
this.message = message;
}
public override string ToString() {
return Message;
}
}
public abstract class Model<T> where T : new() { //mecanismo básico de validação separado do modelo em si. Tem maneiras mais simples de fazer o controle de erros
public T Value { get; private set; } = new T();
public bool IsValid { get; protected set; } = false;
public Dictionary<string, Invalidation> Errors { get; protected set; } = new Dictionary<string, Invalidation>();
public Dictionary<string, string> Messages { get; set; } = new Dictionary<string, string>();
public abstract bool Validate();
protected void Required(string field, string value) { //alguns exemplos de códigos genéricos de validação
if (string.IsNullOrEmpty(value)) {
AddError(field + "Required", field, "Required", "O campo {0} é obrigatório.");
}
}
protected void Range(string field, int value, int min, int max) {
if (value < min || value > max) {
AddError(field + "Range", field, "Range", "O campo {0} deve estar na faixa de {1} até {2}.", min, max);
}
}
protected void Custom(string field, string messageKey, string messageAlt, Func<object[], bool> condition, params object[] values) {
if (!condition(values)) {
if (values != null && values.Length > 0) {
AddError(field + messageKey, field, messageKey, messageAlt, values?[0], values?[1], values?[2]); //está porco, mas não vou gastar tempo
} else {
AddError(field + messageKey, field, messageKey, messageAlt);
}
}
}
protected void AddError(string errorKey, string field, string messageKey, string defaultMessage, object value1 = null, object value2 = null, object value3 = null) {
var message = "";
Errors[errorKey] = new Invalidation(field, string.Format(Messages.TryGetValue(messageKey, out message) ? message : defaultMessage, field, value1, value2, value3));
}
}
Behold working in the ideone. And in the .NET Fiddle. Also put on the Github for future reference.
If I have time and the AP thinks it’s good, I can see if I can make a version like framework using attributes and automation and generalization of some tasks. I do not promise.
Two comments that have nothing to do with what you ask: "there is" of the verb "there" ("Checks the content in the variable." ) and "if (employee.Age < 18)" instead of "if (employee.Age <= 18)".
– Alexandre Cartaxo
Thanks, I didn’t notice that.
– gato
You are using ASP.NET MVC?
– Leonel Sanches da Silva
I’m using a layered architecture, but it’s very similar to MVC, but the focus of this layered architecture is desktop applications.
– gato
Related: Using client validation is sufficient?
– Laerte
@Laerte the system I’m developing is for desktop.
– gato