El post de Ayende de la semana pasada sobre planificación de tareas me ha recordado que tenía pendiente escrubir sobre una librería bastante útil para plafinicar tareas: Quarz.Net.

Muchas veces para resolver ciertos problemas necesitamos ejecutar tareas de forma periódica. No me refiero aquí a “comprobar cada X segundos si la conexión con la base de datos sigue activa” (aunque la solución que vamos a ver también sirve para estos casos), sino más bien a tareas del tipo “todos los lunes a las 0:00 revisar los cobros vencidos y notificar al departamento de facturación que los reclame“.

Para el primer tipo de procesos con un Timer te puedes apañar, pero para el segundo caso, existen básicamente 3 opciones:

  • Implementar nuestro propio sistema de programación de tareas. Si crees que ejecutar tareas en background de forma periódica es algo trivial, piénsalo un poco y léete los comentarios del post de Ayende con todas las cosas que se deben tener en cuenta y pueden salir mal.
  • Usar el planificador de tareas del sistema operativo para que lance nuestros procesos de forma automatica. Es una muy buena opción, que nos da mucha flexibilidad con multitud de opciones para establecer la planificación, reintentos, notificaciones en caso de fallo, etc. A cambio, tiene algunos inconvenientes. Obliga a tener un ejecutable por cada tarea que queramos realizar o, en su defecto, algún tipo de interfaz de línea de comandos para indicar a nuestro ejecutable la tarea que queremos que realice. Además, establece una dependencia adicional entre nuestra aplicación y el sistema operativo sobre el que está desplegada.
  • Utilizar alguna librería para realizar la planificación de tareas desde nuestra propia aplicación. Aquí es donde esta Quarz.Net, ya que nos permite realizar precisamente eso.

Conceptos básicos de Quarz.Net

Para trabajar con Quarz.Net necesitamos comprender cuáles son los conceptos básicos entorno a los que se organiza toda la libería:

Conceptos básicos de Quarz.Net
  • El Scheduler (planificador) es el motor de planificación de tareas como tal. A nivel de código, se corresponde con clases que implementan el interface IScheduler.
  • Los Jobs (trabajos) son los tareas que queremos realizar y que, para ello, registramos en el Scheduler. En código, son clases que implementan el interface IJob.
  • Los Triggers (disparadores) son los que marcan las condiciones bajo las cuales se deberá iniciar una tarea. Por ejemplo, cada hora en el minuto 17 o cada 3 minutos. En código, son clases que heredan de Trigger.

Es importante tener claro que en Quarz.Net existe una separación entre lo que hacemos (Job) y cuándo lo hacemos (Trigger). Gracias esto obtenemos una gran flexibilidad porque podemos tener tareas que se disparan con varios Triggers diferentes o Triggers que disparan varias tareas a la vez. Un buen ejemplo de separación de responsabilidades.

Cómo usar Quarz.Net

Junto a la documentación de Quarz.Net (que está muy bien, por cierto) puedes encontrar un completo tutorial para empezar a trabajar con Quarz.Net. De todas formas, para que puedas hacerte una idea rápida de cómo funciona, vamos a ver un ejemplo muy sencillo.

Empezamos creado un Job muy básico:

public class SampleJob : IJob
{
	public void Execute(JobExecutionContext context)
	{
		Console.Out.WriteLine("Job executed at {0:HH:mm:ss}", DateTime.Now);
	}
}

Para crear un Job tenemos que crear una clase que implemente el interfaz IJob, que únicamente contiene el método Execute, en el cual deberemos hacer lo que sea que tenga que hacer nuestra tarea. Vamos, un patrón Command de toda la vida. Es importante que tenga un constructor sin parámetros.

El Job recibe un JobExecutionContext que nos permite acceder, entre otras cosas al Trigger que ha desencadenado la ejecución o la hora a la que está prevista la siguiente ejecución de la tarea.

Ahora necesitamos un Trigger para disparar nuestro Job:

var trigger = new CronTrigger("MyTrigger")
{
	CronExpressionString = "0 0 2 ? * *"
}

Quarz.Net incluye dos tipos de Triggers, el SimpleTrigger y el CronTrigger. En este caso estamos creando un CronTrigger que se disparará todos los días a las 2 de la mañana. Jugando con el CronExpressionString podemos usar la sintaxis del comando cron de Unix para crear planificaciones más complejas, del estilo de todos los martes a las 4 de la tarde, el último viernes de cada mes, etc.

Una vez que tenemos nuestro Job y nuestro Trigger, necesitamos un planificador para que todo esto empiece a funcionar:

var schedulerFactory = new StdSchedulerFactory();
var scheduler = schedulerFactory.GetScheduler();

var job = new JobDetail("MySampleJob", typeof (SampleJob));
scheduler.ScheduleJob(job, trigger);

El Scheduler lo obtenemos a través de un StdSchedulerFactory y le añadimos la programación de la tarea.

Ya sólo nos queda arrancar el planificador para que empiece a ejecutar tareas según la planificación establecida:

scheduler.Start();

Console.Out.WriteLine("Scheduler started. Press <ENTER> to quit.");
Console.ReadLine();

scheduler.Shutdown(true);

Una última cosa. Quarz.Net utiliza Common.Logging como sistema de logging, lo que nos permite aprovechar los distintos adapters de Common.Logging para usar la librería de logging que más nos guste: log4net, nlog, etc.

Si queremos activar el sistema de log con log4net, deberemos incluir algo parecido a esto para inicializarlo antes de empezar a utilizar Quarz.Net:

LogManager.Adapter = new Log4NetLoggerFactoryAdapter(new NameValueCollection()
{
	{"configType", "FILE-WATCH"},
	{"configFile", Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "log4net.config")}
});

Resumiendo…

Quarz.Net es una librería muy útil para planificar tareas. En mi trabajo del Mundo Real ™ llevamos bastante tiempo usándolo para lanzar procesos internos desde un servicio de Windows y la verdad es que funciona muy bien y es muy estable.

Desde el punto de vista de implementación hay algunas cosas que no me gustan. Se nota que es un port de una librería de Java (Quarz) y hay cosas que me resultan poco idiomáticas en .NET, como el StdSchedulerFactory o la falta de sobrecargas genéricas para algunos métodos, que obligan a escribir cosas como new JobDetails("The Job", typeof(MyJob)) en lugar de new JobDetails<MyJob>.

