martes, 27 de marzo de 2018

Todas las abstracciones fugan

Esta frase la he escuchado (y leído) bastantes veces, y para ser honesto, nunca terminé de entender a que se refería. Hace poco volví a leerla y me dije: Entendámosla de una vez por todas
¿De donde viene todo? Del artículo The law of leaky abstractions, escrito en Noviembre de 2002 por Joel Spolsky.

“All non-trivial abstractions, to some degree, are leaky”

Viene a decir que toda abstracción, excluyendo aquellas que son triviales, tarde un temprano fuga, revelando aspectos que tratábamos de abstraer.

Veámoslo con un ejemplo:


Todos hemos visto una implementación del patrón repository como la siguiente:

    public interface IStudentRepository : IDisposable
    {
        IQueryable<Student> GetStudents();
        Student GetStudentByID(int studentId);
        void InsertStudent(Student student);
        void DeleteStudent(int studentID);
        void UpdateStudent(Student student);
        void Save();
    }

El hecho de que el método GetStudents devuelva un IQueryable implica que debamos conocer detalles acerca de la implementación del repositorio, conocer que hay detrás, ya que el proveedor de Linq ejecutará algunas instrucciones en memoria y otras sobre una motor de bases de datos. No podemos ignorarlo si queremos no meternos en líos. Podemos decir por tanto que nuestra abstracción IStudentRepository fuga


¿Hay más ejemplos comunes?


Por supuesto, una rápida búsqueda por internet nos dará un bonito listado de abstracciones, que usamos comúnmente, y que fugan (¿todas lo hacen no? XDXD)
  • El garbage collector: tarde o temprano, si tu aplicación realiza mucha presión sobre la memoria, o si es una aplicación que necesita un altísimo rendimiento, tendrás que entender su implementación para alcanzar ese nivel de optimización deseado.
  • Un sistema de ficheros distribuido, bajo SMB por ejemplo, trata de abstraerte de que los ficheros están en remoto, mostrándolos como si de un disco local se tratase.  No obstante la latencia o los posibles micro-cortes de red pueden hacer que tengas que manejar dichos ficheros de un modo diferente (y conocer que de hecho esos ficheros no son locales).
  • La clase string de C# por ejemplo, que debido su implementación genera un nuevo string cada vez que concatenamos dos strings existentes previamente. Si no conocemos este detalle, podemos sufrir serios problemas de rendimiento y escalabilidad.

¿Es esto malo?


No lo considero necesariamente malo, simplemente está ahí y lo mejor es que lo conozcamos y seamos conscientes de ello.
Nuevas tecnologías y herramientas tratan de elevar el nivel de abstracción que manejamos al desarrollar software, nos obstante, ya que estas abstracciones fugarán de un modo u otro, debemos conocer sus detalles, cómo estas abstracciones funcionan y han sido implementadas, antes de pasar sin más a utilizarlas.

Recursos: 


miércoles, 23 de agosto de 2017

Cuestiónate todo: Throw vs Throw ex

Hace unos días leía una entrada sobre excepciones en Campus MVP: GAMBADAS: Los tres principales pecados al gestionar errores y excepciones. Allí nos daban consejos para trabajar con excepciones, y entre ellos nos recordaban como debemos relanzar una excepción, y cual es la diferencia entre relanzarla haciendo throw o haciendo throw ex. Me refiero a esto:

image

En el artículo se indicaba que cuando relanzamos una excepción haciendo throw ex estamos “reseteando” el call stack, por lo que un elemento superior que capture la excepción no va a poder saber donde se generó originalmente,y obtendrá como origen la línea en la que hacemos el throw ex.

image

Según lo leí la cabeza me jugo una mala pasada y me dije: “eso esta mal, hacer throw o throw ex es lo mismo”. Lo malo es que rápidamente me fui a Twitter a pregonarlo, donde me corrigieron a la primera de cambio: no no es lo mismo. Si nos vamos a la documentación oficial de C# (https://docs.microsoft.com/es-es/dotnet/csharp/language-reference/keywords/throw) vemos esto:
image

