jueves, 31 de marzo de 2011

Inyección de Dependencias

Como colofón de la serie de cinco artículos dedicados a los principios SOLID, en esta ocasión toca hablar del Principio de Inyección de Dependencias (Dependency Inyection, DI).

Introducción

Si nos remontamos a los primeros años de la programación, nos encontraremos con programas rígidos repletos de código monolítico y lineal. La propia evolución hizo aparecer conceptos hoy por hoy imprescindibles como la modularidad y la reutilización de componentes, conceptos fundamentales en el paradigma de la Programación Orientada a Objetos.

La modularidad y reutilización de clases conlleva un flujo de comunicación entre instancias cuyo mal uso deriva en un hándicap que limita la flexibilidad, robustez y reusabilidad del código debido a la dependencia o alto acoplamiento entre las clases.

En la figura 1 podemos ver un sencillo diagrama de clases de un sistema de adquisición y control de datos meteorológicos. Existen dos clases participantes: una para la captura de la temperatura, y otra que representa a la estación meteorológica. Ambas tienen una responsabilidad a la hora de mostrar los datos, como puede apreciarse en el listado 1.

Listado 1
public class EstacioMeteorologica
{
public void MostrarDatos()
{
Console.WriteLine(
string.Format("Datos a {0} n", DateTime.Now));
Termometro termometro = new Termometro();
termometro.MostrarTemperaturaActual();
}
}
public class Termometro
{
public int Valor { get; set; }
public void MostrarTemperaturaActual ()
{
Console.WriteLine(
string.Format("Temperatura: {0} º", Valor));
}
}

Identificando el problema

Cuando hablamos en términos de calidad, solemos utilizar los adjetivos "bueno" o "malo" para definir la calidad de un diseño. Sin embargo, no siempre utilizamos los argumentos o criterios que sustentan la afirmación "éste es un mal diseño". Existe un conjunto de criterios más allá del siempre subjetivo TNTWIWHDI (That’s Not The Way I Would Have Done It, "Yo no lo habría hecho así") acuñado por Robert C. Martin, y son los que miden el nivel de rigidez, la fragilidad y la inmovilidad del sistema.

En nuestro ejemplo de la estación meteorológica, podemos afirmar que el diseño es rígido, porque cualquier cambio será difícil de llevar a cabo, ya que no conocemos el impacto que la modificación de una clase de bajo nivel (clase Termometro) tendrá sobre la clase de alto nivel (clase EstacioMeteorologica).

Cuando los cambios tienen una repercusión en otras entidades, no necesariamente dependientes, se dice que un sistema o aplicación es frágil. Si nos fijamos en el listado 1, la clase EstacioMeteorologica depende tanto de Termometro como de System.Console. Un cambio del flujo de salida de datos del programa (por ejemplo, a una impresora en lugar de System.Console) repercutiría en las clases de bajo nivel.

El termino inmovil lo utilizamos para medir el nivel de dependencia entre una parte del diseno y otros datos no directos. El ejemplo es inmovil porque la clase Estacio] Meteorologica depende de las clases Termometro y System. Console para mostrar los datos. Dicho en otras palabras, no podriamos extraer la clase de mayor nivel y utilizarla con otras entidades. Lo mismo pasaria con la clase de bajo nivel por su dependencia de System.Console.

Nota: Entre los criterios que permiten determinar si un diseño es bueno o malo están los que miden su nivel de rigidez, fragilidad e inmovilidad.

Planteemos un nuevo diseño a nuestro sistema. En primer lugar, eliminemos la dependencia que la clase Termometro tiene de System.Console, ya le que estamos otorgando la responsabilidad de salida por pantalla cuando realmente no le corresponde. El resultado sería el que se muestra en el listado 2.

Listado 2
public class EstacioMeteorologica
{
public void MostrarDatos()
{
Termometro termometro = new Termometro();
string temperatura =
termometro.MostrarTemperaturaActual();
Console.WriteLine(
string.Format("Datos a {0} n{1}",
DateTime.Now, temperatura));
}
}
public class Termometro
{
public int Valor { get; set; }
public string MostrarTemperaturaActual ()
{
return string.Format("Temperatura:{0} º", Valor);
}
}

Ahora la clase Termometro ha quedado libre de dependencias, y por tanto es reutilizable. Sin embargo, aún EstacioMeteorologica depende tanto de System.Console como de Termometro. Por otro lado, la clase Termometro no es más que una representación de un valor referencial meteorológico cualquiera; por tanto, podríamos abstraer la interfaz IMeteoReferencia, tal y como se muestra en el listado 3, y hacer que la clase Termometro la implemente. Esto es un ejemplo de aplicación del patrón Fachada (Façade), mediante el cual simplificamos la firma de varias clases a través de una única interfaz.

Listado 3
public interface IMeteoReferencia
{
int Valor { get; set; }
string Mostrar();
}
public class Termometro : IMeteoReferencia
{
public int Valor { get; set; }
public string Mostrar()
{
return string.Format("Temperatura:{0} º", Valor);
}
}

Ahora que hemos abstraído la interfaz, ésta nos servirá como contrato para las clases que quieran utilizarla. Esto nos permitirá desacoplar la clase EstacioMeteorologica de Termometro, tal y como muestra el listado 4.

Listado 4
public class EstacioMeteorologica
{
private IMeteoReferencia termometro;
public EstacioMeteorologica()
{
termometro = new Termometro();
}
public void MostrarDatos()
{
Console.WriteLine(
string.Format("Datos a {0}", DateTime.Now));
Console.WriteLine(termometro.Mostrar());
}
}

Sin embargo, aún no hemos solucionado el problema, pese a que estamos más cerca. Lo que pretendemos es eliminar completamente la instanciación de la clase Termometro, y la solución pasa por inyectar la dependencia directamente a través del constructor, como se muestra en el listado 5.

Listado 5
public class EstacioMeteorologica
{
private IMeteoReferencia termometro;
public EstacioMeteorologica(
IMeteoReferencia termometro)
{
this.termometro = termometro;
}
public void MostrarDatos()
{
Console.WriteLine(
string.Format("Datos a {0}", DateTime.Now));
Console.WriteLine(termometro.Mostrar());
}
}

El Principio de Inyección de Dependencias

Robert C. Martin afirma en el Principio de Inyección de Dependencias:

A. Las clases de alto nivel no deberían depender de las clases de bajo nivel. Ambas deberían depender de las abstracciones.
B. Las abstracciones no deberían depender de los detalles. Los detalles deberían depender de las abstracciones.

Imaginemos por un momento la solución inicial de la estación meteorológica (listado 1). La clase de alto nivel EstacioMeteorologica depende de la clase de bajo nivel Termometro (o Barometro, Anemometro, etc.). Toda la lógica de la solución se implementaría en la clase de alto nivel, y cualquier modificación en las clases de bajo nivel tendría repercusión no únicamente sobre la definición de la clase de alto nivel, sino sobre la propia lógica de la aplicación, llegando incluso a forzar cambios en la misma, cuando debería ser la clase de alto nivel la que debería forzar el cambio a las clases de bajo nivel sin comprometer la lógica de la aplicación; es decir, justamente lo contrario. Además, la clase de alto nivel sería difícilmente reusable debido a este acoplamiento. Sencillamente, y resumiendo, la clase EstacioMeteorologica no debe depender de la clase Termometro; en todo caso, al contrario.

Existen tres formas de implementación de la Inyección de Dependencias:

  • por constructor
  • por setter
  • por interfaz.
El primer caso lo hemos visto en la sección anterior, donde hemos inyectado la dependencia a través del constructor de la clase; el listado 6 muestra una generalización. La inyección por setter se realiza a través de una propiedad de la clase (listado 7); y por último, la inyección por interfaz se realiza a través de un método, recibiendo como parámetro el objeto a inyectar (listado 8).

Listado 6
IMeteoReferencia referencia = ObtenerReferencia();
EstacioMeteorologica estacion =
new EstacioMeteorologica(referencia);

Listado 7
EstacioMeteorologica estacion = new EstacioMeteorologica();
estacioMeteorologica.Referencia = ObtenerReferencia();

Listado 8
EstacioMeteorologica estacion = new EstacioMeteorologica();
estacioMeteorologica.LecturaContador(ObtenerReferencia());

Inversión de control y contenedores

No podemos hablar de DI sin dejar de hablar de la Inversión de control (Inversion of Control, IoC). IoC también es conocido como Principio de Hollywood, nombre derivado de las típicas respuestas de los productores de cine a los actores noveles: "no nos llames; nosotros lo haremos".

IoC invierte el flujo de control de un sistema en comparación con la programación estructurada y modular. En el fondo, DI es una implementación de IoC. Aún hoy existe la discusión acerca de si IoC es un principio, un patrón o ambas cosas a la vez. IoC, en definitiva, es una característica fundamental de un framework, y de hecho lo que lo hace realmente diferente a una librería de funciones.

En escenarios de producción, las clases no son tan triviales como la que hemos presentado en este artículo. Imagine por un momento que la interfaz IMeteoReferencia tiene una implementación de IEntradaDatos e IVerificador, y éstas a su vez implementan otras interfaces. En realidad, obtendremos una jerarquía de dependencias (figura 3), cuyo manejo en tiempo de diseño es imposible de gestionar "manualmente"; es aquí donde entra a jugar el término contenedor IoC (IoC Container).

El principal cometido de un contenedor IoC, a diferencia de una factoría, es el de gestionar el ciclo de vida de los objetos. El contenedor IoC registra una implementación específica para cada tipo de interfaz y retorna una instancia de objeto. Esta resolución de objetos tiene lugar en un único punto de las aplicaciones; normalmente, a nivel de infraestructura.

Conclusión

Con este artículo, hemos tratado de mostrar de una forma práctica la relación existente entre dependencias, detalles y abstracciones. Con el Principio de Inyección de Dependencias, ponemos fin a la última de las siglas que componen SOLID. Existen libros íntegros que hablan de este principio, y podrá encontrar en Internet una gran cantidad de recursos relacionados.

A lo largo de esta serie sobre los principios SOLID, hemos presentado aspectos muy importantes que debemos tener en cuenta ante cualquier nuevo desarrollo, y hemos visto cómo muchas de las problemáticas lógicas del diseño pueden ser reducidas mediante la aplicación de estos principios. Trate de entender cada uno de los principios desde un punto de vista práctico. Algunos de ellos (y lo digo por experiencia) son realmente complejos de llevar a la práctica; recuerde además que son principios, no reglas.