Tampoco me acaba de gustar el uso de Common.Logging, pero comprendo que este es uno de los casos en que tiene sentido montar una capa de abstracción sobre la librería de logging para poder usar Quarz.Net en aplicaciones que ya estén usando alguna librería de logging. De hecho, es de los pocos casos en que le veía sentido a Common Service Locator (un caso similar) cuando me quejaba de él hace tiempo.

En este post sólo hemos visto una introducción rápida, pero con Quarz.Net se pueden hacer cosas más complicadas, como guardar la planificación en una base de datos y así repartir la carga de la ejecución de tareas entre distintas máquinas ejecutando Quarz.Net.

En definitiva, la próxima vez que tengas que implementar tareas programadas, antes de usar una solución casera que puede acabar siendo demasiado frágil, puedes probar Quarz.Net.

Compartir:
Facebook Twitter Email Linkedin Plusone

Hace unos días Eduard mostraba en su blog cómo cargar información de reflection sin cargar el assembly. La técnica que explica en el post usando AppDomains es muy interesante, pero en los comentarios saqué el tema de Mono.Cecil y me pareció una buena excusa para probarlo.

¿Qué es Mono.Cecil?

Mono.Cecil es un proyecto englobado dentro de Mono, una implementación alternativa y multiplataforma de .NET. Mono.Cecil permite leer assemblies de .NET e inspeccionar los tipos que contienen, los métodos de cada tipo, e incluso modificar los assemblies y volver a guardarlos en disco.

Aunque forme parte de Mono, Cecil es un assembly que puede ser utilizado desde casi cualquier versión de .NET Framework. Actualmente está alojado en github, de donde podéis bajar los fuentes y compilarlos vosotros mismos, pero si os resulta más cómodo también existe un paquete para NuGet. Para utilizarlo, sólo necesitamos referenciar el assembly Mono.Cecil.dll.

A diferencia de reflection, Cecil no carga el assembly dentro del AppDomain, sino que únicamente lee la información del assembly y nos permite trabajar con ella. De hecho, esto permite usar sin problemas Cecil para leer assemblies de versiones del framework distintas de la que estamos usando. Por ejemplo, podemos crear una aplicación con .NET 2.0 que use Cecil y analice assemblies compilados con .NET 4.0.

¿Para que sirve? Pues se me ocurren varios usos, aunque lo mejor es verlo con algunos ejemplos.

Inspeccionar un assembly sin cargarlo

Lo primero que he probado es hacer lo mismo que hacía Eduard con los AppDomain, pero con Cecil: averiguar los tipos que hay un assembly sin tener que cargarlo. Para ello, sólo hay que usar el siguiente código:

var assembly = AssemblyDefinition.ReadAssembly("the.assembly.dll");
var types = assembly.MainModule.Types.Select(x => x.FullName);

El punto de entrada es siempre AssemblyDefinition.ReadAssembly(), que permite leer la información de un assembly desde disco y devuelve un objeto de tipo AssemblyDefinition. Todo el API de Cecil está organizada en una estructura de clases análoga a los AssemblyInfo/MethodInfo/PropertyInfo... que se usa con reflection, pero en este caso se llaman AssemblyDefinition/MethodDefinition/PropertyDefinition/....

Un AssemblyDefinition contiene una colección de ModuleDefinition que nos permite tratar con assemblies que contengan más de un módulo. La verdad es que esto no es muy frecuente, lo más parecido a ello que me he encontrado nunca es el caso de assemblies en mixed mode, como el de SQLite, así que generalmente nos bastará con acceder a la propiedad MainModule que nos permite acceder al módulo principal del assembly y a lo que contiene. A partir de ahí, podemos obtener los TypeDefinition y de ellos… bueno, ya te puedes imaginar como sigue la cosa viendo el código de arriba.

Inspeccionar el cuerpo de los métodos

Otro caso en el que puede ser útil Cecil es si queremos inspeccionar qué es lo que hace un método. A través de la propiedad Body.Instructions de un MethodDefinition podemos acceder a las instrucciones IL que forman la implementación del método. Esto de toquetear el IL puede parecer un poco raro al principio, pero cuando te pones, es hasta divertido.

Como ejemplo, podríamos ver qué métodos en un assembly están usando un método determinado, por ejemplo Console.WriteLine:

var assembly = AssemblyDefinition.ReadAssembly("assembly.dll");
var methods = assembly.MainModule
    .Types.SelectMany(type => type.Methods)
    .Where(method => method.Body.Instructions
                        .Any(x => (x.OpCode == OpCodes.Call ||
                                   x.OpCode == OpCodes.Calli ||
                                   x.OpCode == OpCodes.Callvirt) &&
                                  x.Operand.ToString().Contains("System.Console.WriteLine")));

Para entender el ejemplo hay que tener en cuenta que las invocaciones de métodos en IL se realizan con una instrucción que tiene como código de operación Call, Calli o Callvirt, usando como parámetro el nombre del método a invocar. En este caso la instrucción que se usa realmente es Call por tratarse de un método estático, pero he preferido poner todas para tenerlas como referencia por si queréis probar con otros métodos.

Esto, aparte de ser curioso, puede tener su utilidad para automatizar chequeos basados en análisis estáticos de código. Algo parecido a lo que hace NDepend, pero más de andar por casa. Por ejemplo, podrías tener un test unitario que validase que todas las excepciones que se lanzan desde el Domain son del tipo DomainException o cosas similares.

Modificar assemblies

Por último, otra de las cosas que nos permite hacer Cecil es modificar un assembly y volver a guardarlo en disco. ¿Por qué querría alguien hacer eso? Lo más habitual es para aplicar técnicas AOP con IL-Weaving.

Ya he puesto algún ejemplo en este blog de como aplicar AOP usando un contenedor IoC y LCG, pero esas técnicas implican generar clases en tiempo de ejecución, por lo que suponen una penalización al rendimiento. Usando Cecil podemos modificar el assembly generado después de la compilación, consiguiendo no penalizar el rendimiento al ejecutar la aplicación.

Para que no se complique la cosa, en el ejemplo vamos a hacer algo más simple, vamos a sustituir todas las invocaciones a Console.WriteLine con invocaciones a Trace.WriteLine:

// TODO: Obtener los métodos que usan System.Console.WriteLine
// como hemos visto en el ejemplo anterior
var methods = ...; 

