ASP.NET MVC–Recibir contenido en mayúsculas

Imagina que estás desarrollando un proyecto, con ASP.NET MVC y cuando llevas digamos unas, no sé, cincuenta pantallas, todas llenas con sus formularios, algunos que hacen peticiones AJAX, otros que no… bueno, imagina que cuando llevas ya bastantes vistas hechas, aparece un nuevo requisito, de aquellos que están agazapados, esperando el momento propicio para saltate a la yugular: Todos los datos que entre el usuario, deben ser guardados y mostrados en mayúsculas.

Hay varias técnicas que puedes empezar a usar: Podríamos usar text-transform de CSS para transformar lo que ve el usuario en mayúsculas. Pero eso no sirve porque los datos que el servidor recibiría serían tal y como los ha entrado el usuario (text-transform cambia solo la representación pero no el propio texto).

Otra opción, claro está, es modificar todos los viewmodels porque al enlazar las propiedades las conviertan en mayúsculas. Así, si el usuario entra “edu” en la propiedad Name de un viewmodel, el viewmodel lo convierte a EDU y listos. Pero claro… si tienes muchos viewmodels ya hechos, eso puede ser muy tedioso. Y si no los tienes, pero vas a tenerlos, pues también…

En este dilema estaba un compañero de curro, cuando me planteó exactamente dicho problema. La cuestión era si había una manera que no implicase tener que tocar todos los viewmodels, para asegurar que recibíamos los datos siempre en mayúsculas. Y por suerte, la hay.

Si conoces un poco como funciona ASP.NET MVC internamente, sabrás que hay dos grupos de objetos que cooperan entre ellos para traducir los datos de la petición HTTP a los parámetros de la acción del controlador (que suele ser un viewmodel en el caso de una acción invocada via POST). Esos dos grupos de objetos son los value providers y los model binders.

La idea es muy sencilla: Los value providers recogen los datos de la petición HTTP y los dejan en un “saco común”. Los model binders recogen los datos de ese “saco común” y con esos datos recrean los valores de los parámetros de los controladores. De esa manera los value providers tan solo deben entender de la petición HTTP y los model binders solo deben entender como recrear los parámetros del controlador. Separación de responsabilidades.

Hay varios value providers porque cada uno de ellos se encarga de una parte de la petición HTTP. Así uno se encarga de los datos en la query string, otro de los form values (datos en el body usando application/x-www-form-urlencoded), otro de los datos en json… Y hay varios model binders, porque clases específicas pueden tener necesidades específicas de conversión. Aunque el DefaultModelBinder que viene de serie puede con casi todo, a veces es necesario crearse un model binder propio para suportar algunos escenarios.

En este caso la solución pasa por usar un value provider nuevo. Un value provider que recogerá los datos de la petición HTTP y los convertirá a maýsuculas antes de enviarlos a los model binders. Con eso los model binders recibirán los datos ya en mayúsculas, como si siempre hubiesen estado así. Solucionado el problema.

Veamos el código brevemente.

Lo primero es crear la factoría que cree el nuevo value provider. Los value providers se crean y se eliminan a cada petición, y el responsable de hacerlo es una factoría, que es el único objeto que existe durante todo el ciclo de vida de la aplicación:

  1. public class ToUpperValueProviderFactory : ValueProviderFactory
  2. {
  3.     private readonly ValueProviderFactory _originalFactory;
  4.     public ToUpperValueProviderFactory(ValueProviderFactory originalFactory)
  5.     {
  6.         _originalFactory = originalFactory;
  7.     }
  8.     public override IValueProvider GetValueProvider(ControllerContext controllerContext)
  9.     {
  10.         var provider = _originalFactory.GetValueProvider(controllerContext);
  11.         return provider != null ? new ToUpperProvider(provider) : null;
  12.     }
  13. }

La idea es muy sencilla: dicha factoría delega en la factoría original para obtener el value provider. Y luego devuelve un ToUpperProvider que va a ser nuestro value provider propio:

  1. public class ToUpperProvider : IValueProvider
  2. {
  3.     private readonly IValueProvider _realProvider;
  4.     public ToUpperProvider(IValueProvider realProvider)
  5.     {
  6.         _realProvider = realProvider;
  7.     }
  8.     public bool ContainsPrefix(string prefix)
  9.     {
  10.         return _realProvider.ContainsPrefix(prefix);
  11.     }
  12.     public ValueProviderResult GetValue(string key)
  13.     {
  14.         var result = _realProvider.GetValue(key);
  15.         if (result == null)
  16.         {
  17.             return null;
  18.         }
  19.         var rawString = result.RawValue as string;
  20.         if (rawString != null)
  21.         {
  22.             return new ValueProviderResult(rawString.ToUpperInvariant(),
  23.                 result.AttemptedValue.ToUpperInvariant(),
  24.                 result.Culture);
  25.         }
  26.         var rawStrings = result.RawValue as string[];
  27.         if (rawStrings != null)
  28.         {
  29.             return new ValueProviderResult(
  30.                 rawStrings.Select(s => s != null ? s.ToUpperInvariant() : null).ToArray(),
  31.                 result.AttemptedValue.ToUpperInvariant(),
  32.                 result.Culture);
  33.         }
  34.         return result;
  35.     }
  36. }

