Para el que no leyó los articulos antes citados, básicamente la discusión ronda sobre la mejor ubicación (en namespaces) para declarar un método extensor. Por ejemplo, veamos el ejemplo usado en los artículos anteriores que sirve para determinar si un valor se encuentra dentro de un rango determinado:
namespace CustomExtensions { public static class ExtensionMethods { public static bool Between<T>(this T @value, T min, T max) where T : IComparable<T> { return @value.CompareTo(min) >= 0 && @value.CompareTo(max) <= 0; } } }En este caso, el método "Between" se encuentra ubicado dentro de un namespace con el nombre "CustomExtensions", lo que provocará que al tratar de usarlo, tengamos primeramente que ponerlo en uso de la siguiente forma:
using CustomExtensions; ... var x = 10; Console.WriteLine(x.Between(5, 15) ? "yes" : "no");Obviamente, si no ponemos en uso el namespace "CustomExtensions", el método "Between" no podrá ser utilizado en nuestro código a menos que nos encontremos en una clase ubicada en el mismo namespace. En el primero de los artículos referenciados al inicio de este post, el autor propone tres formas de lidiar con esto, y se decanta por la variante de ubicar las extensiones en el mismo namespace de la clase a la que están extendiendo, en este caso, en el namespace System.
Particularmente no soy partidario de esta solución.
Veamos primeramente cuál es el "problema" en cuestión, y cito del artículo de Lluís Franco (las negritas son mías):
...si deseamos evitar declarar el using (tenéis que pensar que este método extensor lo podéis reutilizar en 1000 proyectos distintos)...¿Por qué motivo vamos a querer evitar declarar el using? El que tengamos que utilizar el método extensor en 1,000 proyectos diferentes no creo sea una razón válida para no crear nuestro propio namespace, sino todo lo contrario. Creo que esta posibilidad reafirma aún más la necesidad de definir correctamente el espacio de nombres en el cual ubicaremos nuestro método. Veamos qué dice Microsoft sobre los namespaces:
The namespace keyword is used to declare a scope. This namespace scope lets you organize code and gives you a way to create globally unique types.En otras palabras, un namespace nos permite organizar nuestro código y crear tipos de datos globalmente únicos, evitando colisiones de nombres cuando usamos bibliotecas de terceros. Imaginemos por un instante que Microsoft decidiera comenzar a colocar todas sus clases en el namespace System, para evitar de esta manera que los desarrolladores tengan que importar (o usar) los namespaces correspondientes. ¿Tiene sentido? Obviamente no.
¿Qué sucedería si estamos usando una biblioteca cualquiera en la cuál sus desarrolladores decidieron crear una clase llamada "Persona" y colocarla en el namespace System? Supongamos que nosotros mismos necesitamos crear una clase con el mismo nombre, y "para evitar declarar el using", la queremos colocar también en el namespace System. Colisión de nombres... o dejamos de usar la biblioteca, o ubicamos nuestra clase "Persona" en un namespace diferente.
¿O qué tal si el propio Microsoft decide incluir un método extensor "Between" en su próxima versión del Framework? ¿Qué sucede con nuestro código? Obviamente no compilará, así que tendremos que cambiar el namespace e incluir todas las referencias del mismo donde quiera que lo estemos usando.
Otro punto es precisamente la afirmación de que incluyendo el método extensor en el namespace System evitará tener que declarar el using correspondiente. Esto no tiene tampoco por qué ser realidad. Todos los namespaces hay que ponerlos en uso utilizando la palabra reservada "using" a menos que nos encontremos dentro del propio namespace, lo que sucede es que, en el caso de una aplicación de Windows Forms (como el ejemplo del post referenciado), el namespace System ya estará por defecto en uso, así que no habría que declarar nada para utilizar el método extensor. Sin embargo, ¿qué sucede cuándo vayamos a usar el método extensor en una nueva clase? ¿O qué sucede cuando no estemos utilizando Visual Studio y por lo tanto nadie incluya automáticamente el namespace System?
Por último, de acuerdo con su objetivo de organizar el código, el uso correcto de los namespaces le indica al desarrollador de dónde viene cada clase utilizada. Esta información puede ser útil en muchos sentidos, y si decidimos empezar a colocar cosas en el namespace System, el código confundirá a más de uno. Simplemente no habría forma de determinar de dónde sale el método "Between": ¿es este un método del propio Framework o está definido en cualquier otra biblioteca? El hecho de que solamente se use "using System" me indica que Between es una extensión de Microsoft incluída en el framework, ¿o no?
Resumiendo, creo que hay demasiados puntos en contra de este criterio como para comulgar con él, pero me encanta el hecho de que cada cuál tenga su propia visión al respecto y la exprese abiertamente. A veces cosas que nos parecen tan sencillas y comúnes como la definición de los espacios de nombres, pueden volverse complicadas y decisivas en un buen diseño. Discutir al respecto y oír criterios dispares es el único modo de encontrar la solución adecuada.
Creo que es una solución muy simplista asumir que el namespace System estará ahí siempre (implícito), y todo para qué? para evitarnos un "using"?
ReplyDeleteNo creo que valga la pena, por el contrario, creo que declarar explítamente el "using" le aporta más metadatos al código y por ende legibilidad.
Buenas!
ReplyDeleteBuen post! :D
Sólo una puntualización...
"¿O qué tal si el propio Microsoft decide incluir un método extensor "Between" en su próxima versión del Framework? ¿Qué sucede con nuestro código? Obviamente no compilará"
Eso no es exactamente así: nuestro código compilará igualmente, con independencia de en que namespace esté el método Between (System u otro), y en ambos casos el comportamiento será el mismo (en código anterior me refiero).
Pueden suceder dos cosas:
1) Que el método Between de MS tenga la misma firma que el nuestro de extensión
2) Que el método Between de MS tenga una firma distinta.
En el primer caso se ejecutará SIEMPRE el método de MS: En la resolución de métodos, los de extensión están en último lugar. Eso es, el compilador sólo los llama SI NO encuentra ningún otro método antes (de la propia clase o heredado).
En el segundo caso se ejecutará el de MS o el nuestro de extensión en función de los parámetros exactos. De nuevo, el compilador INTENTARÁ que la llamada sea tratada por los métodos de la propia clase (llevando a cabo si fuese necesario todas las conversiones implícitas que le sean posibles). Si las conversiones implícitas no permiten convertir de nuestros parámetros a los esperados por el método Between de la clase, entonces el compilador usará el método de extension.
Un saludo!
Eduard, gracias por tus comentarios, pero en realidad el código no compilará. Aquí tienes un ejemplo:
ReplyDeleteCrea una nueva clase en un proyecto cualquiera de la siguiente manera:
namespace System.Linq
{
static class Extensions
{
public static IEnumerable Take(this IEnumerable source, int count)
{
return null;
}
}
}
Básicamente estarás implementando una extensión que ya existe en el namespace System.Linq. La implementación del método no es importante, así que estoy retornando NULL.
Ahora trata de usar la extensión en otra clase cualquiera:
static void Main(string[] args)
{
Console.WriteLine("svpino".Take(3));
Console.ReadLine();
}
El proyecto no compilará y el error mostrado será el siguiente:
The call is ambiguous between the following methods or properties: 'System.Linq.Extensions.Take(System.Collections.Generic.IEnumerable, int)' and 'System.Linq.Enumerable.Take(System.Collections.Generic.IEnumerable, int)'
El compilador no sabe a cuál de los dos métodos extensores llamar, el del framework o el nuestro. En este caso usamos System.Linq como namespace para ambos, pero pudiera ser cualquier otro.
Ehhmm...
ReplyDeleteVale, perdona.
No te compila pero es porque en este caso la duda está ENTRE DOS MÉTODOS DE EXTENSIÓN. Recuerda que Linq también son métodos de extensión :)
En estos casos, tienes razón no compila.
Pero si MS añadiese el método Take, dentro de la interfaz IEnumerable, entonces el código compilaría (y ese sería el que usaría en primer lugar además). A eso me refería con mi comentario! :D
Un saludo!
pd: Descubrí tu blog "por casualidad" gracias a la entrada en el blog de lluis franco, y la verdad es que me está pareciendo muy bueno! :)
Eduard, entendido tu ejemplo. En el post yo me estoy refiriendo a las extensiones y es por ello que puse que el código no compilaría.
ReplyDeleteMuchisimas gracias por tus comentarios sobre el blog. Me alegro mucho de tenerne entre los seguidores.