foreach (var method in methods)
{
	// Importamos el método Trace.WriteLine en el módulo principal
	// del assembly para obtener un MethodDefinition
	var traceWriteLine = assembly.MainModule
		.Import(typeof (Trace).GetMethod("WriteLine", new[] {typeof (string)}));

	// Obtenemos las instrucciones que usan WriteLine
	var writeLines = processor.Body.Instructions
					.Where(x => x.OpCode == OpCodes.Call &&
						    x.Operand.ToString().Contains("System.Console.WriteLine"))
					.ToArray();

	foreach (var instruction in writeLines)
	{
		// Aprovecho que los parámetros son los mismos. El parámetro
		// quedará apilado con una instrucción Ldstr antes de Call Console.WriteLine
		// así que no lo toco

		// Creamos la instrucción Call Trace.WriteLine
		var trace = method.Body.GetILProcessor().Create(OpCodes.Call, traceWriteLine);

		// La insertamos justo antes del Console.WriteLine...
		processor.InsertBefore(instruction, trace);
		// ... y eliminamos el Console.WriteLine
		processor.Remove(instruction);
	}
}

// Escribimos a disco el assembly modificado
assembly.Write("assembly.patched.dll");

Es un poco lioso, pero creo que leyendo los comentarios del código se puede llegar a entender. La parte más complicada realmente es la de generar las instrucciones IL correctas para hacer lo que queramos hacer en cada caso. En este ejemplo, como sólo queríamos reemplazar el método que se estaba invocando y, además, tenían los mismos parámetros, es relativamente sencillo.

Conclusiones

Mono.Cecil es una librería que no creo que vayas a usar cada día, pero que “en lo suyo” me parece que hace una labor excelente. Es muy fácil de manejar, el API está bien estructurada y, al menos para hacer las tres tonterías que he hecho en este post, funciona como esperas (que es más de lo que se puede decir de muchas librerías por ahí).

En definitiva, una herramienta más a tener en cuenta para cuando haga falta.

Compartir:
Facebook Twitter Email Linkedin Plusone

En mi noble (y seguramente ilusa) intención de construir software de calidad, últimamente he estado pensando bastante sobre el tema de las pruebas. No me refiero en este caso a los tests automáticos, ya sean unitarios, de aceptación, de aprobación, etc. Estoy pensando en las pruebas manuales. Sí, manuales a mano, de las de arrancar la aplicación y empezar a comprobar si funciona como debe.

Al final, y especialmente en aplicaciones que tienen algún tipo de interfaz de usuario, es inevitable y fundamental realizar pruebas manuales. Hay cosas que son imposibles muy difíciles de detectar con pruebas automáticas, como etiquetas mal alineadas, textos mal redactados, interacciones “extrañas”… prácticamente todo lo que afecta a la experiencia de usuario.

Además, hay que tener en cuenta que las pruebas automáticas sólo cubren aquello en lo que hemos pensado a priori, durante la creación de las pruebas (o el diseño del sistema, si estás usando TDD), pero en cualquier caso es posible (y hasta frecuente) que en ese momento estemos pasando cosas por alto.

Mi opinión es que los primeros que deben realizar estas pruebas manuales son los propios desarrolladores.

Pero, ¿para eso están los testers, no?

Pues sí y no. La realidad es que la mayoría de las veces, y más en España donde el número de pequeñas empresas es muy elevado, eso de contar con un equipo independiente de testers es pura fantasía.

Por otra parte, los testers no tienen el mismo conocimiento sobre el código de la aplicación que quien lo está creado o modificando, por lo que tienen mucho más difícil evaluar algunos riesgos estructurales como los que mencionaba al hablar de planificación y riesgos.

También hay que tener en cuenta que, cuando implementamos algo, sabemos mejor que nadie los puntos flojos de la implementación, los casos que podemos considerar límites y las partes que remotamente se podrían ver afectadas por los cambios.

Muchas veces el tester se centra en las cosas nuevas que se han implementado, en lo que aparece en el changelog de la versión, y es lógico y correcto, pero eso puede provocar que pasen desapercibidos bugs de regresión que hemos introducido en otras partes del sistema que, teóricamente no se han modificado, pero cuya implementación se ha podido ver afectada por las nuevas funcionalidades.

Sí, ya sé que con un montón de test automatizados se supone que evitamos estos riesgos, pero seamos realistas y admitamos que muchas trong>a veces no es viable escribir tests automáticalgunas ciertas cosas (sigo pensando en el interfaz de usuario). Además los tests tampoco son infalibles (no dejan de ser código escrito por nosotros) y seguramente sea más complicado escribir buenos tests que escribir buen código.

Donald Knuth decía:

Cuidado con los errores en el código anterior; sólo he demostrado que es correcto, no lo he probado.

Él hablaba de verificación formal, pero parafraseándole:

Cuidado con los errores en el código anterior; sólo he comprobado que pasa los tests, no lo he probado.

Suele ser más productivo probar las cosas justo cuando las hemos acabado de implementar, sin dejar pasar mucho tiempo, para tener todavía frescos los cambios que hemos hecho, han pasado los tests, pero no estamos muy seguros de que vayan a funcionar realmente.

Entonces, ¿pasamos de los testers y lo hacemos todo nosotros?

Ni por asomo. Si tienes la suerte de contar con gente dedicada a probar la aplicación, aprovéchalo.

La gente que está acostumbrada a probar aplicaciones, sabe mejor que tú cómo romper una aplicación y qué cosas suelen fallar o se te suelen pasar por alto (casos típicos: teclas rápidas en los diálogos, orden de tabulación, …).

Además, hay un factor muy importante: se trata de una persona distinta de la que ha implementado la funcionalidad que está probando. Eso hace que parta sin prejuicios y explore caminos que a ti seguramente se te hayan pasado por alto: combinaciones de teclas raras, errores en la entrada de datos, distintas formas de interactuación con el sistema, etc.

La estrategia que encuentro más efectiva es combinar pruebas manuales exhaustivas durante el desarrollo realizadas por el propio equipo de desarrollo, con una fase posterior de pruebas por parte de los testers. De esta forma se consiguen detectar más errores antes de que la aplicación llegue a los usuarios y se obtiene un software de mejor calidad.

Habrá quien piense que tener al equipo de desarrollo probando aplicaciones es un mal uso de recursos porque, en general, las horas de programador son más caras que las horas de probador, pero no hay que olvidar que también los fallos detectados en producción son más caros de corregir que los detectados en desarrollo, así que seguramente emplear parte del tiempo de desarrollo en asegurarse de que las cosas funcionan, no sea tan mala idea.