La clase ToUpperProvider implementa la interfaz IValueProvider, pero delega en el value provider original.

Lo único que hace es, en el método GetValue, una vez que tiene el valor original obtenido por el value provider original convertirlo a mayúsculas. Tratamos dos casuísticas: que el valor sea una cadena o un array de cadenas.

¡Y listos!

El último paso es configurar ASP.NET MVC. P. ej. para hacer que todos los datos enviados via POST usando form data (es decir, un submit de form estándard) se reciban en mayúsculas basta con hacer (en el Application_Start de Global.asax.cs):

  1. var old = ValueProviderFactories.Factories.OfType<FormValueProviderFactory>().FirstOrDefault();
  2. if (old != null)
  3. {
  4.     ValueProviderFactories.Factories.Remove(old);
  5.     ValueProviderFactories.Factories.Add(new ToUpperValueProviderFactory(old));
  6. }

Con eso sustituimos la factoría original que devuelve el value provider que se encarga de los datos en el form data por nuestra propia factoría.

Para probarlo basta con crear una vista con un formulario y hacer un post normal y corriente… y verás que todos los datos que se entren se pasarán a mayúsculas automáticamente 🙂

Saludos!

ASP.NET MVC–Descargar ficheros con soporte para continuación – ii

En el post anterior vimos todo la parte teórica de HTTP que nos permite realizar descargas de ficheros, pausarlas y continuarlas. Pero ahora viene lo bueno… Vamos a implementar el soporte en el servidor para soportar dichas descargas.

Dado que nuestro amado FileStreamResult no soporta la cabecera Range, nos va a tocar a nostros hacer todo el trabajo. Pero, la verdad… no hay para tanto.

Nota importante: Todo, absolutamente todo el código que pongo en este blog está para que hagas con él lo que quieras. Pero del mismo modo, el código que hay en este blog NO ES CÓDIGO DE PRODUCCIÓN. En muchos casos no está suficientemente probado y en otros, para simplificar, no se tienen en cuenta todas las casuísticas o no hay apenas control de errores. Si quieres hacer copy/paste eres totalmente libre de hacerlo, pero revisa luego el código. Entiéndelo y hazlo tuyo.

Bien, el primer paso va a ser crearnos un ActionResult nuevo, en este caso una clase llamada RangeFileActionResult y que herederá pues de ActionResult. En dicho ActionResult vamos a obtener el valor del campo Range y a parsearlo. Empecemos por el constructor:

  1. public RangeFileActionResult(string filename, string contentType)
  2. {
  3.     _stream = new FileStream(filename, FileMode.Open, FileAccess.Read);
  4.     _length = _stream.Length;
  5.     _contentType = contentType;
  6.  
  7.     byte[] hash;
  8.     using (var md5 = MD5.Create())
  9.     {
  10.         hash = md5.ComputeHash(_stream);
  11.     }
  12.     _etag = Convert.ToBase64String(hash);
  13.     _stream.Seek(0, SeekOrigin.Begin);
  14. }

Siplemente nos guardamos el stream al fichero y calculamos el MD5. Vamos a usar el MD5 como ETag del fichero.

Ahora, el siguiente paso es implementar el método ExecuteResult, que es donde se procesa toda la petición:

  1. public override void ExecuteResult(ControllerContext context)
  2. {
  3.     using (_stream)
  4.     {
  5.         var ranges = ParseRangeHeader(context.HttpContext.Request);
  6.         if (ranges == null || ranges.Empty)
  7.         {
  8.             new FileStreamResult(_stream, _contentType).ExecuteResult(context);
  9.             return;
  10.         }
  11.         if (ranges.Multiple)
  12.         {
  13.             ProcessMultipleRanges(context, ranges);
  14.         }
  15.         else
  16.         {
  17.             ProcessSingleRange(context, ranges.Values.Single());
  18.         }
  19.     }
  20. }