Para finalizar, agradecer a Hadi Hariri, quien me ha servido de "enciclopedia de consulta" para esta serie, por su apoyo y ayuda en todo momento.

Por José Miguel Torres

Principio de Segregación de Interfaces

Principio de Segregación de Interfaces (Interface Segregation Principle, ISP), que trata sobre las desventajas de las interfaces "pesadas" y guarda una estrecha relación con el nivel de cohesión de las aplicaciones.

Como parte de la serie dedicada a presentar los cinco principios SOLID de programación, este mes nos centramos en el Principio de Segregación de Interfaces (Interface Segregation Principle, ISP). Este principio trata sobre las desventajas de las interfaces "pesadas", y guarda una estrecha relación con el nivel de cohesión de las aplicaciones. En este artículo veremos qué perjuicios ocasionan las interfaces "pesadas", qué impacto tienen en la cohesión del sistema y cómo en muchas ocasiones vulneran el Principio de Responsabilidad Única, y cómo ISP ofrece una solución a estos problemas.

El Principio de Segregación de Interfaces

El Principio de Segregación de Interfaces fue utilizado por primera vez por Robert C. Martin durante unas sesiones de consultoría en Xerox. Por aquella época, Xerox estaba diseñando una impresora multifuncional. El software diseñado para la impresora funcionaba y se adaptaba perfectamente a las necesidades iniciales de la impresora; sin embargo, conforme fue evolucionando, y por lo tanto cambiando, se hizo cada vez más difícil de mantener. Cualquier modificación tenía un gran impacto global sobre el sistema. La utilización de ISP permitió reducir los riesgos de las modificaciones y otorgó una mayor facilidad al mantenimiento. El ISP declara que:

Los clientes no deben ser forzosamente dependientes de las interfaces que no utilizan.

A continuación veremos la utilidad de ISP y su significado, una vez que identifiquemos qué es una interfaz "pesada" y qué problemas lleva asociados.

Interfaces "pesadas"

Observemos el diagrama de clases de la figura 1. Básicamente, consta de dos modelos de impresoras representadas por las clases Modelo1998 y Modelo2000, ambas herederas de la clase abstracta ImpresoraMultifuncional.

Inicialmente, la clase abstracta ImpresoraMultifuncional declaraba los métodos correspondientes a las funciones típicas que realiza una impresora multifuncional, como son la propia impresión, el escaneado, el envío de fax y la cancelación de cualquier operación. La impresora Modelo1998 fue el primer modelo en basarse en esta interfaz; poco después se añadió un nuevo modelo, Modelo2000, que además de las funciones anteriores añadía la posibilidad de hacer fotocopias.

Posteriormente, surgió un nuevo modelo (Modelo2002) que se basaba en la misma clase abstracta ImpresoraMultifuncional e incorporaba el soporte para comunicaciones TCP/IP en lugar del servicio de fax; este modelo permitía enviar un documento directamente por correo electrónico, evitando así los altos costes de telefonía. El problema se presenta al implementar en Modelo2002 el método heredado EnviarFax, ya que dicho modelo prescinde de dicha funcionalidad. Una posible implementación sería la que se presenta en el listado 1.

Listado 1:
class Modelo2002 : ImpresoraMultifuncional
{
public override void Imprimir()
{
Impresion.EnviarImpresion();
}
public override void Escanear()
{
Escaner.DigitalizarAFormatoPng();
}
public override void Cancelar()
{
Impresion.CancelarImpresion();
}
public override void EnviarFax()
{
throw new System.NotImplementedException();
}
public void EnviarEMail()
{
// Enviamos por correo electrónico
}
}

El método EnviarFax no se implementa, y por consiguiente una llamada al método generaría una excepción del tipo NotImplementedException. Sí, es cierto que podríamos quitar dicha excepción y podríamos dejar el método vacío; pero entonces el programador que utilice la clase se encontrará con un método que sencillamente no hace nada. Esto podríamos intentar solucionarlo de varias formas: mediante documentación, indicando que el método no es funcional; mediante comentarios en el código (pero es posible que un programador que utilice la clase no tenga acceso al código), etc. En cualquier caso, el problema seguiría existiendo, y solo estaríamos ocultándolo.

De "pesada" a confusa

Es importante que nos concienciemos de este problema. En nuestro ejemplo, se trata de un único método, y eludir el problema puede ser bastante obvio; pero si la clase abstracta implementara una docena de métodos y únicamente utilizáramos tres o cuatro de ellos en un contexto no tan claro como el de las impresoras multifuncionales, el problema se haría más complejo.

Un ejemplo de esto lo tenemos en el propio .NET Framework. La clase abstracta System.Web.Security.Member - shipProvider contiene todos los métodos necesarios para la autenticación ASP.NET: valida credenciales accediendo a algún mecanismo de almacenamiento (SQL Server, sistema de archivos, etc.), bloquea usuarios, gestiona contraseñas, etc. Si implementamos nuestro propio proveedor de autenticación e implementamos la clase MembershipProvider, seguramente solo utilizaremos algunos de los métodos heredados (es decir, implementaremos únicamente ciertas funcionalidades disponibles en la clase base), y en este caso no es tan evidente cuáles de esos métodos deberemos redefinir. ¿Cómo sabría el programador que utilice nuestra clase qué métodos ésta implementa? ¿Qué comportamiento debería tener nuestra clase ante la llamada a un método no implementado? En definitiva, ¿qué valor real tienen los métodos que están disponibles pero no implementados? La respuesta es: ninguno, aparte de crear confusión.