Compartir:
Facebook Twitter Email Linkedin Plusone

Cuando uno empieza a mezclar ciertas cosas tiene que andarse con bastante ojo para no liarla. La programación concurrente es una de esas cosas que, en cuanto requiere compartir información entre procesos/hebras (de la forma que sea), se convierte en algo que hay que tratar con cariño para evitar introducir condiciones de carrera (race conditions).

Las hebras

Una condición de carrera (tuve un profesor que la llamaba carrera crítica, que me parece un nombre mucho más bonito), se produce cuando dos hebras ejecutan una secuencia de instrucciones en un orden que no teníamos previsto, lo que genera fallos bastante difíciles de reproducir. El caso más habitual son dos hebras accediendo a una misma variable y “pisándose” el valor que acaban de escribir.

Todo esto viene a cuento porque hace poco tuve que depurar un fragmento de código con una pinta parecida a ésta:

public class Wrapper
{
	private int current;
	private int total;
	private Control control;

	// Más atributos, propiedades, métodos...

	public void Start(int param1, int param2)
	{
		SomeFunctionInBackgroundThread(
			param1, param2,
			(newCurrent, newTotal) =>
			{
				this.current = newCurrent;
				this.total = newTotal;

				// Unas cuantas cosas más...

				if (this.total > 0)
					control.BeginInvoke((Action)(() =>control.Text = (current/total).ToString("p")));
			});
	}
}

En el método Start se invocaba una función en una dll nativa a la que se le pasaba una función callback que era invocada cada cierto tiempo para informar del progreso. La función callback lo único que hace es actualizar los valores almacenados con el estado del progreso y actualizar el texto de un control, para lo que necesita usar BeginInvoke y redirigir la llamada a la hebra de interfaz de usuario.

Cuando acaba el proceso, la dll nativa invocaba la función callback pasando como parámetro newTotal == 0 para indicar que ha finalizado. Para evitar la división por 0, antes de lanzar el BeginInvoke comprobamos que total > 0 y ya está, ¿no? Pues no, eso no funciona.

Los cierres lambda

Al usar una expresión lambda C# estamos creando un cierre lambda sobre las variables que referenciamos en la expresión. En un cierre lambda, aquellas variables que no son locales a la función (en este caso, a la expresión lambda) son enlazadas a variables del contexto exterior, en este caso, al método que contiene la expresión lambda.

Para invocar BeginInvoke estamos pasando una expresión lambda:

control.BeginInvoke((Action)(() =>control.Text = (current/total).ToString("p")));

A primera vista, podría parecer que el cierre lambda se realiza sobre las variables control, current y total, pero ojo que no se trata de variables locales. Realmente el cierre se realiza sobre la referencia implícita a this, ya que en realidad total es this.total.

¿Y esto que quiere decir? Esto quiere decir que, si mezclamos la forma en que se realiza el cierre lambda con las condiciones de carrera que contábamos antes, tenemos un problema. Puede ser que para cuando se ejecute la expresión pasada a BeginInvoke (que se ejecuta de forma asíncrona) se haya vuelto a invocar la función callback y el valor de total haya pasado a ser 0.

La solución es muy sencilla: se puede meter el if dentro de la expresión lambda o usar las variables locales de la función callback, que como son locales no se van a ver modificadas por las siguientes invocaciones de la función callback, pero me costó un rato verlo.

Moraleja

La programación concurrente no es trivial. Si puedes evitar compartir estado, mejor. Si puedes evitar estado mutable, mejor.

Los cierres lambda son poderosos, pero ten cuidado de estar seguro de lo que cierras. Los bugs derivados de mezclar cierres lambda con programación concurrente son muy divertidos de depurar.

Compartir:
Facebook Twitter Email Linkedin Plusone

Después de hablar sobre diseño web sensible, ha llegado el momento de ser consecuente y predicar como el ejemplo. El resultado es el nuevo aspecto del blog que (probablemente) estás viendo ahora mismo. Mi diseñadora está muy ocupada, por lo que estéticamente no es todo lo bonito que me gustaría, pero por lo menos ahora el 10% de visitantes que acceden desde un dispositivo móvil podrán leerlo más cómodamente. Si eres de los que me siguen a través de un lector rss o planetacodigo.com, anímate a echar un vistazo al nuevo aspecto de Koalite.

Como no soy ningún experto en PHP, ni en WordPress, ni en diseño web en general, he hecho lo único sensato que podía hacer: aprovecharme del trabajo de los que sí saben. El tema está basado en un 90% en WordPress Bootstrap, que a su vez usa Bootstrap y Bones. Sin duda, una muestra más de lo que se puede conseguir con el código abierto :-)

Nunca había trasteado tanto con WordPress (y eso que casi ni he arañado la superficie), pero ha sido una experiencia entretenida y me ha servido para ver algunas cosas:

  • PHP es feo. Lo siento, seguro que hay gente a la que le encanta, pero estéticamente me resulta poco agradable, eso de poner el $ en las variables no me gusta nada (me pasa lo mismo con Powershell) y la mezcla de PHP + HTML me resulta complicada de leer (echo de menos la limpieza de razor o jade).
  • Las APIs de WordPress están bastante conseguidas. Tienen un montón de puntos de extensión y están bastante bien pensadas para construir cosas a partir de ellas.
  • Me queda la sensación de que la mayoría de las páginas lanzan como el triple de consultas a la base de datos de las que son necesarias e intuyo unos select n+1 por ahí que no me gustan nada, pero como no sé mucho de WordPress a lo mejor me equivoco y, en cualquier caso, para un sitio como este me sobra.
  • Fiver, el tema que usaba hasta ahora, es un prodigio de minimalismo y elegancia, al menos en cuanto código. Es difícil conseguir más con tan poco.
  • Siempre, siempre, pase lo que pase, por pequeño que sea el proyecto que estés haciendo, usa algún sistema de control de código fuente. Pese a las tres tonterías que he tocado, más de una vez me ha salvado tener el proyecto en github para poder dar marcha atrás o comprobar que cambio había roto algo.
  • Cuando no sabes lo que estás haciendo, todo parece fácil hasta que se vuelve difícil. Hacía mucho que no tenía la sensación de estar ñapeando tanto y me siento un poco “sucio” por ello.

Todavía hay bastantes cosas que no me acaban de convencer y que es posible que cambie (si es que no me acabo acostumbrando a verlas así, que también puede pasar). Sobre todo la cabecera del blog y los pies de los posts son partes que me gustan más bien poco.