Básicamente:

  • Miramos el valor de la cabecera Range. Si no existe o no lo sabemos interpretar, creamos un FileStreamResult tradicional y lo ejecutamos. Es decir, nos saltamos todo lo de los rangos y devolvemos una descarga de fichero tradicional.
  • En caso de que haya más de un rango vamos a generar la respuesta multipart, con el método ProcessMultipleRanges.
  • En el caso de que haya un solo rango vamos a generar la respuesta con el método ProcessSingleRange.

El método ParseRangeHeader se encarga de parsear el valor de la cabecera Range:

  1. private Ranges ParseRangeHeader(HttpRequestBase request)
  2. {
  3.     var rangeHeader = request.Headers["Range"];
  4.     if (string.IsNullOrEmpty(rangeHeader)) return null;
  5.     rangeHeader = rangeHeader.Trim();
  6.  
  7.     if (!rangeHeader.StartsWith("bytes="))
  8.     {
  9.         return Ranges.InvalidUnit();
  10.     }
  11.  
  12.     return Ranges.FromBytesUnit(rangeHeader.Substring("bytes=".Length));
  13.  
  14. }

Se apoya en unas clases llamadas Ranges (que es básicamente una coleccion de objetos Range) y Range que representa un rango (origen, destino). El método importante de Ranges es el FromBytesUnit que es el que realmente parsea una cadena (sin el prefijo bytes=):

  1. public static Ranges FromBytesUnit(string value)
  2. {
  3.     var tokens = value.Split(',');
  4.  
  5.     var ranges = new Ranges();
  6.     foreach (var token in tokens)
  7.     {
  8.         ranges.Add(new Range(token));
  9.     }
  10.  
  11.     return ranges;
  12. }

Y la otra parte del trabajo la realiza el constructor de Range. Así si tenemos la cadena “0-100, 101-200” se llamará dos veces al constructor de Range pasándole primero la cadena “0-100” y luego “101-200”.

  1. public Range(string value)
  2. {
  3.     if (!value.Contains("-"))
  4.     {
  5.         _from = long.Parse(value);
  6.         _to = 1;
  7.     }
  8.     else if (value.StartsWith("-"))
  9.     {
  10.         _from = 1;
  11.         _to = long.Parse(value.Substring(1));
  12.     }
  13.     else
  14.     {
  15.         var idx = value.IndexOf('-');
  16.         _from = long.Parse(value.Substring(0, idx));
  17.         _to = idx == value.Length 1 ? 1 :
  18.             long.Parse(value.Substring(idx + 1));
  19.     }
  20. }

Sirviendo rangos

Empecemos por el caso más sencillo: Tenemos un solo rango. En este caso el método ProcessSingleRange toma el control:

  1. private void ProcessSingleRange(ControllerContext context, Range range)
  2. {
  3.     var response = context.HttpContext.Response;
  4.     response.StatusCode = 206;
  5.     response.AddHeader("Content-Range", new ContentRange(range, _length).ToString());
  6.     response.AddHeader("ETag", _etag);
  7.     response.ContentType = _contentType;
  8.     FlushRangeDataInResponse(range, response);
  9. }

Este método es muy sencillo. Establece el código de respuesta (206) y crea las cabeceras Content-Range, ETag y Content-Type. Luego llama al método FlushRangeDataInResponse. Dicho método va leyendo los bytes del fichero y los va escribiendo en el buffer de respuesta. Para evitar cargar todo el rango en memoria del servidor (un rango puede ser todo lo largo que desee el cliente), los datos se van leyendo en bloques de 1024 bytes y se van escribiendo en el buffer de salida:

  1. private void FlushRangeDataInResponse(Range range, HttpResponseBase response)
  2. {
  3.     var creader = new ChunkReader(_stream);
  4.     ChunkResult result = null;
  5.     var startpos = 0;
  6.     do
  7.     {
  8.         result = creader.GetBytesChunk(range, startpos);
  9.         startpos += result.BytesRead;
  10.         if (result.BytesRead > 0)
  11.         {
  12.             response.OutputStream.Write(result.Data, 0, result.BytesRead);
  13.         }
  14.         response.Flush();
  15.     } while (result.MoreToRead);
  16. }

