Algunas particularidades respecto a la inicialización y validación en setters
Introducción
La semana pasada, en la serie de entradas que sobre la especificación de C# 9 he hecho en mi blog, publiqué una entrada sobre C# 9.0 – Specification – Init-only Setters.
En LinkedIn, Eduard Tomás me hizo unas apreciaciones sobre dicha entrada que copio/pego aquí:
A mi lo que me chirría muchísimo de esa entrada es lo de lanzar una excepción en el getter porque la propiedad no ha sido inicializada.
En este caso creo que la solución es tan sencilla como usar un constructor que obligue a introducir un nombre. De hecho para eso son los constructores: para asegurar que la creación de un objeto es correcta.
Mi propósito aquí, es el de a partir del comentario de Eduard, responder a estas dudas, explicando o aclarando mi punto de vista al respecto, así como algunos aspectos que a mi juicio son bastante interesantes repasar y tener en cuenta para que no queden dudas al respecto.
¡Vamos allá!
Validación en constructor vs otro sitio
Efectivamente, lo recomendado, habitual y más sensato, es inicializar y validar o verificar siempre el dato o datos de entrada en el constructor de la clase.
Yo lo hago así y casi nunca lo hago en otro sitio.
Te ahorras que el flujo de aplicación continúe hasta llegar a las propiedades y que éstas lancen una excepción o devuelva un valor que no es el que deseamos que devuelva,… eso, o sino no nos queda más remedio que forzar en otros puntos de nuestro código la validación, y por lo tanto la excepción, escribir en log, devolver una respuesta que informe de ello, etc.
La idea de hacer las validaciones en el constructor es la de cortar el flujo de ejecución lo antes posible, es decir, ser proactivo en lugar de reactivo.
Que quede claro que validar lo puedes hacer donde consideres oportuno, pero hacerlo fuera del constructor, debería estar justificado, y raramente suele ser así. Así que coincido con Eduard en lo de «chirriar«.
Analizando el ejemplo de Init-only Setters
Sin embargo, en el ejemplo que publiqué de la entrada de especificación de C# 9.0 sobre Init-only Setters, la clase de ejemplo no tenía constructor.
Así que la pregunta de Eduard y esta entrada que estoy desarrollando, sirven para explicar que aunque lo razonable hubiera sido quizás crear una clase con constructor,… no lo hice… ¿porqué?.
Pues porque quería aplicar esa validación en el init, para demostrar que dentro del init se podía meter lógica como en el set.
Mirándolo con perspectiva y precisión quirúrgica, creo que aunque ese fuera mi propósito y se explique bien el uso y funcionamiento de init, es muy posible que el ejemplo que propuse no fuera el idóneo, y podría mal interpretarse y generar confusión, algo que aprovechando esta situación, estoy tratando de aclarar en esta entrada.
Explicando el flujo de ejecución de una clase
Siempre que instanciamos una clase, la primera parte que se ejecuta es el constructor.
Posteriormente se empiezan a inicializar y declarar el resto de variables, propiedades, etc.
Por eso, la aproximación que hacía Eduard de validar en el constructor es correcta y es como digo, lo ideal. Ahora bien, si no validamos ahí, y queremos validar, tendremos que hacerlo en otro sitio.
En mi caso, al tener un constructor vacío, no me quedaba otra que hacerlo en otro sitio, pero es algo que habría quedado resuelto en el constructor. Lo hice, como explicaba anteriormente, para «forzar» ese tratamiento en la propiedad y demostrar que el init se comporta como el set en cuanto a la posibilidad de meter lógica dentro.
Pero volviendo al ejemplo y su flujo, vamos a repasar (sobre todo para los desarrolladores menos experimentados) algo interesante del mismo que aclare todo esto, y demuestre la importancia de hacer las validaciones en el constructor.
Inicialización y validación
Voy a partir del siguiente código completo:
using System; public class Program { public static void Main() { var data = "Hi!"; var personWithoutParameters = new Person() { Name = data }; Console.WriteLine($"Value: '{personWithoutParameters.Name}'"); Console.WriteLine(); var personWithParameters = new Person(data); Console.WriteLine($"Value: '{personWithParameters.Name}'"); } } public class Person { public Person() { Console.WriteLine($"Constructor of {nameof(Person)}"); if (String.IsNullOrWhiteSpace(Name)) Console.WriteLine("IS NULL OR EMPTY"); } public Person(string name) { Console.WriteLine($"Constructor of {nameof(Person)} with parameters"); if (String.IsNullOrWhiteSpace(name)) Console.WriteLine("IS NULL OR EMPTY"); Name = name; } private string _name; public string Name { get { Console.WriteLine($"{nameof(Name)} - Get Value"); return _name; } init { Console.WriteLine($"{nameof(Name)} - Set Value"); _name = value; } } }
En este código tenemos una clase Person, que tiene dos constructores.
Uno vacío como el que dejé en el ejemplo que publiqué, y otro con un parámetro de entrada name.
En la parte superior del código dentro de Main, he puesto el típico código de una aplicación de consola que hará una llamada a la clase sin parámetros en el constructor, y otra con parámetros en el constructor.
var data = "Hi!"; var personWithoutParameters = new Person() { Name = data }; Console.WriteLine($"Value: '{personWithoutParameters.Name}'"); Console.WriteLine(); var personWithParameters = new Person(data); Console.WriteLine($"Value: '{personWithParameters.Name}'");
Nótese también, que en la llamada a la clase sin parámetros en el constructor, indicaremos el valor de Name.
var personWithoutParameters = new Person() { Name = data };
Ejecutando este código observamos varias cosas.
Por un lado y respecto a la primera parte de ejecución que corresponde con personWithoutParameters, el resultado del flujo de ejecución es:
(1) Constructor of Person (2) Name - Get Value (3) IS NULL OR EMPTY (4) Name - Set Value (5) Name - Get Value (6) Value: 'Hi!'
Y su flujo al llamar a Person() es el que se indica en la siguiente imagen:
Es decir, primero se ejecuta su constructor, y posteriormente y cuando ha terminado éste, se establece el valor de la propiedad Name. Por eso, dentro del constructor, Name vale null or empty pese a que la estamos inicializando al mismo tiempo que creamos Person.
Por otro lado y respecto a la segunda parte de ejecución que corresponde con personWithParameters, el resultado del flujo de ejecución es:
(1) Constructor of Person with parameters (3) Name - Set Value (4) Name - Get Value (5) Value: 'Hi!'
Y su flujo al llamar a Person(string name) es el que se indica en la siguiente imagen:
Es decir, primero se ejecuta su constructor, se valida sus argumentos de entrada, y posteriormente se establece el valor de la propiedad Name dentro del propio constructor. Y aquí validamos el parámetro de entrada, el cuál ya no sería necesario validar en el caso de init.
Si los argumentos de entrada no son los esperados, actuaremos proactivamente avisando (2), levantando una excepción, etc., pero en este caso, el paso (2) no se ejecuta porque estamos asegurando/validando el valor de entrada que viene en el constructor.
Conclusiones sobre init y validación
Dicho todo esto, lo interesante de init es que, con independencia de validar en el constructor o en otro sitio, cuando indicamos init a una propiedad y dentro de nuestro código intentamos hacer algo como person.Name = «», el compilador nos va a devolver un error de compilación.
Y por otro lado y de acuerdo a los expuesto anteriormente, esa propiedad que nos da error de compilación, sería candidata idónea de ser agregada en el constructor de la clase, y por lo tanto, validada allí.
Como regla mnemotécnica y de forma recomendada o ideal, cada propiedad init debería ser inicializada y validada en el constructor.
Happy Coding!