Seguro que hay cosas que no funcionan bien y tampoco he hecho unas pruebas demasiado exhaustivas, así que si ves algo raro, sería un bonito detalle que me avisaras en los comentarios o con un email a juan(punto)hernandez(punto)arroyo(arroba)gmail(punto)com.

El tema está subido en github como koalite-bootstrap, aunque me temo que no es usable directamente para otro blog porque hay muchas cosas preparadas específicamente para éste, con sus plugins, sus imágenes y sus cosas. De todas formas, teniendo en cuenta lo mucho que me he aprovechado del trabajo de otros, lo mínimo que podía hacer era colgarlo en algún sitio por si le sirve a alguien, aunque sólo sea para echarse unas risas con las barbaridades que he podido hacer.

Compartir:
Facebook Twitter Email Linkedin Plusone

En C#, como en cualquier lenguaje compilado, cuando llega el momento de usar una librería tenemos siempre dos opciones:

  1. Utilizar una dependencia binaria, es decir, referenciar el assembly que contiene las clases que queremos utilizar.
  2. Copiar y pegar el código de las clases que queremos utilizar en nuestro proyecto y compilarlas junto con el proyecto.

Hasta hace no mucho tiempo, la primera opción parecía la forma más ortodoxa y andar copiando código de un sitio a otro podía llegar a verse como un “atajo”, algo que podías hacer pero que, en el fondo, sabías que estaba mal y que deberías convertir en una librería “de verdad” porque cuando hubiera que cambiar algo tendrías que ir proyecto a proyecto corrigiéndolo.

Las dependencias binarias son una opción más cómoda cuando se trata de librerías grandes, porque andar copiando y pegando 400 clases es bastante incómodo, sobre todo si lo que estamos copiando está minimamente organizado y las 400 clases están repartidos en otros tantos archivos que, a su vez, se distribuyen en varias carpetas. Algunos han pensado en esto e incluso hay una herramienta, tape, que permite fusionar esos 400 ficheros es un único .cs listo para añadir al proyecto, aunque el propio autor dice que no la ha probado extensivamente.

La segunda opción, copiar y pegar el código, es algo que, en el fondo, casi todos hemos estado haciendo continuamente. Hay algunos fragmentos de código que usamos en casi todos los proyectos pero que no llegan a tener la categoría de librerías. A veces acaban en una librería utils.dll o similar, pero en general suelen copiarse (y modificarse ligeramente) de un proyecto a otro. A priori, esta segunda opción puede parecer peor idea ya que estamos duplicando código en un montón de sitios, y todos sabemos que duplicar código es malo, DRY y todas esas cosas.

Sin embargo, con la llegada de mejores gestores de paquetes como NuGet, que simplifica el proceso de copia de los ficheros implicados en el proyecto de destino, y la “moda” de los micro-frameworks (Massive, TinyIoC, etc.), que hace que los frameworks sean más pequeños y manejables, la segunda opción empieza a ganar adeptos.

Duplicar el código tiene sus ventajas:

  • Reduce el número de assemblies a desplegar. Aparte de mejorar algo el rendimiento, simplifica el despliegue de la aplicación y simplificar cosas es siempre bueno. Es cierto que se pueden usar herramientas como ilmerge para integrar las librerías en el proyecto, pero siempre es una complicación adicional.
  • Facilita la depuración. Al incluir el código fuente en el proyecto es más sencillo depurar. Claro que puedes incluir símbolos de depuración con una librería binario, pero es mucho más sencillo depurar directamente código fuente y eso siempre funciona; con los símbolos a veces Visual Studio se pone un poco más pesado para dejarte depurar si no tienes el código fuente por ahí.
  • Evita dependencias ocultas entre aplicaciones.

    Si tenemos una librería que usamos en dos aplicaciones distintas y queremos cambiarla para añadir cosas necesarias en una de las aplicaciones, tenemos que tener cuidado para que estos cambios no afecten a la otra aplicación. Es de suponer que en un futuro la otra aplicación se actualizará a la nueva versión de la librería y no sería agradable que se rompiera.

    Al tener el código compartido entre dos aplicaciones diferentes, estamos obligados a valorar con mucho más cuidado cada cambio que hacemos en la librería. Por supuesto tenemos la opción de hacer un fork del código inicial y tener distintas ramas de la librería por cada aplicación pero en ese caso, ¿qué ventaja tendría crear una librería común?

Viendo esto parece que sea mucho mejor, copiar y pegar el código que usar una librería compilada, pero no hay que olvidar que usar una librería compilada tiene también sus ventajas:

  • Evitamos duplicar código, por lo que cuando se produce un problema o queremos introducir una mejora, sólo tenemos que tocar en un sitio.
  • Tenemos más herramientas para controlar la visibilidad de las clases (internal) y eso puede ayudar tener un API más clara.
  • Es más sencillo desplegar nuevas versiones porque sólo hace falta copiar una dll en lugar de varios archivos de código fuente.
  • Se reduce el tiempo de compilación.

Cuándo usar cada tipo

A la hora de decidir la forma en que usamos una dependencia la primera variable a tener en cuenta es cómo se distribuye. Por ejemplo, si quieres usar NHibernate, es un bastante incómodo copiar su miles de clases en tu proyecto, así que no seas terco y usa la dll que ya te dan compilada. Si vas a usar Massive, no compiles una dll sólo para él y añádelo directamente al proyecto que contenga el código de acceso a datos.

En cuanto a las librerías que se mantienen internamente dentro de una organización (empresa/equipo/etc.), depende mucho del tipo de dependencia:

Para librerías grandes, de propósito general, en las que estés dispuesto a invertir tiempo manteniéndolas estables, compatibles hacia atrás y con una calidad elevada, merece la pena crear dependencias binarias por su facilidad a la hora de añadirlas a otros proyectos y la ventaja de poder corregir bugs para todos los proyectos que usan la librería.

Cuando sea algo que parece que es general, pero de momento sólo vas a usar en una aplicación, no crees una librería, es una pérdida de tiempo. Cuando encuentres un problema en esa aplicación, en lugar de poder corregirlo allí tienes que irte a la librería, corregirlo, generar una nueva versión y actualizarla en el proyecto de la aplicación. Mantén el código en la aplicación y ya veremos que pasa en el futuro. YAGNI.