Aquí el que hace el trabajo de verdad es la clase ChunkReader. Esta clase es la que va leyendo un stream, por “trozos” y devuelve después de cada trozo si hay “más trozos por leer”:

  1. public class ChunkReader
  2. {
  3.     private readonly Stream _stream;
  4.     public ChunkReader(Stream stream)
  5.     {
  6.         _stream = stream;
  7.     }
  8.  
  9.  
  10.     public ChunkResult GetBytesChunk(Range range, int startpos)
  11.     {
  12.         var chunk = new ChunkResult();
  13.         var reader = new BinaryReader(_stream);
  14.         var remainingLen = range.Length != 1 ? range.Length startpos : 1;
  15.         if (remainingLen == 0)
  16.         {
  17.             return new ChunkResult();
  18.         }
  19.                 
  20.         var bytesWanted = remainingLen != 1 ? Math.Min(1024, remainingLen) : 1024;
  21.         reader.BaseStream.Seek(range.FromBegin ? startpos : range.From + startpos, SeekOrigin.Begin);
  22.         var buffer = new byte[bytesWanted];
  23.         chunk.BytesRead = reader.Read(buffer, 0, (int)bytesWanted);
  24.         chunk.Data = buffer;
  25.         chunk.MoreToRead = remainingLen != 1
  26.             ? chunk.BytesRead != remainingLen
  27.             : chunk.BytesRead != 0;
  28.  
  29.         return chunk;
  30.     }
  31. }

El objeto ChunkResult contiene los bytes leídos, el número real de bytes leídos y un booleano que indica si hay “más datos” que leer.

Vayamos ahora al soporte para múltiples rangos. La idea es exactamente la misma, salvo que entre cada rango hay que generar el boundary correspondiente en la respuesta. Y eso es exactamente lo que hace el método ProcessMultipleRanges:

  1. private void ProcessMultipleRanges(ControllerContext context, Ranges ranges)
  2. {
  3.     var response = context.HttpContext.Response;
  4.     response.StatusCode = 206;
  5.     response.AddHeader("ETag", _etag);
  6.     response.AddHeader("Content-Type", "multipart/byteranges; boundary=THIS_STRING_SEPARATES");
  7.     foreach (var range in ranges.Values)
  8.     {
  9.         AddRangeInMultipartResponse(context, range);
  10.     }
  11. }

Primero añadimos los campos comunes (es decir el código de respuesta, el ETagm el content-type con el boundary). Y luego para cada rango llamamos al método AddRageInMultipartResponse, que simplemente coloca el boundary, luego el content-range y el content-type correspondiente y finalmente volca los datos del rango en el buffer de respuesta:

  1. private void AddRangeInMultipartResponse(ControllerContext context, Range range)
  2. {
  3.     var response = context.HttpContext.Response;
  4.     response.Write("– THIS STRING SEPARATES\x0D\x0A");
  5.     response.Write(string.Format("Content-Type: {0}\x0D\x0A", _contentType));
  6.     var contentRange = new ContentRange(range, _length);
  7.     if (contentRange.IsValid)
  8.     {
  9.         response.Write("Content-Range: " + contentRange + "\x0D\x0A\x0D\x0A");
  10.     }
  11.  
  12.     FlushRangeDataInResponse(range, response);
  13.     response.Write("\x0D\x0A");
  14. }

¡Y ya estamos! Algunos ejemplos de lo que vemos. La imagen de la izquierda contiene un solo rango y la de la derecha dos:

imageimage

Con esto hemos visto como añadir soporte para rangos desde el servidor. Por supuesto no está del todo pulido, faltaría añadir el soporte para el If-Range pero bueno… los mimbres vendrían a ser eso.

Nota: Si lo pruebas y colocas un valor de Range inválido (p. ej. Range: bytes=100) recibirás un HTTP 400. Este 400 es generado por IIS incluso antes de que la petición llegue a ASP.NET MVC.

Saludos!

He subido todo el código del POST en un repositorio de GitHub: https://github.com/eiximenis/PartialDownloads. La aplicación web de demo simplemente tiene el siguiente código en Home/Index:

  1. public ActionResult Index()
  2. {
  3.     return this.RangeFile("~\\Content\\test.png", "image/png");
  4. }

Por lo tanto si navegas con un navegador a /Home/Index deberás ver o descargarte la imagen entera. Pero usando fiddler o cURL puedes generar una petición con rangos para ver como funciona.

Para usar cURL te basta con la opción –header:

curl –header "Range: bytes=0-100" http://localhost:39841/

Saludos!

ASP.NET MVC–Descargar ficheros con soporte para continuación – i

Muy buenas! El objetivo de esta serie posts es ver como podemos implementar en ASP.NET MVC descargas de ficheros con soporte para “pausa y continuación”.

En este primer post veremos (por encima) que cabeceras HTTP están involucradas en las peticiones y las respuestas para permitir continuar una descarga.