Parece estar meridianamente claro… ¿pero es así en todos los casos?  ¿De donde había sacado yo las idea de que era lo mismo una cosa que la otra?

El ejemplo

Vamos a comprobarlo, para ello he creado un código de ejemplo, es sencillo. Tenemos un método  que lanza una excepción, ya que hace una división por cero
public static int FaultyCode(int number)
{
    int divisor = 0;
    return number / divisor;
}
Luego tenemos un segundo método (CatchAndThrowEx) llama al anterior, recubriendo la llamada en un bloque try/catch. Una vez capturada la excepción la relanza con throw ex.
public static void CatchAndThrowEx()
{
    try
    {
        var result = FaultyCode(5);
    }
    catch (Exception ex)
    {
        throw ex;
    }
}
También un tercer método(CatchAndThrow) similar al anterior y que también llama al código que falla. La llamada es recubierta en un bloque try/catch. Una vez capturada la excepción la relanza con throw.
public static void CatchAndThrow()
{
    try
    {
        var result = FaultyCode(5);
    }
    catch(Exception ex)
    {
        throw;
    }
}
Finalmente, desde el main de una aplicación de consola, invocamos a los dos métodos que relanzan la excepción, y la capturamos, trazando su call stack, para que veamos si este ha sido “reseteado” o no.

public static void Main(string[] args)
{
    try
    {
        Console.WriteLine("==============================================");
        Console.WriteLine("CatchAndThrow:");
        CatchAndThrow();
    }
    catch (Exception ex)
    {
        ex.ToConsole();
    }

    try
    {

        Console.WriteLine("==============================================");
        Console.WriteLine("CatchAndThrowEx:");
        CatchAndThrowEx();
    }
    catch (Exception ex)
    {
        ex.ToConsole();
    }
}
Si ejecutamos lo anterior, obtenemos lo esperado: cuando se relanza con throw ex se pierde el call stack y no somos capaces de ver el método donde se originó la excepción… malo
Core 2

Seguimos cuestionando

Antes de abandonar me dije, lo he ejecutado compilando en  debug… ¿habrá cambio en release?
Si ejecuto en release (dotnet run -c Release) las cosas cambian!  Como podéis ver a continuación throw y throw ex se comportan exactamente igual.
image

¡Esto no estaba en el guión!
Visto lo visto me dije, será cosa de .Net Core 2? Vamos a comprobar en otras configuraciones. El resultado está en la siguiente tabla, y siempre es el mismo: cuando se ha compilado en release throw y throw ex se comportan igual:

PlataformaCompilaciónResultado
.Net Core 1.1 Debug Throw mantiene el call stack. Throw ex no lo hace
.Net Core 1.1 Release Throw y Throw ex tienen el mismo comportamiento
.Net Core 2 Debug Throw mantiene el call stack. Throw ex no lo hace
.Net Core 2 Release Throw y Throw ex tienen el mismo comportamiento
.Net Framework 4.6.2  Debug Throw mantiene el call stack. Throw ex no lo hace
.Net Framework 4.6.2  Release Throw y Throw ex tienen el mismo comportamiento 



¿Donde está el truco?

Suele haber un motivo, siempre lo hay, que explique comportamientos extraños, comportamiento que contradicen la documentación oficial como es en este caso.