Si la librería es pequeña o está compuesta por muchas pequeñas clases de utilidad, cópiala y pégala de un sitio a otro. Es bastante probable que un mismo proyecto no necesite todos esos extension methods tan chulos que has creado, sino sólo una parte de ellos, y es también bastante probable que incluso parte de ellos tengan que ser ligeramente modificados para usarlos en ese proyecto. No merece la pena el esfuerzo de intentar cubrir el caso general cuando copiar/pegar/adaptar es muchas veces la opción más rentable.

Conclusiones

Puede parecer que hay más argumentos a favor de copiar y pegar que de crear librerías reutilizables y seguramente sea así porque realmente no hay tantas oportunidades como a nosotros nos gustaría de reutilizar el código de un proyecto a otro sin realizar ningún cambio. Más de una vez me ha pasado pensar que parte de un proyecto era “digna” de convertirse en librería para luego, con el tiempo, ver que sólo se usa en el proyecto original, complicando la corrección de fallos y la implementación de nuevas mejoras.

Por otra parte, hay componentes que tiene sentido convertir en librerías, bien porque son muy estables en el tiempo, muy genéricos, o tan útiles que merece la pena luchar por mantener su flexilbidad y compatibilidad. En ese caso, cuando estés seguro de que merece la pena, la opción de la librería es buena, pero no intentes crear el super framework universal, eso no suele funcionar.

Compartir:
Facebook Twitter Email Linkedin Plusone

Ya he hablado en otras ocasiones sobre la importancia de decidir qué es exactamente lo que vamos a hacer cuando estamos desarrollando software. Incluso a veces me atrevería a decir que tan importante como saber lo que vamos a hacer es saber lo que NO vamos a hacer.

Hace poco he tenido que planificar las funcionalidades que íbamos a incluir la siguiente versión de una de las aplicaciones estándar que desarrollamos en la empresa para la que trabajo y, quizá por experiencias no muy positivas en versiones anteriores (hay que ser positivos, de todo se aprende), he acabado planteándome un factor que a veces se ignora durante las planificaciones: el riesgo.

Me refiero al riesgo en el sentido más amplio de la palabra, no al riesgo que aparece en los Diagramas de Gantt y otras herramientas típicas de ingeniería del software (disciplina de la que, por cierto, ya hablaré en algún momento).

En toda aplicación existen partes funcionalmente más importantes que otras. Si estás desarrollando un procesador de texto, es mucho más importante poder guardar un documento que poder cambiar el interlineado entre párrafos, por ejemplo. Obviamente realizar desarrollos que afectan a las partes más importantes de la aplicación supone un riesgo mayor, aunque también hay ocasiones en que permiten obtener mayores beneficios.

Además, los que nos encargamos de hacer la aplicación somos conscientes de que hay partes de la aplicación que, internamente, son más complejas de tocar. A veces esto se debe a que están mal diseñadas, en cuyo caso podemos considerarlas como deuda técnica (technical debt) y deberíamos reservar un hueco para refactorizarlas; pero otras veces se trata simplemente de tareas inherentemente complejas que no podemos (o no sabemos) simplificar.

Modificar estas partes más complicadas de tocar supone un riesgo mayor, porque es más fácil introducir fallos y, en ocasiones, también es más difícil encontrarlos durante las pruebas (incluyo aquí test unitarios, de integración, manuales, betas, todo lo que estés usando para controlar la calidad).

Por último, algunos desarrollos son arriesgados de por si debido a que son más complicados de realizar (aunque afecten a partes de la aplicación fáciles de tocar), porque no estamos seguros de si serán útiles o no, o por cualquier otro motivo.

Por desgracia, muchas veces este tipo de factores no se tienen en cuenta (o tan en cuenta como deberían) y cuando llega el momento de priorizar nuestro backlog de producto, sólo nos guiamos por cosas como:

  • El tiempo que se tarda en implementar la funcionalidad/historia-de-usuario/lo-que-uses.
  • Lo prioritario que es algo, ya sea basándonos en las peticiones de nuestros usuarios o en nuestra visión del producto.

Ambos son factores razonables. El primero es que va a marcar si tendremos tiempo o no de implementar las cosas en la siguiente versión y el segundo actúa como “indicador” de valor para dar prioridad a aquellas cosas que se supone que van a generar más valor. Sin embargo, centrarnos sólo en esos dos factores sin tener en cuenta el riesgo que entraña cada implementación es… arriesgado.

Por una parte, podemos comprometer la estabilidad de la aplicación al añadir muchas áreas de incertidumbre simultáneamente y, creéme, prefiero un procesador de textos que no es capaz de cambiar el interlineado que uno que pierde mi trabajo cada 5 minutos.

Por otro lado, cuando añadimos muchas cosas con riesgo elevado en la misma versión/sprint/lo-que-uses, también estamos dificultando cumplir los objetivos de tiempo que tenemos impuestos, porque es más fácil que las cosas se compliquen durante la fase de implementación o pruebas y no podamos mantener la planificación original.

Hay veces que es necesario asumir riesgos y eso no es malo, pero lo que es importante es ser consciente de que se están asumiendo. Está claro que el que no se arriesga no consigue nada, pero también está claro que arriesgarse a lo loco es una receta clara para el fracaso.

A veces es preferible limitar los riesgos que asumimos en cada versión y avanzar más despacio, si eso nos permite mantener la aplicación más sólida y segura.

Compartir:
Facebook Twitter Email Linkedin Plusone

Hace unos días hablaba sobre inyección de dependencias en Javascript y hoy, echando un vistazo en microjs, he tropezado con un contenedor IoC para Javascript llamado Swiftcore.js.

Se trata de una librería bastante simple (que conste que lo digo como algo bueno) y he pensado que sería una buena oportunidad para para entender mejor cómo se podría implementar un contenedor IoC en Javascript. La documentación de Swiftcore.js es bastante clara, pero la mejor forma de comprender algo es jugar un poco con ello.

Vamos a ver un ejemplo. Supongamos que tenemos los dos componentes siguientes:

function Server() {
	this.getCustomer = function() {
		// This should be downloaded from a server
		return 'Paco Clavel';
	}
}

function ViewModel(deps) {
	var server = deps.Server;
	this.customer = server.getCustomer();
}

Hasta aquí todo (casi) normal. Tenemos una función, Server, para crear objetos que descargan datos desde un servidor, y otra función ViewModel, con la que crear objetos que usan los datos descargados para dejarlos listos para vista.