Dicho soporte debe estar implementado en el cliente, pero también en el servidor. En el cliente porque ese tiene que efectuar lo que se llama una range request, es decir pasar en la petición HTTP que “parte” del archivo quiere. Y el servidor debe tener soporte para mandar solo esta parte.

En ASP.NET MVC usamos un FileActionResult para soportar descargas de archivos. Es muy cómodo y sencillo pero no tiene soporte para range requests. En esta serie posts veremos como podemos crearnos un ActionResult propio para que soporte este tipo de peticiones!

En el apartado range de la definición de HTTP1.1. se encuentra la definición de la cabecera Range que es la madre del cordero.

De hecho, básicamente hay dos cabeceras involucradas en una range request (que envía el cliente y que el servidor debe entender):

  1. Range: Especifica el rango que desea obtener el cliente
  2. If-Range: Especifica que se haga caso de “Range” solo si los datos no se han modificado desde xx (xx se especifica en la cabecera If-Range). Si los datos se han modificado, olvida Range y envía todos los datos.

Formato de Range

El formato de Range es muy sencillo. Oh sí, leyendo la especificación parece que es super complejo, pero para eso son las especificaciones… 😛

A la práctica Range tiene el siguiente formato:

  • Range: bytes=x-y: El cliente quiere los bytes desde x hasta y (ambos inclusivos). P. ej. Range 0-499: Devuelve los 500 primeros bytes.

Si y no aparece entonces significa “dame desde el byte x hasta el final”:

  • Range: bytes=9000: El cliente quiere desde el byte 9000 (inclusive) hasta el finak

Si x no aparece entonces significa “dame los últimos y bytes”. P. ej:

  • Range: bytes=-500: El cliente quiere los últimos 500 bytes.

La cabecera admite distintos rangos separados por comas:

  • Range: bytes=100-200,400-500, –800: Dame los bytes del 100 al 200. Y del 400 al 500 y los últimos 800.

Si los rangos se solapan esto no debe generar un error:

  • Range: bytes=500-700, 601-999: El cliente quiere los bytes del 500 al 700 y del 601 al 999.

Nota: Que la cabecera Range empiece por bytes= no es superfluo. El estándard es extensible y permite que se puedan definir otras unidades además de bytes para especificar los rangos. De ahí que deba especificarse la unidad usada en la cabecera Range. De hecho el servidor puede usar la cabecera Accept-Ranges para especificar que unidades de rangos soporta (p. ej. Accept-Ranges: bytes). Nosotros nos centraremos única y exclusivamente en rangos de bytes.

Respuesta del servidor

Si el cliente solo pide un rango, la respuesta del servidor es una respuesta normal, salvo que en lugar de usar el código 200, devuelve un 206 (Partial content) y con la cabecera Content-Range añadida.

El formato de la cabecera Content-Range es el rango servido, seguido por la longitud total del elemento separado por /. P. ej:

  • Content-Range: bytes 100-200/5000 –> Se están sirviendo los bytes 100 a 200 (ambos inclusives) de un recurso cuya longitud es de 5000 bytes.
  • Content-Range: bytes 100-200/* –> Se están sirviendo los bytes 100 a 200 de un recurso cuya longitud es desconocida.

Sí, en Content-Range no hay el símbolo = entre bytes y el valor de rango. Mientras que en la cabecera Range si que existe dicho símbolo…

Por otra parte, si el cliente ha pedido más de un rango la respuesta del servidor pasa a ser una multipart, es decir, dado que el cliente nos envía varios rangos, en la respuesta debemos incluirlos todos por separado. Hay varias cosas a tener presente.

  1. El código de retorno no es 200, es 206 (Como en el caso anterior)
  2. El content-type debe establecerse a multipart/byteranges y debe indicarse cual es la cadena separadora (el boundary).
  3. Cada subrespuesta viene precedida del boundary y tiene su propio content-type y content-range indicados.

Un ejemplo sería como sigue (sacado de http://greenbytes.de/tech/webdav/draft-ietf-httpbis-p5-range-latest.html#status.206):

HTTP/1.1 206 Partial Content
Date: Wed, 15 Nov 1995 06:25:24 GMT
Last-Modified: Wed, 15 Nov 1995 04:58:08 GMT
Content-Length: 1741
Content-Type: multipart/byteranges; boundary=THIS_STRING_SEPARATES

--THIS_STRING_SEPARATES
Content-Type: application/pdf
Content-Range: bytes 500-999/8000

...the first range...
--THIS_STRING_SEPARATES
Content-Type: application/pdf
Content-Range: bytes 7000-7999/8000

...the second range
--THIS_STRING_SEPARATES--

Si el servidor no puede satisfacer la petición del cliente debido a que los rangos pedidos son inválidos o hay demasiados que se solapan o lo que sea, puede devolver un HTTP 416 (Range not Satisfiable). Si lo hace deberia añadir una cabecera Content-Range inválida con el formato:

Content-Range: bytes */x (siendo x el número de bytes totales del recurso).