En nuestro ejemplo, la clase Modelo2000 implementa, al contrario que Modelo1998, la característica de Fotocopiar. Dicho método se implementa en la propia clase Modelo2000, y quizás nos hayamos preguntado por qué no hemos añadido el método a la clase abstracta ImpresoraMultifuncional. El motivo puede ser bien dispar. Quizás quién diseñó el sistema decidió no tocar la clase abstracta y extender la clase Modelo2000; sin embargo, resulta que a partir de Modelo2000 todas las impresoras tienen soporte de fotocopia y por lo tanto todos los modelos deberán implementar el método Fotocopiar. Utilizando esta estrategia, tenemos que vulnerar el principio DRY (Don't Repeat Yourself); y lo que es más preocupante, otro programador puede tener la ocurrencia o la necesidad de modificar el contrato o el nombre del método. En definitiva, ello complica el mantenimiento del sistema; cualquier modificación sobre del método Fotocopiar implicará buscarlo por todo el código, aumentando por tanto el riesgo de error. Es contradictorio que tengamos encapsulados en una clase abstracta miembros que no usamos, por ejemplo, en Modelo2002, mientras que otros que sí serían firmes candidatos a serlos, como el caso del método Fotocopiar, no lo son.

Segregación de interfaces

Para solucionar el problema, debemos segregar las operaciones en pequeñas interfaces. Una interfaz es un contrato que debe cumplir una clase, y tales contratos deben ser específicos, no genéricos; esto nos proporcionará una forma más ágil de definir una única responsabilidad por interfaz - de otra forma, violaríamos además el Principio de Responsabilidad Única (Single Responsibility Principle, SRP).

Retomando de nuevo el ejemplo práctico, volvamos a replantear el sistema y separemos las responsabilidades por interfaces. En el listado 2 podemos ver el resultado de dicha segregación.

Listado 2:
public interface IImprimible
{
void Imprimir();
}
public interface IFotocopiable
{
void Fotocopiar();
}
public interface IEscaneable
{
void Escanear();
}
public interface IFaxCompatible
{
void EnviarFax();
void RecibirFax();
}
public interface ITcpIpCompatible
{
void EnviarEMail();
}
class Modelo1998 : IImprimible, IEscaneable, IFaxCompatible
{
// ...
}
class Modelo2000 : IImprimible, IEscaneable, IFaxCompatible,
IFotocopiable
{
// ...
}
class Modelo2002 : IImprimible, IEscaneable, IFotocopiable,
ITcpIpCompatible
{
// ...
}

Conclusión

Mediante la segregación de interfaces, el planteamiento del diseño otorga una mayor cohesión al sistema, lo que se traduce, por una parte, en un menor coste de mantenimiento, y por otra, en un menor riesgo de errores y una mejor localización de los mismos.

martes, 29 de marzo de 2011

Principio de Sustitución de Liskov

Tercer principio de programación SOLID. En esta ocasión presentamos los fundamentos del Principio de Sustitución de Liskov y cómo la aplicación de este principio tiene una repercusión directa sobre las jerarquías de herencia entre clases.

En nuestra entrega anterior, hablábamos de lo útil que puede ser el Principio Open/Closed para desarrollar código fácil de mantener y reusar mediante el diseño de clases abiertas a la extensión y cerradas a la modificación, y para conseguirlo nos basamos en dos características clave de la Programación Orientada a Objetos (POO): la abstracción y el polimorfismo.

En lenguajes OO como C# o VB.NET, la clave para conseguir la abstracción y polimorfismo de entidades es mediante la herencia, y es precisamente en esta característica en la que se basa el Principio de Sustitución de Liskov (Liskov Substitution Principle, LSP). Cuáles son los fundamentos básicos de diseño que debe seguir la herencia en un caso particular, o cuál es la mejor forma de crear jerarquías de herencia entre clases, son algunas de las preguntas a las que responde dicho principio.

El Principio de Sustitución de Liskov

Echemos un vistazo al código del listado 1. Este código trata de calcular los impuestos de un vehículo en base a la matricula (antigüedad) y la cilindrada del mismo.

Listado 1
class Vehiculo
{
public string Marca { get; set; }
public string Modelo { get; set; }
public int Cilindrada { get; set; }
}
class Ciclomotor: Vehiculo
{
public string ObtenerNumLicencia()
{
// Devuelve número de licencia
}
}
class Coche: Vehiculo
{
public string ObtenerMatricula()
{
// Devuelve matrícula
}
}
class Impuestos
{
public void CalcularImpuesto(Vehiculo vehiculo)
{
string matricula = ((Coche) vehiculo).ObtenerMatricula();
ServicioCalculoImpuestos(matricula, vehiculo.Cilindrada);
}
}

En 1987, Barbara Liskov presentó en una conferencia sobre jerarquía y abstracción de datos la siguiente definición:
Si por cada objeto o1 del tipo S existe un objeto o2 del tipo T tal que para todos los programas P definidos en términos de T, el comportamiento de P permanece invariable cuando o1 es sustituido por o2, entonces S es un subtipo de T.

Básicamente, LSP afirma que si tenemos dos objetos de tipos diferentes –Coche y Ciclomotor– que derivan de una misma clase base –Vehiculo–, deberíamos poder reemplazar cada uno de los tipos –Coche/Ciclomotor y viceversa– allí dónde el tipo base –Vehiculo– esté implementado. En el ejemplo anterior tenemos un claro caso de violación del LSP, ya que la ejecución del método CalcularImpuesto generará una excepción de conversión de tipo si el objeto pasado por parámetro es de tipo Ciclomotor en lugar de Coche, pese a que ambas clases derivan de la misma clase base Vehiculo.

Podríamos pensar en solucionar el problema de la forma que se expone en el listado 2. Pese a que el compilador no genere ninguna excepción de conversión de tipo, esta clase aún viola el LSP. Esto es debido a que estamos forzando a un objeto Vehiculo pasado como parámetro a comportarse como Ciclomotor o Coche. Además, esta aproximación vulnera el Principio Open/Closed que vimos en la entrega anterior, ya que ante cualquier nueva entidad que derive de Vehiculo deberemos modificar el método CalcularImpuesto.

Listado 2
public void CalcularImpuesto(Vehiculo vehiculo)
{
string matricula = string.Empty;
if (vehiculo.GetType().Name == "Coche")
matricula = ((Coche) vehiculo).ObtenerMatricula();
else if (vehiculo.GetType().Name == "Ciclomotor")
matricula = ((Ciclomotor)vehiculo).ObtenerNumLicencia();
ServicioCalculoImpuestos(matricula, vehiculo.Cilindrada);
}

Un objeto también es comportamiento

Equivocadamente, tendemos a pensar que una clase únicamente representa datos; eso es cierto solamente en el caso en los objetos de transferencia de datos (DTO) y poco más. En el caso general, las clases incorporan métodos, que son los que aportan la clave diferencial en la POO: el comportamiento.

En el caso anterior, probablemente no tendríamos problemas de herencia si obviáramos los métodos de las clases Coche y Ciclomotor, pero no es el caso. Los métodos ObtenerMatricula y ObtenerNumLicencia probablemente se conecten a un servicio o repositorio de datos externo, o calculen sus resultados según la fecha de matriculación y por tanto ese dato no se almacena en la clase. Dichos métodos otorgan un comportamiento a la clase, y es en la implementación de la herencia dónde puede verse modificado el comportamiento de una clase.

Un ejemplo clásico que encontraremos si buscamos referencias acerca del LSP en Internet o en nuestra biblioteca es el de herencia entre dos clases Rectangulo y Cuadrado; este ejemplo refleja lo que denominamos una incorrecta implementación de la herencia. Fijémonos en el código del listado 3 y el diagrama de clases de la figura 1.

Listado 3
public class Rectangulo
{
public virtual int Ancho { get; set; }
public virtual int Alto { get; set; }
}
public class Cuadrado : Rectangulo
{
public override int Ancho
{
get
{
return base.Ancho;
}
set
{
base.Ancho = value;
base.Alto = value;
}
}
public override int Alto
{
get
{
return base.Alto;
}
set
{
base.Ancho = value;
base.Alto = value;
}
}
}
Nota: En el caso general, las clases incorporan métodos, que son los que aportan la clave diferencial en la POO: el comportamiento

Si ejecutamos el código del listado 4, podremos apreciar cómo se vulnera el LSP. La idea subyacente es que en realidad la clase derivada Cuadrado no solo debe “ser un” sino que también debe “comportarse como un” Rectangulo, y efectivamente no lo hace, ya que la propiedad Cuadrado.Alto modifica tanto la altura como el ancho.

Listado 4
[Test]
public void AreaRectangulo()
{
Rectangulo r = new Cuadrado { Ancho = 5, Alto = 2 };
// Fallará, pues Cuadrado establece
// a 2 el ancho y el alto
Assert.IsEqual(r.Ancho * r.Alto, 10); // false
}

Nota: Al sobrescribir métodos en las clases heredadas, debemos especificar una precondición menos restrictiva y una poscondición más restrictiva que las especificadas en la clase base

Este ejemplo demuestra además la estrecha relación que existe entre LSP y el Diseño por Contratos (Design by Contract – DbC) expuesto por Bertrand Meyer. Utilizando DbC, declaramos en los métodos unas precondiciones y poscondiciones; la precondición debe ser cierta antes de ejecutar el método y tras la ejecución, mientras que el propio método debe garantizar que la poscondición se cumpla.

Pues bien, existe una pequeña variación de DbC cuando se aplica en la herencia. Los métodos de la clase base implementan sus propias precondiciones y poscondiciones; sin embargo, cuando sobrescribimos dichos métodos en las clases heredadas debemos especificar una precondición menos restrictiva y una poscondición más restrictiva que las especificadas respectivamente en la clase base - de otra forma, se violaría el LSP. La razón es que el cliente de la llamada conoce la precondición y poscondición de la clase base, pero no la de la clase heredada, y por lo tanto no podemos suponer que el cliente conozca la precondición del método de la clase heredada, y por tanto debemos ser menos restrictivos en la precondición. Debido a la baja restricción de la precondición y para asegurar el comportamiento correcto tras la ejecución del método, la poscondición debe ser más restrictiva.

El ejemplo del Rectangulo y el Cuadrado, en la propiedad Rectangulo.Alto podríamos establecer la poscondición como:
POSTCONDICIÓN —> (Alto == value && Ancho == Ancho)

Es decir, el valor de Alto debe contener el nuevo valor y el valor de Ancho debe permanecer inalterado. Por lo tanto, la poscondición de Cuadrado.Alto será menos restrictiva al no cumplir el segundo predicado Ancho==Ancho, y en consecuencia Cuadrado.Alto viola el contrato de la clase base y por consiguiente el LSP.

Conclusión

No olvidemos que una clase son datos más comportamiento, y que dicho comportamiento no debe ser sacrificado entre herencias. Por tanto, minimizaremos el impacto de una incorrecta implementación y por tanto de la modificación del comportamiento aplicando el Principio de Sustitución de Liskov.

Por José Miguel Torres

viernes, 25 de marzo de 2011

Principio Open/Closed (II)

Continuamos con el segundo principio SOLID sobre la Programación Orientada a Objetos.

Ya hemos visto la definición del principio Open/Closed en el artículo anterior, y ahora continuamos con nuestra explicación y ponemos ejemplos de como implementar dicho principio.

Fundamentos de la orientación a objetos

La cuestión se centra en cómo minimizar el impacto de una modificación en nuestro sistema, sin comprometer OCP; esto es, manteniendo la "simbiosis" entre las dos características del principio: abierto en extensión y cerrado en modificación.

Volvamos a la entidad Tarea del ejemplo anterior. Por lo que hemos podido ver, los métodos dependen en gran medida del estado de la tarea. Así, una tarea podrá finalizarse o cancelarse dependiendo de su estado previo, pues no podremos cancelar una tarea que haya sido finalizada. De la misma forma, introduciendo el nuevo estado EstadosTarea. Pospuesta implementaríamos un nuevo método llamado Posponer, cuya lógica sería obvia: únicamente podría posponerse una tarea que estuviera en estado pendiente. En definitiva, todo gira alrededor del estado de la tarea, y por tanto el comportamiento de la misma dependerá del estado en que se encuentre. Una opción sería encapsular dicho estado en una clase auxiliar e implementar en ella los métodos Finalizar, Cancelar y Posponer, mediante los cuales definimos el comportamiento, tal y como se muestra en el listado 4, para luego delegar los métodos del objeto Tarea hacia dicha clase.

Listado 4
class EstadosTareaHelper
{
public virtual void Finalizar(EstadosTarea estado)
{
switch ( estado) {
case EstadosTarea.Pendiente:
// finalizamos
case EstadosTarea.Pospuesta:
throw new ApplicationException("Imposible finalizar. Tarea no completada");
default:
throw new ArgumentOutOfRangeException();
}
}
public virtual void Cancelar(EstadosTarea estado)
{
switch (estado) {
// ...
// cancelamos
}
}
public virtual void Posponer(EstadosTarea estado)
{
switch (estado) {
// ...
// posponemos
}
}
}

Nota: Cuando un requisito cambie, lo que debemos hacer es extender el comportamiento añadiendo código, y no modificando el existente.

Pese a que hayamos extraido y aislado el estado de la entidad Tarea, aun no hemos resuelto el problema. De hecho, ahora hemos aislado la responsabilidad en la clase EstadosTareaHelper; sin embargo, estamos algo mas cerca de la solucion. Estudiemos de nuevo los estados -metodos- de la clase Estados- TareaHelper. La logica de cada accion esta escrita en todos los metodos y por tanto se repite; es decir, todos los metodos contemplan la opcion de Finalizar una tarea, y en base a ello actuan de una forma u otra. La operacion Posponer no podra ejecutarse si el estado de la tarea es Cancelada, y la operacion Cancelar unicamente podra ejecutarse si el estado es Pendiente. A traves de este razonamiento, podemos detectar un patron: un mismo contrato .los metodos. y diferentes comportamientos en base a un estado. Esto en OO puede ser solucionado mediante polimorfismo, como se muestra en el listado 5.

Listado 5
abstract class EstadoTareaBase
{
protected Tarea _tarea;
public abstract void Finalizar();
public abstract void Cancelar();
public abstract void Posponer();
}
class EstadoTareaPendiente : EstadoTareaBase
{
public override void Finalizar()
{
// finalizamos
}
public override void Cancelar()
{
// cancelamos
}
public override void Posponer()
{
// posponemos
}
}
class EstadoTareaFinalizada : EstadoTareaBase
{
public override void Finalizar()
{
throw new ApplicationException("Tarea ya finalizada");
}
public override void Cancelar()
{
throw new ApplicationException("Imposible cancelar. Tarea finalizada");
}
public override void Posponer()
{
throw new ApplicationException("Imposible posponer. Tarea finalizada");
}
}
class EstadoTareaCancelada : EstadoTareaBase
{
public override void Finalizar()
{
throw new ApplicationException("Imposible finalizar. Tarea cancelada");
}
public override void Cancelar()
{
throw new ApplicationException("Tarea ya cancelada");
}
public override void Posponer()
{
throw new ApplicationException("Imposible posponer. Tarea cancelada");
}
}
class EstadoTareaPospuesta : EstadoTareaBase
{
public override void Finalizar()
{
throw new ApplicationException("Imposible posponer. Tarea finalizada");
}
public override void Cancelar()
{
// cancelamos
}
public override void Posponer()
{
throw new ApplicationException("Tarea ya pospuesta");
}
}
class Tarea
{
private EstadoTareaBase _estadoTarea;
public Tarea()
{
_estadoTarea = new EstadoTareaPendiente();
}
public void Finalizar()
{
_estadoTarea.Finalizar();
}
public void Cancelar()
{
_estadoTarea.Cancelar();
}
public void Posponer()
{
_estadoTarea.Posponer();
}
}

Básicamente, lo que hemos hecho es crear una clase por cada estado en lugar de tener una única clase cuyos métodos están basados en sentencias condicionadas por el estado de la tarea (switch o if). Además, con esta nueva implementación hemos delegado la responsabilidad de finalizar, cancelar o posponer a una nueva clase EstadoTareaBase que hemos marcado como abstracta. La clase Tarea implementará sus propios métodos y delegará la responsabilidad a través de las clasesestados que heredan de EstadoTareaBase. Debido a que la clase Tarea gira en torno a un estado, asumimos que el estado inicial por defecto es Pendiente, y así lo especificamos en el constructor, instanciando EstadoTareaPendiente.

En realidad, hemos aplicado un patrón ya conocido, el patrón de diseño State, ya que el comportamiento de la clase cambia dependiendo del estado, en este caso, de la tarea, y por lo tanto hemos abstraído cada uno de los estados como entidades independientes. Ante un nuevo requisito en el que intervenga un nuevo estado, lo único que deberemos hacer es crear una nueva clase que herede de EstadoTareaBase e implementar los métodos virtuales, extendiendo así el comportamiento de la aplicación sin comprometer el código existente.

Conclusión

Cuando hablábamos el mes pasado del Principio de Responsabilidad Única, argumentamos la importancia de que cada clase tuviera una y solo una responsabilidad dentro del sistema, de forma que cuanto menos impacto tenga una clase en el conjunto global del sistema, menos repercusión global tendrá una modificación de la clase en dicho sistema. Este mismo argumento es la línea que pretende seguir el Principio Open/Closed, que pese a ser relativamente sencillo de comprender conceptualmente, no sucede lo mismo cuando se aplica. Las claves para la correcta aplicación de este principio son la abstracción y el polimorfismo, como hemos podido ver en el ejemplo.

Por José Miguel Torres

miércoles, 23 de marzo de 2011

Principio Open/Closed (I)

Este es el segundo de una serie de cinco principios SOLID y su aplicación en la Programación Orientada a Objetos.

Después de examinar en nuestra entrega anterior el Principio de Responsabilidad Única, este mes nos adentramos en otro de los principios, que además guarda una estrecha relación con el primero: el Principio Open/Closed.

odas las aplicaciones cambian durante su ciclo de vida, y siempre vendrán nuevas versiones tras la primera release. No por ello debemos adelantarnos a desarrollar características que el cliente podría necesitar en el futuro; si nos pusiéramos en el papel de adivinos, seguramente fallaríamos y probablemente desarrollaríamos características que el cliente nunca necesitará. El principio YAGNI ("You Ain’t Gonna Need It" o "No vas a necesitarlo"), utilizado en la Programación Extrema, previene de implementar nada más que lo que realmente se requiera. La idea es desarrollar ahora sobre los requisitos funcionales actuales, no sobre los que supongamos que aparecerán dentro de un mes.

La actitud de adelantarnos a los acontecimientos es un mecanismo de defensa que en ocasiones acusamos los desarrolladores para prevenir lo que tarde o temprano será inevitable: la modificación. Lo único que podemos hacer es minimizar el impacto de una futura modificación en nuestro sistema, y para ello es imprescindible empezar con un buen diseño, ya que la modificación de una clase o módulo de una aplicación mal diseñada generará cambios en cascada sobre las clases dependientes que derivarán en unos efectos indeseables. La aplicación se convierte, así, en rígida, impredecible y no reutilizable.

Ahora bien, ¿cómo debemos plantear nuestras aplicaciones para que se mantengan estables ante cualquier modificación?

El Principio Open/Closed

El Principio Open/Closed (Open/Closed Principle, OCP) fue acuñado por el Dr. Bertrand Meyer en su libro "Object Oriented Software Construction" y afirma que:
Una clase debe estar abierta a extensiones, pero cerrada a las modificaciones.

OCP es la respuesta a la pregunta que hacíamos anteriormente, ya que argumenta que deberíamos diseñar clases que nunca cambien, y que cuando un requisito cambie, lo que debemos hacer es extender el comportamiento de dichas clases añadiendo código, no modificando el existente.

Las clases que cumplen con OCP tienen dos características:

  • Son abiertas para la extensión; es decir, que la lógica o el comportamiento de esas clases puede ser extendida en nuevas clases.
  • Son cerradas para la modificación, y por tanto el código fuente de dichas clases debería permanecer inalterado.
Podría parecer que ambas características son incompatibles, pero eso no es así. Veamos un ejemplo de una clase que rompe con OCP. Supongamos un sistema de gestión de proyectos al estilo de Microsoft Project. Obviemos de momento la complejidad real que existe en dicho sistema, y centrémonos únicamente en la entidad Tarea, tal y como muestra la figura 1. Dicha clase viene determinada por uno de los estados Pendiente, Finalizada o Cancelada, representados mediante la enumeración EstadosTarea. Además, la clase implementa dos métodos, Cancelar y Finalizar que cambian, si es posible, el estado de la tarea. En el listado 1 podemos ver la implementación inicial del método Finalizar.

Listado 1
public void Finalizar()
{
switch (_estadoTarea)
{
case EstadosTarea.Pendiente:
// finalizamos
break;
case EstadosTarea.Finalizada:
throw new ApplicationException("Tarea ya finalizada");
case EstadosTarea.Cancelada:
throw new ApplicationException("Imposible finalizar. Tarea cancelada");
default:
throw new ArgumentOutOfRangeException();
}
}

Un cambio típico solicitado por el cliente de la aplicación sería la adición de un nuevo estado para controlar las tareas que se han pospuesto, con lo que la adaptación a esta modificación podría ser la expuesta en el listado 2. Aparentemente, parece una modificación trivial; sin embargo, este cambio puede replicarse en otros métodos o clases que utilicen la enumeración EstadosTarea, de forma que en nuestro caso también deberíamos modificar el método Cancelar (listado 3).

Listado 2
public void Finalizar()
{
switch (_estadoTarea)
{
case EstadosTarea.Pendiente:
// finalizamos
break;
case EstadosTarea.Finalizada:
throw new ApplicationException("Tarea ya finalizada");
case EstadosTarea.Cancelada:
throw new ApplicationException("Imposible finalizar. Tarea cancelada");
case EstadosTarea.Pospuesta:
throw new ApplicationException("Imposible finalizar. Tarea no completada");
default:
throw new ArgumentOutOfRangeException();
}
}

Listado 3
public void Cancelar()
{
switch (_estadoTarea)
{
case EstadosTarea.Pendiente:
// cancelamos
_estadoTarea = EstadosTarea.Cancelada;
break;
case EstadosTarea.Finalizada:
throw new ApplicationException("Imposible cancelar. Tarea finalizada");
case EstadosTarea.Cancelada:
throw new ApplicationException("Tarea ya cancelada");
case EstadosTarea.Pospuesta:
// cancelamos
_estadoTarea = EstadosTarea.Cancelada;
break;
default:
throw new ArgumentOutOfRangeException();
}
}

En definitiva, por cada nuevo estado que implementemos tendremos que identificar todas las clases que lo utilizan (tanto la clase Tarea como las clases lógicamente involucradas) y modificarlas, violando no únicamente OCP sino también el Principio DRY ("Don’t Repeat Yourself", "No te repitas"), otro principio que pretende reducir al máximo cualquier tipo de duplicación. En este tipo de modificaciones existe una alta probabilidad de olvidar modificar algún método relacionado con el nuevo estado implementado en el enumerador EstadosTarea, lo que elevaría la probabilidad de aparición de un nuevo bug.

En el siguiente artículo sobre el principio Open/Closed continuamos con la explicación y finalizamos con un ejemplo.

Por José Miguel Torres

jueves, 17 de marzo de 2011

Principio de responsabilidad única (II)

Continuamos con el Principio de Responsabilidad Única, una de las bases de la programación oriendada a objetos.

Continuando con el artículos anterior sobre el principio de resposabilidad única dentro del manual sobre los principios fundamentales de la Programación Orientada a Objetos, pasamos a ver:

Detectando responsabilidades

La piedra angular de este principio es la identificación de la responsabilidad real de la clase. Según SRP, una responsabilidad es "un motivo de cambio"; algo que en ocasiones es difícil de ver, ya que estamos acostumbrados a pensar un conjunto de operaciones como una sola responsabilidad.

Si implementamos la clase Factura tal y como se muestra en el listado 1, podríamos decir que la responsabilidad de esta clase es la de calcular el total de la factura y que, efectivamente, la clase cumple con su cometido. Sin embargo, no es cierto que la clase contenga una única responsabilidad. Si nos fijamos detenidamente en la implementación del método CalcularTotal, podremos ver que, además de calcular el importe base de la factura, se está aplicando sobre el importe a facturar un descuento o deducción y un 16% de IVA. El problema está en que si en el futuro tuviéramos que modificar la tasa de IVA, o bien tuviéramos que aplicar una deducción en base a una tarifa por cliente, tendríamos que modificar la clase Factura por cada una de dichas razones; por lo tanto, con el diseño actual las responsabilidades quedan acopladas entre sí, y la clase violaría el principio SRP.

Listado 1
public class Factura
{
public string _codigo;
public DateTime _fechaEmision;
public decimal _importeFactura;
public decimal _importeIVA;
public decimal _importeDeduccion;
public decimal _importeTotal;
public ushort _porcentajeDeduccion;
// Método que calcula el total de la factura
public void CalcularTotal()
{
// Calculamos la deducción
_importeDeduccion = (_importeFactura * _porcentajeDeduccion) / 100;
// Calculamos el IVA
_importeIVA = _importeFactura * 0.16m;
// Calculamos el total
_importeTotal = (_importeFactura - _importeDeduccion) + _importeIVA;
}
}

Separando responsabilidades

El primer paso para solucionar este problema es separar las responsabilidades; para separarlas, primero hay que identificarlas. Enumeremos de nuevo los pasos que realiza el método CalcularTotal1:
  • Aplica una deducción. En base a la base imponible se calcula un descuento porcentual.
  • Aplica la tasa de IVA del 16% en base a la base imponible.
  • Calcula el total de la factura, teniendo en cuenta el descuento y el impuesto.
En este método se identifican tres responsabilidades. Recuerde que una responsabilidad no es una acción, sino un motivo de cambio, y por lo tanto se deberían extraer las responsabilidades de deducción e impuestos en dos clases específicas para ambas operaciones; estableciendo por un lado la clase IVA y por otro la clase Deduccion, tal y como se presenta en el listado 2.

Listado 2
public class IVA
{
public readonly decimal _iva = 0.16m;
public decimal CalcularIVA(decimal importe)
{
return importe * _iva;
}
}
public class Deduccion
{
private decimal _deduccion;
public Deduccion(ushort porcentaje)
{
_deduccion = porcentaje;
}
public decimal CalcularDeduccion(decimal importe)
{
return (importe * _deduccion) / 100;
}
}

Ambas clases contienen datos y un método y se responsabilizan únicamente en calcular el IVA y la deducción, respectivamente, de un importe. Además, con esta separación logramos una mayor cohesión y un menor acoplamiento, al aumentar la granularidad de la solución. La correcta aplicación del SRP simplifica el código y se traduce en facilidad de mantenimiento, mayores posibilidades de reutilización de código y de crear unidades de testeo específicas orientadas a cada clase/responsabilidad. El listado 3 muestra la nueva versión de la clase Factura, que hace uso de las dos nuevas clases IVA y Deduccion.

Listado 3
public class Factura
{
public string _codigo;
public DateTime _fechaEmision;
public decimal _importeFactura;
public decimal _importeIVA;
public decimal _importeDeduccion;
public decimal _importeTotal;
public ushort _porcentajeDeduccion;
// Método que calcula el total de la factura
public void CalcularTotal()
{
// Calculamos la deducción
Deduccion deduccion = new Deduccion(_porcentajeDeduccion);
_importeDeduccion = deduccion.CalcularDeduccion(_importeFactura);
// Calculamos el IVA
IVA iva = new IVA();
_importeIVA = iva.CalcularIVA(_importeFactura);
// Calculamos el total
_importeTotal = (_importeFactura - _importeDeduccion) + _importeIVA;
}
}

Nota: La correcta aplicación del SRP simplifica el código y se traduce en facilidad de mantenimiento, mayores posibilidades de reutilización de código y de crear unidades de testeo específicas para cada responsabilidad

Ampliando el abanico de "responsabilidades"

Comentábamos anteriormente que no es fácil detectar las responsabilidades, ya que generalmente tendemos a agruparlas. No obstante, existen escenarios o casuísticas en los que "se permite" una cierta flexibilidad. Robert C. Martin expone un ejemplo utilizando la interfaz Modem:

interface Modem
{
void dial(int pNumber);
void hangup();
void send(char[] data);
char[] receive();
}

En este ejemplo se detectan dos responsabilidades, relacionadas con la gestión de la comunicación (dial y hangup) y la comunicación de datos (send y receive). Efectivamente, cada una de las funciones puede cambiar por diferentes motivos; sin embargo, ambas funciones se llamarán desde distintos puntos de la aplicación y no existe una dependencia entre ellas, con lo que no perderíamos la cohesión del sistema.

Conclusión

Pensemos siempre en el ciclo de vida de una aplicación, y no únicamente en su diseño y desarrollo. Toda aplicación sufre modificaciones a causa de cambios en los requisitos o arreglo de fallos existentes, y el equipo de desarrollo puede variar; si a ello le sumamos que el código es poco mantenible, los costes de mantenimiento se dispararán, y cualquier modificación se presentará como una causa potencial de errores en entidades relacionadas dentro del sistema.

martes, 15 de marzo de 2011

Principio de responsabilidad única (I)

Comenzamos con el Principio de Responsabilidad Única, una de las bases fundamentales sobre la programación oriendada a objetos.


Fue en los preparativos del Code Camp de Tarragona ‘2009 cuando surgió la idea de escribir esta serie de artículos para describir cinco principios fundamentales de la POO cuyas iniciales conforman las siglas SOLID. La comprensión de dichos principios nos permitirá mejorar la percepción del no siempre fácil campo de la POO, evitando así malas prácticas que la gran flexibilidad que ofrece esta metodología otorga, fundamentalmente a través de los lenguajes y herramientas que la soportan.

Las herramientas de desarrollo rápido (Rapid Application Development, RAD), como Visual Studio 2010, ofrecen al desarrollador un conjunto de funcionalidades que aumentan, ya sea a través de asistentes o mediante "arrastrar y soltar", la productividad en el desarrollo de aplicaciones, y le permiten focalizarse únicamente en la utilización de propiedades o eventos específicos, sin tener que preocuparse en muchas ocasiones del código generado "por debajo". Sin embargo, en ocasiones acabamos pagando un precio muy elevado, ya que estas herramientas dejan tras de sí "cajas negras lógicas" de código difíciles de modificar o reutilizar, en las que los conceptos clave de la orientación a objetos son sacrificados en aras de la productividad; pero hablar de productividad es hablar de facilidad de mantenimiento y calidad del software, con lo que dicho sacrificio, sencillamente, no tiene o debería tener cabida.

Cohesión

Para tratar de comprender el término "nivel de cohesión", vamos a utilizar como ejemplo el teléfono móvil. Como todos sabéis, un dispositivo móvil está compuesto por una serie de componentes, tales como la pantalla, el teclado, la radio GPRS/3G, el módulo Bluetooth, etc. Todos estos componentes tienen una responsabilidad específica, ya sean los módulos de entrada y salida de datos o los módulos de conectividad y comunicación, etcétera, y por tanto existe una cohesión entre todos los componentes, ya que no hay ningún componente que haga funciones que se solapen con las de otros componentes, ni ninguna función básica que no quede descubierta por un determinado componente. A la hora de buscar un nuevo móvil, miraremos las características y especificaciones técnicas del dispositivo, y además de contemplar las especificaciones que deseamos, también esperaremos que los componentes sean de alta calidad; no es viable que un móvil esté compuesto por los últimos componentes electrónicos del mercado y se venda con una pantalla no táctil en "blanco y negro" de 3 pulgadas, o que, por ejemplo, no tenga algo tan básico como un micrófono.

En un sistema informático también tenemos componentes (además de módulos, clases, etc.), y todos estos componentes lógicos tienen su responsabilidad dentro del sistema. Pensemos pues en el nivel de cohesión de una aplicación no como en una suma de los componentes, sino como el conjunto de los mismos.

Acoplamiento

Siguiendo con el ejemplo del teléfono móvil, un ejemplo de acoplamiento lo encontramos en los cargadores de nuestros móviles. Son cada vez más los fabricantes que optan por adaptadores de corriente estándar en lugar de crear adaptadores propietarios que casi siempre acaban tirados en el contenedor de reciclaje cuando sustituimos el móvil. Se entiende que se opta por la universalidad de los dispositivos de corriente para su reaprovechamiento en otros dispositivos, incluso de diferentes fabricantes. En la ingeniería del software, el acoplamiento entre módulos, clases o cualquier otro tipo de entidad lógica es el grado de dependencia entre ellos. Cuanto más estándar sea la relación de una entidad lógica con otras, mayor reaprovechamiento podremos hacer de ella.

Encapsulación

Seguramente se habrá dado cuenta de que la parte interna de un móvil no es fácilmente accesible; es decir, no tenemos acceso a la electrónica interna. La idea de la encapsulación es la de abstraer determinadas funciones para que, además de ser reutilizables, no requieran que los usuarios tengan los conocimientos del diseñador. La complejidad de un dispositivo móvil o de cualquier otro dispositivo electrónico es muy elevada, y la gran mayoría de usuarios son capaces de sacarle el máximo provecho sin tener nociones específicas sobre su arquitectura interna. La radio Bluetooth o el módem GPRS/3G son los mismos para varios modelos, incluso de diferentes fabricantes. Esta es precisamente la idea de la encapsulación de funciones o características, que lo único que requiere es que el usuario sepa qué se puede hacer con esa función y no cómo está diseñada.

Aplicaciones SOLIDas

Siguiendo el modelo del teléfono móvil y la idea subyacente de estos tres aspectos, podríamos afirmar que el teléfono ideal sería aquel que estuviera compuesto por los mejores componentes del mercado, cuyas interfaces de conexión para la sincronización de datos y recarga de la batería fueran estándares, y cuyas funcionalidades pudiéramos conocer en profundidad sin necesidad de tener que consultar detalles en la documentación técnica para saber cómo han sido diseñadas y así poder sacarles el máximo provecho.

Extrapolando este ejemplo a nuestro mundo, el mundo del software, tenemos que tener siempre presentes estos tres aspectos fundamentales desde el momento mismo en que empezamos el diseño de una nueva aplicación. Es ahí donde entran en escena los cinco principios descritos por el acrónimo mnemotécnico SOLID y presentados a principio de esta década por Robert C. Martin (figura 1).

Los principios SOLID pretenden ser una guía a seguir durante la fase de desarrollo para facilitar el mantenimiento de las aplicaciones y tratar de eliminar el impacto de las inevitables modificaciones que éstas sufren durante su ciclo de vida, además de facilitar el uso de las unidades de testeo, entre otras ventajas.

Nota: Los principios SOLID pretenden ser una guía a seguir durante la fase de desarrollo para facilitar el mantenimiento de las aplicaciones

Principio de responsabilidad única

El Principio de responsabilidad única (Single Responsability Principle - SRP) fue acuñado por Robert C. Martin en un artículo del mismo título y popularizado a través de su conocido libro [1]. SRP tiene que ver con el nivel de acoplamiento entre módulos dentro de la ingeniería del software. En términos prácticos, este principio establece que:
Una clase debe tener una y solo una única causa por la cual puede ser modificada.

Si una clase tiene dos responsabilidades, entonces asume dos motivos por los cuales puede ser modificada. Por ejemplo, supongamos una clase llamada Factura, la cual dentro de un contexto determinado ofrece un método para calcular el importe total, tal y como muestra la figura 2.

miércoles, 9 de marzo de 2011

Dale vida a tu blog

¿Ha notado últimamente que su blog no es tan emocionantes o interesante, ya que solía ser? ¿Usted a menudo encontrará quedando sin grandes cosas para hablar de que atraerá a nuevos lectores? Si usted está encontrando que está teniendo un momento difícil la creación de nuevos contenidos que mantendrá a sus lectores actuales feliz y atraer a nuevos lectores a la vez, entonces tal vez usted podría querer una lluvia de ideas algunas estrategias para dar a su blog una nueva mirada fresca.

Aquí hay algunas maneras de darle vida a tu blog.

1. Vuelve al contenido antiguo

Si simplemente parece que no puede llegar con nuevos mensajes en tus blogs que son interesantes, emocionantes, muy informativos, o beneficioso para sus lectores, entonces tal vez genera nuevas ideas por revisar el contenido de edad que ya han publicado. Hurgando en viejas ideas muchas veces es una buena manera de despertar el interés en los nuevos temas relacionados. Usted puede incluso ampliar en los temas anteriores con el fin de entrar en más detalles acerca del tema en particular. Muchas veces algunos de los mejores contenidos proviene de las viejas ideas que se discutieron anteriormente.

2. Discute Nuevos Temas

Otra forma que puede dar a su blog una nueva mirada es mover por completo nuevos temas. Al igual que muchas personas reacomodar los muebles en sus casas para conseguir un cambio de escenario de vez en cuando, puede reordenar los temas de tu blog para lograr el mismo objetivo. Tal vez usted tiene un nuevo tema que a usted le gustaría discutir que es completamente diferente de lo que se utilizan para su publicación. No sólo la creación de trabajo de contenido nuevo y único maravillas para tu blog desde el punto de vista SEO, pero también se puede obtener en su manera de llegar a un público completamente nuevo y más grande.

3. Obtene comentarios de los lectores

Otra forma que puede dar nueva vida a tu blog es pidiendo comentarios de sus lectores. Muchas veces te vuelves tan sumido en su escritura y en el trabajo de sitio que se olvida de volver a dar un paso atrás de vez en cuando y echar un vistazo a la imagen grande o cómo encaja todo. Usted puede ser capaz de pedir a sus lectores por sus comentarios con el fin de averiguar lo que piensan sobre el contenido de su blog, mira, características, y más.

4. Anima a otros bloggers a publicar en tu blog

Otra gran manera de insuflar nueva vida a tu blog es invitado al permitir su publicación a otros bloggers. Sus lectores podrían ser capaces de ampliar en un debate que ha creado en su blog y proporcionar información muy valiosa que no pensaba por su cuenta. Muchas veces al hacer clic sobre un tema en particular, usted comienza a desarrollar una forma unilateral de pensar que pueden evitar que usted vea que desde todos los ángulos. Mensajes huéspedes pueden ayudarle a conseguir los dos lados de la historia que puede hacer para grandes discusiones en su blog.

5. Revisa la presentación / Tema.

Por último, tal vez tu simplemente necesita un poco de actualización en relación con el tema, los colores, o el diseño. Después de todo, un bien diseñado, atractivo y visualmente atractivo es tan importante como un blog con contenido de calidad.

Escribe títulos cortos

Manten tus titulares cortos.

Los titulares de los periodicos son demasiado largos, esto mismo se puede aplicar a cualquier título que pongamos en nuestras páginas web por lo que debemos cuidar este aspecto.

Portadas


Con estructuras de títulos tan largos es casi imposible fijar la atención en estas portadas.
Con titulares de 12, 13, 14 palabras es bastante complejo identificar palabras clave, o sencillamente, algo de espacio en el que descansar la vista.

Interiores


En el interior la cosa no mejora ya que el titular hace practicamente invisible el subtitular (si, ese texto pequeño que va debajo del titular y que apenas se puede leer).

Hazlos más cortos

El mensaje es sencillo. Haz titulares cortos. El objetivo del titular es sencillamente atraer al usuario, no darles el brief de la noticia.

Por otro lado, si quieres posicionar correctamente en buscadores sería mejor utilizar aquellos términos realmente clave y no 10 palabras de relleno.

¿Qué elementos debes incluir en nuestros titulares?

Nombres de personas
Incluye los nombres de los protagonistas, personajes destacados, etc...

Datos, números, fechas
10 millones, 25 personas, 13,5% de subida... los datos atraen la vista. Úsalos para crear puntos de referencia dentro de tu página.

Lugares, ciudades, direcciones
Si la localización ayuda a entender la información, úsalo.

¿El denominador común de todos estos criterios...?
Se concreto, específico, directo, claro en tus titulares. Usa los subtitulares para alcarar.

Este criteiro ayudará a la limpieza visual de la página.

Titulares breves ayudarán a que tu portada esté más limpia visualmente y si aplicas los criteiros de ser concreto y específico en tus titulares, tendrás un mejor posicionamiento.

Cuando poner un botón, cuando poner un enlace

Este artículo se encaja dentro del "ABC" de la usabilidad pero merece la pena recordar algunas reglas sencillas para saber cuando poner un botón del sistema y cuando poner un enlace.

Cuando poner un botón

Los botones del sistema se deben utilizar en aquellas "acciones" que queremos que el usuario ejecute.

Por ejemplo:
  1. Enviar
  2. Buscar
  3. Descargar fichero
  4. Subir fichero
  5. Confirmar
  6. Guardar

Ejemplos de uso.

Tenemos una web llena de documentos que el usuario puede descargar.

El formato ideal para una página de este estilo sería:

Titulo del fichero

Botón para descargar los ficheros fuera un botón del sistema etiquetado correctamente ("descarga este fichero").
Debajo del botón podemos indicar que va a pasar (si el usuario va a una nueva página, cuanto pesa el fichero, formato, etc...).

Cuando poner enlaces

Los enlaces los debemos utilizar para vincular ficheros HTML. Es decir, el usuario va a ir a otra página web.

El formato de enlaces ha de ser claro y debe indicar que vamos a ver, evitando los "pincha aquí", "más información", etc...

El problema de poner bajo enlaces "acciones" que no son ver otras páginas web es que pueden confundir al usuario.

Si tenemos el formato anterior:

Titulo del fichero

Descarga fichero.

Debajo podemos indicar que va a pasar (si el usuario va a una nueva página, cuanto pesa el fichero, formato, etc...).

Uno podría adivinar que se va a bajar el fichero, pero se podría pensar que uno va a ir a la página de descargas.

Los botones no crean confusión frente a los enlaces para lo que hemos denominado como "acciones".

No se habla de usabilidad

En algunos sites se comenta la idea de que no hace falta hablar de usabilidad para proyectos web. Que eso ya está por defecto en el paquete.

Creo que la verdad de este debate está en la idea de alejarse de Jakob y ocupar otros terrenos aun no capitalizados por ningún gurú.

Hablar de usabilidad es hablar de Jakob Nielsen

Cualquiera que quiera hablar de usabilidad o presentarse como consultor de usabilidad, siempre tendrá el tufillo de ser un replicante de Jakob Nielsen, un sucedaneo, una copia "local" del guru, sus ideas, sus perspectivas...

Jakob ha capitalizado de manera global el concepto de usabilidad y su fuerza de atracción es dificil de escapar.

Hablemos de otra cosa, eso de la usabilidad ya está incluido en el paquete

Creo que meter la usabilidad dentro del paquete de desarrollo es perder 2 batallas.

Por un lado, creo que aun queda mucho camino por recorrer a nivel de usabilidad en la mayoría de los proyectos. Es cierto que el mensaje ha calado en el sector y clientes, pero de eso a tener desarrollos "decentes" hay unos cuantos pasos por dar.

Por otro lado, creo que la usabilidad como servicio debe tener su valor y precio y debe quedar claro cuanto se está inviertiendo en eso.
De esta forma el cliente y el desarrollor pueden cuantificar de forma claro este aspecto del desarrollo y contrastar con el resultado si merece la pena.
Metiendo la usabilidad dentro del saco del desarrollo es una forma de esconder este aspecto y si la cosa sale bien o mal será complejo reconocer donde está el error.

La otra cosa... diseño de producto / interracción...

La otra cosa en la que se quiere meter la usabilidad es una especie de nebulosa donde entra desde el diseño visual al de contenidos, presentación de datos... sin definir de forma muy concreta que sale de esta nebulosa...

"Es que sale todo".

Se podría argumentar que estamos hablando de algo mas completo, integrado... pero creo que es todo lo contrario. Es algo vago, sin definir.

La usabilidad tiene una meta muy clara. Hacer que el uso sea fácil, claro, intuitivo.

El diseño de interacción no tiene una meta tan clara y por eso se queda en algo más difuso.

Al diseño de interacción se le puede dotar de valores dependiendo del perfil del equipo, individuo, empresa de desarrollo... pero con la usabilidad uno está comprando un bien algo más concreto "La web será fácil de usar".

Usabilidad si.

La usabilidad es una disciplina de la que se pueden impregnar todos los componenetes del equipo de desarrollo.
Los diseñadores, editores, programadores...

Pero de la misma forma ocurre con el diseño, la programación...

Un diseñador puede ser consciente de las posibles necesidades de programación, un programador puede tener en cuenta criterios de diseño a la hora de crear su código... pero esto hace que el proyecto dependa del talento de los individuos cuando es mejor tener responsables claros, con una visión coherente de su disciplina.

En proyectos pequeños, hombre orquesta; en proyectos grades, solistas con un director de orquesta

Para proyectos grandes lo mejor es contar con los mejores solistas de cada disciplina con un buen director de orquesta.

Un individuo para usabilidad, otro para diseño, otro para contenidos, otro para marketing, otro para programación, otro para HTML/CSS... y así hasta donde sea posible.

Los hombres orquesta son válidos para proyectos medianos / pequeños.

En cuanto el proyecto crece es mejor contar con los mejores individuos y cada cual debe ser especialista en una disciplina clara donde los objetivos y resultados de la misma sean claramente identificables y por tanto evaluables.

Comprobar los datos en un formulario de entrada

Verificar la corrección de los datos a introducir en un sistema constituye una función fundamental en cualquier aplicación informática pero, a menudo, las validaciones afectan negativamente a la usabilidad.

Evitar que se introduzca información incorrecta en un sistema es uno de los requisitos exigidos a la informática desde sus inicios y se ha convertido en una de las funciones mejor implementada por la sencilla razón de que la fiabilidad de un sistema informático depende de la información que contiene. Por ello, la validación constituye una función imprescindible en cualquier aplicación informática.

Lamentablemente, a menudo, la extrema importancia que lógicamente se otorga a impedir que el sistema se alimente con datos incorrectos acaba deteriorando la usabilidad del sistema ya que los programadores se centran sólo en controlar en lugar de guiar. En este artículo se analizan las características de los procesos de validación de datos de entrada y se proporcionan algunos consejos para mejorar la usabilidad del sistema.

Tipos de validaciones

Las verificaciones a aplicar sobre la información a introducir en un sistema pueden analizarse aplicando diferentes puntos de vista.

A. Según el momento en el que se realicen

Una misma validación, como comprobar que el valor introducido en un campo de fecha es correcto, puede llevarse a cabo en dos momentos distintos:

  1. justo al acabar de informar el campo con lo cual estaremos ante una validación a nivel de campo, o bien
  2. al solicitar que se introduzca toda la información del formulario de entrada, es decir, al darle al “OK”, con lo cual estaremos ante una validación a nivel de formulario.

Elegir entre estos dos momentos para efectuar una validación concreta entabla un debate entre dos puntos de vista antagónicos: por un lado, el que defiende que el sistema debe controlar al usuario, y por el otro, el que opina que es el usuario quien debe tener el control sobre el sistema. Bruce Tognazzini considera esta segunda perspectiva como una característica necesaria para lograr una interfaz de usuario efectiva. Por este motivo, se recomienda que, como norma general, las validaciones se apliquen a nivel de formulario ya que ello permite que el usuario “navegue” libremente por los campos del formulario sin que el sistema le interrumpa constantemente mostrándole mensajes de error por no superar una validación a nivel de campo. Seguir esta recomendación permitirá conocer, entre otras informaciones, el contenido de los valores de las combo-boxes y favorecerá que el usuario se forme un mejor modelo mental de la aplicación.

Además, conviene indicar que, si bien el contenido de un campo puede verificarse tanto a nivel de campo como a nivel de formulario, determinados tipos de validaciones sólo pueden efectuarse a nivel de formulario. Entre otras, aquellas que comprueban:

  • información que no se encuentra explícitamente en ningún campo del formulario, como, por ejemplo, el nivel de autorización del usuario que está precisamente introduciendo datos en el formulario;
  • estados que sólo se producen al dar “OK” al formulario de entrada, como, por ejemplo, verificar, en un proceso de búsqueda, que el usuario ha informado un número mínimo de filtros a aplicar.

B. Según el objeto sobre el que se apliquen

Atendiendo al objeto a verificar, una validación puede ser:

  • Simple: cuando analiza el contenido de un solo campo, o bien
  • Compuesta: cuando analiza el contenido de un campo en relación al contenido de otro u otros campos ya informados en el formulario.

En el caso de validaciones compuestas resulta todavía más recomendable, si cabe, realizarlas a nivel de formulario para permitir que el usuario esté al tanto de las diversas combinaciones de valores disponibles antes de darle al “OK”. Para ello, la habilitación/deshabilitación dinámica de campos y controles resulta de gran ayuda.

C. Según el tipo de característica que validen

Cualquiera de las informaciones que gestiona todo sistema se inscribe en una de estas dos categorías:

  1. Aquello que forma parte del conocimiento del usuario, bien sea porque pertenece al mundo real, como, por ejemplo, que las personas se clasifican en mujeres y hombres; bien sea porque se encuentra en su cabeza, como, por ejemplo, que una persona se identifica por su nombre y apellidos.
  2. Aquello que el usuario desconoce. Y que, a menudo, forma parte de la particular lógica de la aplicación, como, por ejemplo, que las fechas deben informarse con 4 dígitos para el año o que para dar de alta un nuevo cliente el usuario debe poseer un nivel de autorización superior a X.

Aunque es cierto que, a medida que el usuario va interaccionando con el sistema, su modelo mental del mismo será mas completo y, en consecuencia, determinados componentes de la lógica de la aplicación pasarán a formar parte del conocimiento del usuario; resulta evidente que el tipo de conocimiento al que pertenezca la característica a validar afectará directamente a la facilidad de comprensión del usuario. No es lo mismo comprobar que en un campo de importe se introducen sólo números, que validar que en un servicio de reserva de entradas a espectáculos no se soliciten más de 6 entradas o que la compra se realice con una antelación mínima de 3 días sobre la fecha celebración. En este caso, resulta imprescindible un tratamiento excelente de los mensajes del sistema tanto en su redacción como en escoger el tipo de mensaje adecuado para que el usuario llegue a conocer las peculiaridades del sistema que está usando y logre su objetivo.

Validaciones sobre el formato de un dato

Son las típicas comprobaciones sobre la conformación y el tamaño del valor introducido en un campo, como, por ejemplo, que sólo sean números o mayúsculas, o que una fecha siga el patrón “DD-MM-AA”. Manteniendo la preferencia por las validaciones a nivel de formulario, en estos casos, resulta un poco más aceptable aplicar una validación a nivel de campo. A pesar de ello; la mejor solución consiste en que sea el propio sistema quien, proactivamente y de forma automática, realice las acciones necesarias para dejar el valor en el formato correcto, como, por ejemplo, introduciéndolo en mayúsculas aunque el usuario esté tecleando minúsculas o formateando los separadores de las fechas con el carácter previsto por el sistema al abandonar el campo correspondiente.

Orden de las validaciones

Otro factor que afecta a la usabilidad es el orden con que se aplican las validaciones a nivel de formulario. Por ejemplo, resulta muy molesto que:

  • al darle al “OK” a un formulario la primera validación genere un mensaje de error indicando que el campo A es obligatorio y que no se ha informado;
  • que el usuario informe el campo A y al darle otra vez al “OK” el sistema le indique que el valor con que se ha informado el campo A no es correcto;
  • que el usuario modifique el contenido del campo A y al darle otra vez al “OK” el sistema le informe, justo ahora, que no posee el nivel de autorización necesario para emplear este formulario de entrada.

La sensación de frustración y de pérdida de tiempo que provoca en el usuario es del todo innecesaria y puede resolverse muy fácilmente si las validaciones se ejecutan siguiendo un orden lógico como, por ejemplo el siguiente:

  1. Confirmar que la aplicación está disponible ya que si no está operativa no tiene sentido comprobar ningún dato del formulario de entrada.
  2. Verificar que el usuario está autorizado utilizar el formulario de entrada.
  3. Comprobar, uno a uno, que están informados todos los campos obligatorios. Estas validaciones se realizarán considerando, para cada campo, las verificaciones A y B que se detallan a continuación:
    1. Si el formulario de entrada sirve para crear una nueva ocurrencia de un objeto, asegurar que esta ocurrencia efectivamente no existe en la base de datos. Si, por el contrario, el formulario de entrada sirve para consultar, modificar o eliminar una ocurrencia; confirmar que ésta sí existe en la base de datos. Esta validación hay que ejecutarla inmediatamente después de haberse superado el paso número 2 sobre el campo que contiene el valor de la ocurrencia.
    2. Ejecutar las validaciones compuestas, aquellas que analizan el contenido de un campo en relación al contenido de otro u otros campos ya informados en el formulario.
  4. Validar el resto de campos

Además, en el orden de ejecución de las validaciones hay que seguir la secuencia de izquierda a derecha y de arriba abajo respetando las agrupaciones de campos que tenga el formulario. Es decir, seguir el mismo orden que se aplique al desplazamiento del foco de teclado entre campos que explica Josep Casanovas en su artículo “Diseño de formularios - Campos (II).

Implementar las menos validaciones posibles

Nadie discute la necesidad de controlar el contenido de un formulario de entrada, pero un diseño inteligente puede reducir mucho el número de validaciones a aplicar y esto, además de favorecer la usabilidad porque el usuario va a padecer menos interrupciones, también puede favorecer un menor coste de desarrollo, pruebas y mantenimiento.

A continuación se indican varios recursos, algunos de los cuales ya menciona Josep Casanovas en su artículo, para reducir el número de validaciones a realizar:

  • Usar campos con listas de valores asociadas o combo-boxes que facilitan que el usuario informe el campo con un valor correcto.
  • Informar los campos con un valor por defecto. Éste debe ser el más probable. En caso que no convenga informar con un valor por defecto, indicar dentro del propio campo las características de los valores correctos.
  • Añadir un pequeño texto explicando el formato y/o rango de valores correcto.
  • Habilitar/deshabilitar dinámicamente campos y/o botones según el estado del formulario, el perfil del usuario o cualquier otra circunstancia.
  • Convertir automáticamente el contenido de un campo al formato correcto.
  • Añadir acciones específicas para asignar valor a un campo, como, por ejemplo, añadir un calendario para introducir una fecha.


Aunque es totalmente imprescindible comprobar la información a introducir en un sistema, cuando las validaciones se aplican con un orden lógico y se reducen al máximo, el usuario las percibirá como una ayuda en lugar de cómo un obstáculo, mejorando la usabilidad del sistema.

martes, 8 de marzo de 2011

Auto automatico de google

En la conferencia TED 2011 en Long Beach, Google dio demostraciones de su coche con conducción automática, es realmente impresionante.



Más información en Search Engine Land.

Mapas y usabilidad

La generalización del uso de los mapas especialmente desde la popularización de Google Maps y su API ha provocado que algunas cuestiones de usabilidad no resueltas cobren importancia.

Desde hace tiempo, pero especialmente desde la aparición de Google Maps, los mapas han cobrado más popularidad en la web y se han comenzado a utilizar de manera extensiva.

El ámbito web no es el único, otros dispostivos como los GPS tienen muchas utilidades prácticas que se irán incorporando a la vida cotidiana. Todo ello se verá facilitado si se consigue que los datos geográficos creados por el estado sean libres (EE.UU.) y no un caro monopolio estatal (Europa) aun florecerán más todo tipo de aplicaciones.

Los mapas interactivos presentan retos de usabilidad y representación de información aún no resueltos.

Navegación estandar

No hay una navegación estándar sobre mapas interactivos. Los procesos de zoom y movimiento por el mapa no están estandarizados. Existen ciertos elementos más comunes que otros, pero luego su funcionamiento es diferente.


- Interfaz de Map24 -

- Interfaz de Maporama -
Navegación lenta

Navegar por un mapa (sin utilizar un buscador) es poco ágil. Es casi imposible encontrar a la primera el área de interés y se requieren muchos movimientos hasta encontrar el área exacta.

La navegación funciona correctamente si se trata de pequeños movimientos para explorar un área pequeña, una vez localizada por otros medios (buscador), pero grandes movimientos dan problemas.

Por estas razón no es recomendable basar un interfaz en la navegación por un mapa o hacer la navegación por mapa la única manera de encontrar los contenidos de un sitio web.

Conocimiento y familiaridad con el área

Para navegar por un mapa hay que conocer suficientemente bien el área en cuestión, de lo contrario es muy dificil y hasta imposible orientarse o localizar un lugar. Las indicaciones que incluyen los mapas ayudan, pero no siempre resuelven el problema.

Estos problemas se reflejan especialmente en sitios creados para turistas extranjeros cuya interfaz principal es un mapa. Los turistas pueden conocer el nombre de la ciudad o de la zona que buscan, pero desconocer totalmente su situación en un mapa del país y menos aún de la provincia.

En casos como webs inmobiliarias que utilizan mapas, es difícil hacerse una idea exacta de las áreas de una ciudad a no ser que se conozca perfectamente el trazado de las calles y los limites de cada barriada. Incluso para vecinos de la ciudad es fácil confundirse, difícil calcular distancias, etc.

Orientación viso-espacial

Entender un mapa no es algo banal. Los mapas utilizan un lenguaje que se ha desarrollado a lo largo de los siglos, pero no son algo extraordinariamente intuitivo.

Un mapa muestra una visión a vista de pajaro cuya comprensión requiere de una habilidad llamada orientación viso-espacial y su memoria asociada.

La orientación viso-espacial es una habilidad compleja, independiente del nivel de inteligencia en otras áreas y con niveles muy desiguales entre la población. A muchas personas les resulta difícil orientarse o encontrar un lugar en un mapa.

Aunque por lo general dado el tiempo suficiente la mayoría de la gente puede consiguer hacerlo, hay que tener en cuenta que pocas personas están dispuestas a dedicar más que unos pocos minutos a un sistema interactivo si no consiguen resultados.

La habilidad de orientación viso-espacial se puede aprender y mejorar. Con la expansión de los mapas interactivos la población aprenderá a utilizarlos.

Niveles muy básicos y con información muy específica, reducida y estandarizada, como mapas de metro si pueden ser dominados por casi toda la población. Sin embargo en otros niveles los mapas seguramente dejan fuera a una parte de la población significativa.

Indicaciones en el mapa

Las indicaciones en el mapa (nombres de ciudades, líneas fronterizas, carreteras, etc.) ayudan a orientarse y localizar los lugares buscados.

El número de indicaciones en el mapa está limitado por la superficie disponible, se trata de evitar que el mapa termine ocultado y que las indicaciones se superpongan unas sobre otras.

El problema de las indicaciones es mayor en los mapas interactivos. A diferentes niveles de zoom, diferentes indicaciones son relevantes. Por ejemplo, a niveles lejanos solo es relevante mostrar capitales y fronteras de paises. A niveles bajos se muestran aldeas o caminos vecinales, etc.

En los mapas generalistas (para cualquier uso) casi hay un estándar sobre qué mostrar a cada nivel de zoom, sin embargo en mapas con usos específicos ya no está tan claro. Dependiendo del escenario de uso y el perfil de usuario, las indicaciones pueden variar.

La gran cuestión es la relevancia. ¿A qué nivel de zoom es relevante un monumento? Una iglesia en principio tiene sentido mostrarla solo a un nivel de zoom muy cercano, pero si estamos en un mapa diseñado para turistas, puede tener sentido mostrarla a otro nivel de zoom.

El otro gran problema de los mapas interactivos es que las informaciones aparezcan y desaparezcan según el nivel de zoom, lo que es en cierto modo confuso para el usuario. Si se busca la catedral de Barcelona en un mapa interactivo, pero el nivel de zoom es demasiado alto la indicación o icono "catedral" no se mostrará, por lo que el usuario puede llegar a pensar que simplemente no está indicada en este mapa. Ciertamente se puede hacer zoom, pero si no se hace sobre el lugar exacto, se "aterrizará" sobre unas calles de Barcelona desconocidas, perdido sin saber hacia donde moverse para encontrar la catedral.

Por último queda el tema de los iconos. Para poder incluir más indicaciones sin ocultar el mapa, se recurre a iconos porque ocupan menos espacio que los textos. Algunos de estos iconos prácticamente son estándares (hospital, iglesia, etc), pero como cualquier sistema de representación basado en imágenes los iconos escalan mal, no todo es claramente representable mediante un icono.

Problemas específicos de Google Maps

Desde que Google Maps ha puesto a disposición de los desarrolladores su API que permite utilizar sus mapas, han aparecido multitud de sitios que los utilizan (Housingmaps.com, Chicagocrime o Trulia.

Incluso en España abundan los proyectos que usan la API: Tagzania, Habitamos, Vivirama y Panoramio (del que soy co-fundador).

Sin embargo los mapas de Google tienen varios problemas de usabilidad y escalabilidad. El sistema parece creado exclusivamente para uso uso en Google Local donde funciona razonablemente bien, pero no para cualquier otro tipología de sitio web.

Problemas de situación, número y selección

Los marcadores (o pines) de Google Maps se superponen unos sobre otros. A veces se ocultan tanto unos bajo otros que no es posible seleccionarlos, saber cuantos hay, ni donde estan situados. En ocasiones hay tantos juntos que ni siquiera se puede ver el mapa.

Desplegar globos

Al seleccionar un marcador se muestra una especie de globo con la información que contiene. Parece es un sistema muy intuitivo, pero luego origina problemas.

Cuando se selecciona un marcador que está en los bordes del mapa, el mapa se mueve automáticamente para dejar libre suficiente espacio para mostrar el globo. Sin embargo al mover el mapa se pierde la situación original, lo que afecta a otros pines y la orientación del usuario, es necesario mover el mapa para volver a la localizacion original.

Listas de elementos como complemento imprescindible, pero limitado

No es posible conocer por adelantado que representa un marcador del mapa. ¿Es un restaurante, un hotelo un bar? Es necesario seleccionarlo y desplegarlo para ver su contenido, un sistema poco ágil.

La solución de Google Local (y otros sitios) es incluir listas de items al lado del mapa permiten encontrar la información que necesitas (nombre, teléfono, precio, etc.) mucho más rápidamente que seleccionando uno a uno los marcadores. Sin embargo esta es una solución limitada.


- Pantalla de resultados de Google Local -
La limitación aparece cuando existen muchos items en una lista y es necesario paginar. Solo se pueden mostrar en el mapa los marcadores (pines rojos) que representan a los items de esa página, pero no todos.

Esto es confuso, porque en un área (una ciudad o una calle) determinada pueden haber items (un hotel, un monumento), pero no ser mostrados porque no están en esa página, sino en otra. La paginación es un elemento de interacción que encaja con las listas de ítems, pero que no funciona muy bien con el modelo mental de interacción con un mapa.

Buscador como interfaz inicial para un mapa

Google Maps, con su homepage llena de explicaciones sobre como usarlo, desapareció hace tiempo y redireccionó a Google Local, un buscador donde indicar “qué” y “dónde”. Los mapas son ahora un complemento a los resultados de la busqueda. En mi opinión por dos razones:

Primero por las limitaciones que he comentado antes, las de mapas en general y las de Google en particular. Estas limitaciones hacen pensar que los mapas solo tienen sentido como resultado a una busqueda, pero no como interfaces principales de búsqueda.

Con un buscador como herramienta inicial todo es más sencillo. Si se conoce el area concreta y la información exacta que la persona busca, por ejemplo “Pizzas en New York” se le puede mostrar directamente solo un tipo de información y en un área reducida. Esto hace que:

- No sea necesario navegar por el mapa, con los problemas que eso conlleva.
- El número de información a incluir sea limitada. En un área pequeña y de un tipo concreto deben haber pocos marcadores y poca información relacionada.

Segundo porque no hay nada superior al lenguaje humano. Un buscador aunque técnicamente busca, en realidad desde el punto de vista del usuario da respuestas a preguntas formuladas en lenguaje natural.

Basta ver las interfaces interactivas para trazar rutas entre dos lugares. Todas incluyen un mapa, pero la respuesta real, la información clara, concisa y valiosa es la ruta descrita en lenguaje textual.


- Ruta en formato gráfico y textual -
Un mapa por sí mismo no da una respuesta, sino que permite encontrar la respuesta.

Un mapa por si mismo no habla en lenguaje natural, sino en otro lenguaje que se debe aprender, muy útil para algunos objetivos y con muchas limitaciones para otros.

Los mapas como complementos

En realidad todos los problemas anteriores dejan claro que más allá del deslumbramiento que nos producen los mapas interactivos y sus infinitas posibilidades, actualmente los mapas funcionan mejor como importantes complementos, en muchos casos imprescindibles, que como interfaces principales o únicas.

Esta situación puede cambiar en un futuro próximo. Por otro lado en la actualidad para determinados, colectivos, objetivos y situaciones de uso, los mapas sí mismos puedan ser la interfaz principal. Ya se sabe, la definición de usabilidad dice que algo no es inheremente usable o inusable, sino que dependera de los usuarios, sus objetivos y el contexto.

Conclusión

La expansión de los mapas es un fenómeno fantástico. Los mapas han venido para quedarse, se utilizarán más y se extenderán a muchos ámbitos, pero hay retos importantes que superar y que hacen la aventura de desarrollar con mapas aún más emocionante.

 

©2009 - 2013 Pichujitos | Theme diseñado por chicoloco123 para Fuutec.com | Ir arriba ↑