La primera cosa “curiosa” es que la dependencia de ViewModel sobre Server, en lugar de definirla como un parámetro de la función, está encapsulada en un objeto deps, que contendría todas las dependencias de esa función. Esta es la forma en que trabaja por defecto Swiftcore.js, encapsulando todas las dependencias de cada función en un único objeto.

Para registrar los componentes en el contenedor, tendríamos un código parecido a éste:

ViewModel.dependencies = ['Server'];

swiftcore.register('Server', Server);
swiftcore.register('ViewModel', ViewModel);

Las dos últimas líneas son muy similares a las que usaríamos en cualquier contenedor de un lenguaje estático, como Castle Windsor o similares. Cada componente se registra con su nombre y, aunque en el ejemplo no se ve, podríamos asignarle un ciclo de vida (singleton, transient, etc.).

Lo que resulta más extraño (e incómodo) es que tenemos que indicar explícitamente las dependencias de cada componente, como hacemos en la primera línea. Esto es así porque al definir la función ViewMode no tenemos forma de indicar los tipos de los argumentos, por lo que no hay forma directa de detectar las dependencias.

Podríamos intentar aplicar alguna convención para evitar tanta configuración, por ejemplo haciendo que los nombres de los parámetros coincidiesen con los nombres de los dependencias y hacer así un enganche automático. Sin embargo, esto no funcionaría correctamente con los minifiers de Javacript porque cambiarán los nombres de funciones y argumentos haciendo que esa convención se vaya al traste.

A la hora de resolver componentes del contenedor, el código es calcado de Castle Windsor (y el resto de contenedores que conozco):

var model = swiftcore.resolve('ViewModel');

Aprovechando que Swiftcore.js usa QUnit para sus propios tests, todo este código lo he metido en un test para ver si funcionaba, quedando algo así:

test('simple', function() {
	function Server() {
		this.getCustomer = function() {
			return 'Paco Clavel';
		}
	}

	function ViewModel(deps) {
		var server = deps.Server;
		this.customer = server.getCustomer();
	}

	swiftcore.register('Server', Server);

	ViewModel.dependencies = ['Server'];
	swiftcore.register('ViewModel', ViewModel);

	var model = swiftcore.resolve('ViewModel');
	ok(model.customer === 'Paco Clavel');
});

El test pasa, así que podemos estar contentos con nuestra primera incursión en el mundo de los contenedores IoC para Javascript.

Conclusiones

Aparte de para entretenerme un rato, este pequeño experimento me ha servidor para verificar algunas de las cosas que ya comentaba al hablar de inyección de dependencias en Javascript.

Para mi, la mayor limitación de estos contenedores es que necesitamos indicar las dependencias explícitamente. Podríamos decir que en C# también pasa eso porque las estamos indicando al definir los tipos de los parámetros de los constructores, pero mientras que en C# eso es algo natural, en Javascript resulta poco idiomático.

Además, en C# las dependencias quedan definidas en el propio constructor de la clase, mientras que aquí están definidas en otro punto distinto de la aplicación, por lo que, si se añade una dependencia a un componente, hace falta acordarse de añadirla también en su definición de dependencias.

Por otra parte, el hecho de tener que pasar todas las dependencias encapsuladas en un único objeto es algo que no me gusta. Es cierto que eso no es algo genérico de todos los contenedores IoC para Javascript, e incluso en Swiftcore.js se puede modificar, pero el hecho de que sea la opción por defecto no me convence. De las dos opciones, me parece mucho más clara la primera:

// ¿Es más legible esto...
function Component(dependency1, dependency2) {}
// ... o esto?
function Component(deps) {
    var d1 = deps.Dependency1;
    var d2 = deps.Dependency2;
}

Pese a todo, seguro que hay situaciones en que un contenedor IoC para Javascript puede ser útil. Cuando me encuentre con una ya tengo un candidato :-)

Compartir:
Facebook Twitter Email Linkedin Plusone

Cuando trabajamos con lenguajes dinámicos como Javascript los tests son mucho más importantes que en lenguajes estáticos. Ahora que he empezado a usar mas Javascript en la vida real y tener cosas más complejas que aplicaciones básicas con node.js o Knockout, mantener el código funcionando se hace más complicado y los tests más necesarios.

La principal razón por la que los tests son más necesarios se porque no hay una fase de compilación como tal. Esto provoca que no podamos detectar fallos hasta el momento de ejecutar la aplicación, pero sobre todo impide aprovechar las capacidades de análisis estático de un compilador moderno.

Si usamos un lenguaje estático como C#, el primer control de errores lo realiza el compilador. Para poder ejecutar la aplicación antes debe compilarse y, durante este proceso, el compilador es capaz de detectar infinidad de errores: variables no declaradas, signaturas de métodos incompatibles, comprobación de tipos de parámetros, restricciones de tipos en tipos genéricos, etc. Sin un compilador, todos estos errores no se descubrirán hasta el momento de ejecutar la aplicación.

Además, en general las herramientas disponibles no son tan completas como las que encontramos en .NET y el soporte que tienen para tareas habituales, como refactorizaciones sencillas (extraer parámetro, renombrar símbolo, etc.), es bastante limitado. Debido a esto, las refactorizaciones se realizan de forma manual, lo que puede provocar errores.

Otro factor a tener en cuenta es que, al aprovechar las características de un lenguaje dinámico todo se vuelve muy… dinámico. Por ejemplo, al usar literal objects estamos creando objetos cuya definición no está en ninguna parte, no hay una definición de la clase a la que acudir para ver qué propiedades o métodos tiene el objeto. En general esto no es problema si sólo creas ese tipo de objetos en un punto de la aplicación, pero como te descuides y empieces a crearlos en varios puntos…

También se da el caso de una función cuyos parámetros pueden ser de varios tipos y hacer cosas diferentes en función de los tipos recibidos. Un ejemplo muy claro es la función jQuery o $, cuyo primer argumento puede ser un string con un selector CSS o un HTMLElement.

Estas características tan dinámicas son muy potentes, pero a cambio pueden convertirse en complicadas de mantener rápidamente. El hecho de no declarar los tipos de los parámetros puede restar legilibidad al código y obliga a mantenerlo documentado. Cuando uso un lenguaje estático, casi nunca escribo un comentario porque entre los tipos y los nombres de las cosas, el código casi se documenta sólo. Sin embargo, al trabajar con Javascript acabo escribiendo más comentarios para poder indicar qué tipos de datos espera cada parámetro de una función, ya que si no lo hago la única forma de saber los tipos soportados es leer el código de función.