Yo ni lo había imaginado, pero Juanma (https://twitter.com/gulnor) rápidamente intuyó que pasaba: El método FaultyCode era demasiado sencillo, tanto que al compilar en Release se convierte en un inline, por lo que pasa a ser código del método que le invoca.

Si complicamos FaultyCode, el compilador no lo convertirá en un inline, y finalmente  veremos que el comportamiento vuelve a ser el esperado, y el documentado:

  • Throw mantiene el call stack
  • Throw ex lo resetea


Saludos, espero que os haya resultado interesante

sábado, 19 de agosto de 2017

Entrada vintage parte 2: .Net Core 2.0 y Visual Studio 2015

Continuando con mi entrada anterior (http://www.serginet.com/2017/08/core-vs), y una vez que .NET Core 2.0 ha sido liberado, vamos  a ver como podemos crear, compilar y ejecutar aplicaciones basadas en .NET Core 2 con proyectos tipo project.json (los de Visual Studio 2015).

Lo primero ver la situación inicial, tal y como hacíamos en la entrada anterior:

  • La versión del runtime de .NET Core, ejecutando el comando dotnet.
  • La versión del CLI, con dotnet --version. 
  • La versión del SDK (que debería ser la misma que la del CLI), con dotnet --info.

image

         
          Como veis hay un runtime 1.0.1 con un SDK (y por tanto CLI) 1.0.0-preview2-003131

  • ¿Hay otros SDK's instalados?, Para ello miramos directamente en la carpeta ProgramFiles\dotnet\SDK y vemos que en esta máquina no hay más SDK's

image

  • ¿Qué dice Windows que tenemos instalado?  Miramos a Agregar/Quitar programas, y reconfirmamos que no hay nada más instalado.

agregarQuitarAntes

Como veis parto de una situación standard de Visual Studio 2015: SDK y Tools en preview, además de un runtime de .NET 1.0.x .

Empezamos descargando el SDK desde la página de descargas oficial ( https://www.microsoft.com/net/download/core ) e instalándolo.

image

Una vez hecho, vamos a ver de nuevo como ha quedado el sistema, consultando:

  • La versión del runtime de .NET Core, ejecutando el comando dotnet.
  • La versión del CLI, con dotnet --version. 
  • La versión del SDK (que debería ser la misma que la del CLI), con dotnet --info. 


image
       Aquí vemos que ha habido cambios en el comando dotnet: ya no vemos el runtime ejecutando dotnet. Si vamos a la ayuda, vemos lo siguiente:
  • dotnet --version nos da la versión del SDK
  • dotnet --info nos da información general de .net Core, incluyendo la versión de CLI (que debe coincidir con el SDK) y el runtime

image

Ahora vamos a compilar, nos ubicamos en un proyecto tipo Visual Studio 2015 (project.json) y ejecutamos dotnet.build.
Es importante fijarnos que no tengamos un fichero project.json que nos cambie el SDK a usar, de momento queremos usar el último SDK que acabamos de instalar

La primera en la frente, tenemos un error.

image

Estamos en la misma situación de la entrada anterior (http://www.serginet.com/2017/08/core-vs): se ha eliminado el soporte a proyectos tipo project.json, por lo que si queremos compilar necesitamos usar un SDK “viejo”. Ahora es el momento de hacer uso de nuestro fichero global.json.
Ubicamos el fichero una carpeta por encima de la carpeta donde está en project.json, con el siguiente contenido:

image

y compilamos y ejecutamos sin problema.

image

Ahora estamos ejecutando una aplicación .Net Core 1.0 con un runtime 2.0, vemos el runtime ejecutando dotnet --info

image

Me sigue sorprendiendo ver una “mezcla” curiosa: el último runtime 2.0 side by side con un SDK antiguo, versión preview anterior a 1.0

Actualizando a .Net Core 2.0


Vamos a actualizar a 2.0, modificando directamente el fichero project.json para dejarlo así:

image

Para compilarlo, nos basta con dotnet restore seguido de dotnet build, y sin eliminar nuestro fichero global.json, ya que seguimos necesitando el SDK viejo que nos da compatibilidad con project.json


image


Y vemos que… ¡compila bien, sin problemas!  Hemos compilado una aplicación con target .Net Core 2.0 con un SDK compatible con Visual Studio 2015  ya que sigue usando project.json

Reto conseguido! A disfrutarlo!