Otra opción si los rangos son inválidos es devolver el recurso entero usando un HTTP200 tradicional (muchos servidores hacen esto, ya que si un cliente usa rangos debe estar siempre preparado por si el servidor no los admite).

Cabecera If-Range

Nos falta mencionar la cabecera If-Range. La idea de dicha cabecera es que el cliente pueda decir algo como “Oye, tengo una parte del recurso descargado pero es de hace 3 días. Si no ha cambiado, pues me mandas los rangos que te indico en Range, en caso contrario, pues que le vamos a hacer me lo mandas todo con un 200 tradicional”.

El valor de If-Range puede ser, o bien un ETag que identifique el recurso o bien una fecha. Si no sabes lo que es un ETag, pues bueno es simplemente un identificador del contenido (o versión) del recurso. Puede ser un valor de hash, un valor de revisión, lo que sea. No hay un estándar definido, pero la idea es que si el cliente sabe el ETag de un recurso y lo manda, el servidor puede indicarle al cliente si dicho recurso ha cambiado o no. El cliente manda el ETag que tiene para dicho recurso (en la cabecera If-Range o bien en la If-None-Match si no hablamos de rangos).

Oh, por supuesto, uno podría crear un servidor que devolviese un ETag único cada vez que el cliente no le pasa un ETag previo y devolver siempre ETag recibido por el cliente en caso contrario. En este caso, se podría asegurar que cada cliente tendría un ETag distinto. ¿Ves por donde vamos, no? Un mecanismo sencillo y barato para distinguir usuarios únicos. Mucho mejor que todas esas maléficas cookies y además para usar ETags no es necesario colocar ningún popup ni nada parecido. Además, a diferencia de las cookies que se eliminan borrando las cookies (lo que mucha gente hace), los ETags se borran generalmente vaciando la cache del navegador (lo que hace mucha menos gente). Por supuesto, he dicho que uno podría hacer esto… no que se haya hecho 😛

Bueno… Hasta ahí el primer post. Rollo teórico, pero bueno, siempre es importante entender como funcionan las cosas ¿no?… en el siguiente post pasaremos a la práctica!!! 😀

Reflexiones del #programadorIO: Tipados vs no tipados

Esta noche he tenido el placer de participar en el marco de un #programadorIO en un debate sobre los lenguajes tipados vs los no tipados. Puedes ver el debate en youtube: https://www.youtube.com/watch?v=sxOM6sYgn5U

Nota: En el contexto de este post “no tipado” significa débilmente tipado o de tipado dinámico. Y tipado significa fuertemente tipado o de tipado estático.