Todos estos factores hacen que el uso de test (unitarios, de aceptación, de integración, de lo que sea) se convierta en algo fundamental para poder mantener funcionando bases de código más o menos complejas. Aparte de ayudar al diseño (si usas TDD) o comprobar la validez del programas (con tests de aceptación), los tests actúan como red de seguridad al refactorizar y sirven de documentación para las distintas partes de la aplicación.

Dependiendo del tipo de tests a ejecutar, podemos usar distintas herramientas, como QUnit para test unitarios en el browser, Jasmine para aplicar BDD sin necesidad de usar un browser (con node.js) o Watin para crear tests de integración.

Por desgracia, la ejecución de tests en Javascript y, especialmente, su uso en un proceso de integración continua, no siempre es una tarea fácil. Además, hay que tener en cuenta que la implementación de Javascript en cada browser presenta algunas diferencias, como vimos hace poco con el tema del parsing de fechas. Esto hace que, para estar seguros de que todo funciona, debamos ejecutar los tests sobre varios browsers distintos, lo que supone una complejidad adicional.

Pese a estas complicaciones, sigo convencido de que merece la pena hacer un esfuerzo por testear el código Javascript porque se le saca bastante partido a los tests. Como decía en mi anterior post, cada vez hacemos aplicaciones más complejas en Javascipt y como tales debemos tratarlas.

Compartir:
Facebook Twitter Email Linkedin Plusone

Desde que empecé a jugar con Javascript hace ya unos cuantos meses no he podido evitar comparar continuamente la forma en que se hacen las cosas en Javascript, un lenguaje dinámico, con la forma en que se hacen las cosas en C#, un lenguaje estático que además es el entorno en que mejor me muevo.

Obviamente he encontrado cosas que me gusta más hacer en Javascript y cosas que me parecen más cómodas en C#, pero en este caso, más que de una cuestión de tareas concretas me gustaría tratar un asunto más general: la inyección de dependencias.

En lenguajes estáticos la inyección de dependencias es necesaria muy útil para poder cambiar la implementación usada de cada componente, que suele estar representado por un interfaz. Es una técnica muy habitual para poder escribir tests unitarios (y a veces únicamente para eso, cosa que algunos creen que no es una buena idea).

En el caso de un lenguaje dinámico como Javascript, en el que es posible redefinir al vuelo los métodos de una clase, se puede conseguir cambiar la implementación fácilmente sin necesidad ni de crear interfaces (que, de hecho, no existen), ni nuevas implementaciones de interfaces (es lógico, si no existen, es difícil crear nuevas implementaciones).

Pero la inyección de dependencias no sólo es útil cuando tenemos varias implementaciones de un interfaz. La inyección de dependencias por si misma ayuda a crear mejor código, más estructurado, con clases de responsabilidad más definida y más desacopladas entre sí, ya que la inyección de dependencias independiza a las clases de elegir cuáles van a ser sus colaboradores.

¿Cómo se implementa la inyección de dependencias en Javascript? Pues igual que en cualquier otro lenguaje. La inyección de dependencias consiste en pasar a un objeto sus dependencias como parámetros en el constructor, lo que se puede hacer sin problemas en Javascript.

Veamos un ejemplo sencillo. Si tenemos un ViewModel de Knockout que debe cargar datos de un servidor externo usando AJAX, podríamos implementar algo así:

function ViewModel() {
	var self = this;
	self.books = ko.observableArray();
	// ... some other observable properties

	self.loadByCategory = function(category) {
		$.ajax({
			url: '/api/category/',
			type: 'POST',
			dataType: 'json',
			data: JSON.stringify({category: category}),
			contentType: 'application/json; charset=utf-8',
			success: function(booksInThisCategory) {
				self.books(booksInThisCategory);
			},
		});
	};
}

En este ejemplo el ViewModel está mezclando dos resposabilidades diferentes, representar la información de una forma adecuada a la vista y descargar la información de un servidor. Esto es una clara violación del SRP (Principio de Responsabilidad Única) y podemos solventarlo separando la responsabilidad en dos objetos diferentes:

function Server() {
	this.findBooksByCategory(category, success) {
		$.ajax({
			url: '/api/category/',
			type: 'POST',
			dataType: 'json',
			data: JSON.stringify({category: category}),
			contentType: 'application/json; charset=utf-8',
			success: success,
		});
	};
}

function ViewModel(server) {
	var self = this;
	self.books = ko.observableArray();
	self.loadByCategory = function(category) {
		server.findBooksByCategory(category, function(booksInCategory) {
			self.books(booksInCategory);
		};
	};
}

var server = new Server();
var model = new ViewModel(server);

Ahora la responsabilidad de descargar datos recae en el objeto server, independizando ViewModel de la procedencia de esos datos. Este ejemplo puede resultar poco idiomático para Javascript porque está calcado de lo que se haría en C#, pero pensad que server no tiene porque ser una instancia de la clase Server, podría ser cualquier objeto siempre y cuando tuviera un método findBooksByCategory.

Es la ventaja de Javascript, al ser un lenguaje dinámico, podemos aprovechar el duck typing y no hace falta definir el tipo de la dependencia.

Hasta el momento no he dicho nada de contenedores de inversión de control. Como ya he explicado anteriormente no hace falta un contenedor IoC para hacer inyección de dependencias. Además, en los lenguajes dinámicos se pierde parte de la gracia de los contenedores porque en los constructores de las clases no está especificado el tipo de las dependencias y si queremos usar un contenedor nos obliga a definir de forma un tanto artificial las dependencias entre nuestros objetos para que el contenedor pueda componer unos con otros.

El duck typing que antes veíamos como una ventaja, se convierte en un inconveniente a la hora de utilizar contenedores de inversión de control. De todas formas, existen contenedores IoC para Javascript como wire.js o squirrel y seguramente haya casos en los que merezca la pena utilizarlos.

En cualquier caso, y aunque a algunos no les haga mucha gracia, está claro las aplicaciones desarrolladas con Javascript + HTML + CSS son cada vez más numerosas y también más complejas/completas. Es importante recordar que estamos desarrollando software, no ñapeando una página web, y por tanto debemos aplicar lo que ya sabemos sobre desarrollo de software (SOLID, DRY, KISS y todas las siglas que tanto nos gustan).

Compartir:
Facebook Twitter Email Linkedin Plusone