Mi opinión es que los lenguajes no tipados son muy adecuados para prototipados, por que las herramientas suelen ser más ágiles y porque te permiten “saltarte” en primera instancia una fase mucho más formal de diseño (fase que luego tarde o temprano tiene que venir, pero en un lenguaje tipado tiene que realizarse al principio para, precisamente, poder diseñar los tipos). De todos modos mi experiencia profesional versa mayoritariamente en los lenguajes tipados (C++, Java y C#). También he mencionado que creo que el auge de JavaScript no es tanto por el lenguaje en sí, si no que viene de la mano del auge del desarrollo web. Si desarrollas para la web, debes hacerlo casi si o si, en JavaScript. Hubiese estado bien la opinión de alguien que hubiese desarrollado tan solo en un lenguaje dinámico que no sea JavaScript (p. ej. Ruby) porque dentro de pequeñas diferencias creo que todos compartíamos mucho en común y que estábamos más del lado de los tipados que de los no tipados.

Bien, aclarado esto, yo he hecho bastante incapié, o lo he intentado al menos, en que a veces un sistema estático de tipos es un “corsé” no deseado y que para ciertas tareas un lenguaje no tipado es mejor o te permite realizarlas de forma mucho más natural o productiva. Y voy a poner algunos ejemplos concretos usando C# (casi todo lo que diré es aplicable a Java también).

Ejemplo 1: Deserialización de datos dinámicos

Este es el ejemplo que apuntaba Pedro. En el fondo es muy simple, puesto que si los datos son dinámicos ¿qué mejor que un lenguaje dinámico para deserializarlos?

Imagina que tienes que consumir una api REST que te puede devolver un objeto (da igual el formato, JSON, XML o lo que sea) que tiene 150 campos posibles, todos ellos opcionales. Pueden aparecer o no pueden aparecer. Si tienes que deserializarlo en un lenguaje tipado, que haces: crear una clase con 150 miembros? Y si alguno de los miembros es un int y vale 0… este 0 es porque no ha aparecido o bien porque realmente he llegado un 0. Si claro, puedes usar Nullable<int> pero… bonito y divertido no es.

¿Y si en lugar de ser campos simples son compuestos? ¡Terminas teniendo una jerarquía enorme de clases tan solo para deserializar las respuestas!

El mismo problema te lo encuentras si eres el que crea la API REST por supuesto. Pero incluso peor… porque igual no puedes usar la clase con 150 miembros porque a lo mejor los miembros vacíos o con el valor por defecto se serializarían también y no quieres eso. Vas a terminar igual: con un numero enorme de clases tan solo para serializar los datos.

Si los datos con los que trabajas tienen una naturaleza dinámica, un lenguaje dinámico es lo mejor para tratarlos.

Por supuesto podrías trabajar con algo parecido a un Dictionary<string, object> y serializar el diccionario con el formato de datos esperado. Si, pero haciendo esto estás haciendo un workaround, te estás enfrentando al sistema de tipos. Estás simulando un tipo dinámico en un lenguaje estático. Todas las ventajas del tipado estático desaparecen (el compilador no te ayudará si te equivocas en el nombre de una clase), las herramientas de refactoring no pueden ayudarte en nada (incluso menos que en el caso de un lenguaje dinámico), tu código queda “sucio” (lleno de dictionarios, llamadas a métodos .Add) y además… tardas más.

Ejemplo 2: Jerarquías de clases distintas autogeneradas

Imagina que tienes dos servicios WCF distintos que te devuelven datos muy parecidos. En ambos casos son datos de productos. En ambos casos siempre hay un nombre, un precio y un id. Los nombres de los campos y los tipos SOAP asociados son los mismos (imagina que eso lo puedes definir o imponer).

Si generas los proxies para acceder a los servicios vas a terminar con dos clases diferentes (una por cada servicio). Pero incluso aunque los miembros para el nombre, precio e id se llamasen igual no podrías intercambiar esos proxies en código. Vas a tener dos clases iguales (ambas tendrán un string nombre, un decimal precio y un int id) pero para el compilador son dos clases distintas. Cualquier función que opere sobre uno de los proxies no puede operar con el otro. A pesar de que el aspecto de ambas clases es “idéntico”, a pesar de que representan el “mismo” concepto, para el compilador tienen la mismo parecido que el de un perro con una manzana.

Sí: el problema principal está en que tienes dos clases distintas para lo mismo, pero eso ocurre en la vida real cuando hay herramientas que autogeneran código. ¿Qué soluciones tienes? Pues crear una tercera clase que sea tu “producto” y “transformar” cada uno de los objetos proxy a un objeto de tu clase “producto” que será con la que trabaje tu código. Sí, es posible que los lenguajes tipados tengan un rendimiento superior a los no tipados, pero si empiezas a tener que copiar objetos muchas veces…

¿No estaría bien que tu código que trabaja con un producto pudiese trabajar directamente con cualquiera de los dos proxies? Aunque sean de clases “distintas”. Aunque no haya ninguna interfaz en común. A fin de cuenta tu código tan solo necesita un nombre, un id y un precio. Estaría bien que pudiese funcionar con cualquier objeto que tiene esos tres campos no? Eso se llama duck typing y viene “de serie” con los lenguajes no tipados.

¡Ojo! Que el hecho de que un lenguaje sea tipado no le impide tener algo muy parecido (a efectos prácticos idéntico) al duck typing: P. ej. este problema de los proxies se podría solucionar en C++ con el uso de templates. El uso de templates en C++ es un ejemplo de lo que conoce como structural typing (que es, básicamente, duck typing en tiempo de compilación). Pero no, ni Java ni C# tienen soporte para structural typing.

Ejemplo 3: Generics

Podría poner muchos ejemplos parecidos al de los proxies, incluso cuando no hay código generado. Un ejemplo rápido. Tengo cuatro clases mías, independientes entre ellas.

Ahora quiero crear una colección propia, que implemente IEnumerable<T> pero que internamente use un diccionario, ya que continuamente se estarán buscando elementos por nombre.

Por supuesto las 4 clases tienen una propiedad string Name para guardar el nombre.

Pues bien, para crear esas cuatro colecciones, tienes dos opciones:

  1. Crearte cuatro clases colección idénticas que solo cambian el tipo de datos que aceptan / devuelven. Si, eso suena muy .NET 1.0
  2. Usar generics… Salvo que no puedes.

Y no puedes usar generics porque dentro del código de la clase genérica no puedes acceder a la propiedad Name del tipo genérico. Porque el tipo genérico es “object” por defecto. Por supuesto si las 4 clases implementasen una interfaz común, que se llamase INamedItem (p. ej.) y que definiese la propiedad Name, podrías poner una restricción de generics para que el tipo genérico implementase INamedItem y entonces podrías usar generics para crear tu colección propia. Pero realmente la interfaz INamedItem no representa ningún concepto real. Está tan solo para permitirte usar generics en este caso. Este es otro caso donde duck typing vendría bien: tu colección propia debería funcionar con cualquier objeto que tenga la propiedad Name. Pero el sistema de tipos de C# (con el de Java pasa lo mismo) es incapaz de dar soporte a esta situación.

Ejemplo 4: delegados

Tengo una función que devuelve un bool y acepta un int. Tengo un delegado de tipo Func<int, bool> que “apunta” a dicha función.

Quiero pasar este delegado a otra función… que espera un Predicate<int>.

Conceptualmente Func<T, bool> es lo mismo que Predicate<T> pero para el compilador son totalmente distintos. Por suerte en este caso la solución es muy sencilla, convertir un Func a un Predicate es muy sencillo, pero tienes que hacerlo igualmente.

Nota: Por cierto, aprovechando, no uses nunca Predicate<T> en tu código. Está obsoleto. Func<T,bool> es lo que se debe usar.

Ejemplo 5: Instanciación de tipos dinámica

Este es muy sencillo: quieres instanciar un tipo cuyo nombre no conoces en tiempo de compilación. Da igual la razón: el método puede venirte de un fichero, BBDD o lo que sea.

Cierto, en C# y en Java puedes usar reflection (p. ej. Activator.CreateInstance en C#) para crear la instancia. El problema es que usar reflection elimina todas las ventajas del tipado estático y además el código queda muy “sucio”. Pasar de reflection a tipado estático otra vez no siempre es posible (si sabes que cualquiera de las posibles clases implementa el mismo interfaz puedes convertir el resultado al interfaz y a partir de allí recuperar el tipado estático). Y si tienes que hacer varias cosas usando reflection el código queda “sucio”, dificil de entender y ninguna herramienta de refactorización puede ayudarte.

En un lenguaje no tipado en cambio, la creación del objeto puede requerir una sintaxis distinta pero una vez creado el objeto invocar los métodos será con la misma sintaxis de siempre.

En resumen

Todos los ejemplos presentados (y hay más de posibles), se resumen en dos grandes tipos: comportamiento dinámico (ejemplos 1 y 5) y “objetos semánticamente compatibles pero incompatibles a la práctica” (el resto de ejemplos).

¿Justifican esos casos usar un lenguaje dinámico para todos tus proyectos? No. Pero si que justifican que los lenguajes estáticos añadan opciones para facilitar la programación dinámica. Por ejemplo el dynamic de C# es un paso en esa dirección. Los templates de C++ son otro (los genérics de .NET o de Java ni de lejos).

Entonces… ¿tienes que usar un lenguaje estático para todos tus proyectos? Pues no. Puedes hacer grandes proyectos tanto en lenguajes tipados como en no tipados. Y puedes hacer aberraciones en ambos.

Aunque yo personalmente prefiero un lenguaje estático a uno dinámico, me siento cómodo en estos y a veces cuando estoy en C# si que me gustaría tener toda la flexibilidad que estos me ofrecen. De hecho, cuanto más me he acostrumbrado a JavaScript más echo en falta ciertas cosas en C#. Pero no siempre, no continuamente. Solo “cuando yo quiero”.

Ahora sí, lo que tengo claro es que desarrollar bien en un lenguaje no tipado requiere mayor disciplina que en un lenguaje tipado y que cualquier desarrollador que se precie debería conocer los conceptos de orientación a objetos clásicos (de hecho yo creo que cualquier desarrollador debería aprender C++, pero esa es otra batalla :P).

Bueno… si has llegado hasta aquí… gracias por leer este tostón! Y por supuesto, siéntete libre de dejar un comentario con tu opinión!

Un saludo!