Polución de Interfaces en Java 8

Una pregunta interesante en relación con Java 8 es por qué razón el grupo de expertos no decidió implementar un tipo función en vez del concepto de interfaces funcionales que utiliza actualmente el nuevo JDK 8. Por ejemplo, en un lenguaje como C# existe un conjunto predefinido de tipos de funciones que varían según el número de argumentos que una función acepta (llamado arity en inglés).

Así en C# tenemos los tipos Func que sirven para representar funciones con un tipo de retorno y Action para representar funciones de tipo de retorno vacío. Ambos tipos están representados por delegates que usan un número variable de tipos genéricos T1, T2, T3, …, T16, según el número de argumentos que la función acepta.

En el JDK 8, sin embargo, lo que tenemos es un concepto de interfaces funcionales que tienen diferentes nombres y diferentes métodos. Sus métodos abstractos representan a alguna de las firmas conocidas de funciones (nularia, unaria, binaria, ternaria, etc). Podríamos preguntarnos por qué el diseño del JDK 8 se aleja de soluciones ampliamente probadas con éxito en otros lenguajes similares, como C#. En este artículo vamos a explorar los diferentes problemas de diseño que enfrentó el grupo de expertos y las razones por las que el JDK 8 fue implementado de esta manera.

El Problema del Borrado de Tipos (Type Erasure)

Uno de los principales problemas de los que adolece Java data de los días en que se agregaron los tipos genéricos al lenguaje (alrededor del 2005 con el JDK 5). En Java la información de los tipos genéricos es descartada por el compilador una vez que éste ha verificado la integridad de los tipos del programa. Las razones por las que los tipos genéricos se implementaron de esta forma en Java son diversas pero una razón de peso era la necesidad de mantener compatibilidad hacia atrás con versiones anteriores de Java. A este proceso que realiza el compilador de descartar la información de los tipos genéricos una vez que ya no la necesita se le conoce como borrado de tipos o en inglés type erasure. La principal implicación de este diseño es, básicamente, que en Java no existiría diferencia entre un tipo Function<T1> y un tipo Function<T1,T2>, o Function<T1,T2,T3> o Function<T1,T2,T3, ..., Tn>. Para efectos de como funciona Java todos ellos estarían representados por un solo tipo llamado Function.

Así que el grupo de expertos tuvo que luchar con este problema. Brian Goetz, líder del proyecto, escribió lo siguiente en la lista de distribución del proyecto lambda:

[…] Como ejemplo sencillo consideremos un tipo función. En la propuesta original del proyecto lambda presentada en devoxx se consideró el tipo función. Yo insistí en quitarlo y esto me hizo muy impopular. Pero mi objeción al uso de un tipo función no se debe a que no me guste el tipo función — me encanta el tipo función — pero este tipo no se lleva bien con un aspecto del sistema de tipos de Java: el borrado de tipos. Las funciones con tipos borrados son el peor de los dos mundos. Así que los quitamos de este diseño.

Pero no estoy diciendo que “Java nunca tendrá un tipo función” (aunque reconozco que Java podría nunca tener un tipo función). Creo que para poder tener un tipo función primero tenemos que lidiar con el problema del borrado de tipos. Puede que eso sea posible o no. Pero en un mundo de tipos estructurales materilazados el tipo función empieza a cobrar mucho más sentido […]

¿Cómo influye todo esto en el diseño final de Java 8?

Bueno, el grupo de expertos decidió proponer una solución poco ortodoxa al problema. En vez de definir un tipo función, en su lugar tenemos una infinidad de nuevas interfaces que representan diferentes tipos de funciones. Veamos algunos ejemplos.

Funciones sin Tipo de Retorno (void)

En el mundo de las funciones sin un tipo de retorno tenemos algo como esto:

Tipo de Función Expresión Lambda Interfaces Funcionales Conocidas
Nullaria
() -> doSomething()
Runnable
Unaria
foo -> System.out.println(foo)
Consumer
IntConsumer
LongConsumer
DoubleConsumer
Binaria
(console,text) -> console.print(text)
BiConsumer
ObjIntConsumer
ObjLongConsumer
ObjDoubleConsumer
n-aria
(sender,host,text) -> sender.send(host, text)
Define tu propio tipo

Funciones con Algún Tipo de Retorno T

En el mundo de las funciones con un tipo de retorno T tenemos algunas de las siguientes:

Tipo de Función Expresión Lambda Interfaces Funcionales Conocidas
Nullaria
() -> "Hello World"
Callable
Supplier
BooleanSupplier
IntSupplier
LongSupplier
DoubleSupplier
Unaria
n -> n + 1
n -> n >= 0
Function
IntFunction
LongFunction
DoubleFunction
IntToLongFunction
IntToDoubleFunction
LongToIntFunction
LongToDoubleFunction
DoubleToIntFunction
DoubleToLongFunction
UnaryOperator
IntUnaryOperator
LongUnaryOperator
DoubleUnaryOperator
Predicate
IntPredicate
LongPredicate
DoublePredicate
Binaria
(a,b) -> a > b ? 1 : 0
(x,y) -> x + y
(x,y) -> x % y == 0
Comparator
BiFunction
ToIntBiFunction
ToLongBiFunction
ToDoubleBiFunction
BinaryOperator
IntBinaryOperator
LongBinaryOperator
DoubleBinaryOperator
BiPredicate
n-aria
(x,y,z) -> 2 * x + Math.sqrt(y) - z
Define tu propio tipo

Alguien pudiera decir que una ventaja de esta solución es que nos permite definir nuestros propios tipos, definir nuestras interfaces que acepten tantos argumentos como queramos, y entonces las podemos usar para crear expresiones lambda y referencias de métodos según se nos antoje. En otras palabras, podemos contaminar el mundo con incluso más interfaces funcionales. Otra ventaja es que podríamos usar interfaces funcionales ya existentes en Java para implementar expresiones lambda (p.ej. Comparable, Runnable y Callable) e incluso interfaces que nosotros mismos hubiéramos definido en el pasado, siempre y cuando sean interfaces funcionales (es decir, interfaces que solo tiene un método abstracto).

Sin embargo, la desventaja más evidente es esta explosión de interfaces con diferentes nombres y con diferentes métodos. Y ahora debemos memorizar o al menos recordar o reconocer que existen a fin de poder usarlas en nuestro código. Como todas tienen diferentes nombres y como todos nosotros podemos todos los días definir nuevas, la tarea se vuelve un poco más compleja en comparación con aquellos lenguajes en donde solo existe un tipo función. Es interesante resaltar que en el lenguaje de programación Scala este problem se resolvió definiendo interfaces con diferentes nombres como Function0Function1Function2, …, FunctionN. Es una manera de evitar el problema del borrado de tipos y definir un tipo función para el lenguaje. Asumo que esta solución no fue considerada como válida de cara a explotar la ventaja que sí tienen las interfaces funcionales de poder usar expresiones lambda incluso con tipos definidos en librerías anteriores a la liberación de Java 8.

El Problema de la Ausencia de Tipos Valor (Value Types)

Claramente el borrado de tipos es una de las fuerzas mas influyentes en el diseño de Java 8, pero no es suficiente para explicar por qué, aparte de esta explosión de interfaces, tenemos además otras con nombres similares y cuyas firmas de métodos se diferencian por el uso de tipos primitivos (p.ej. Function, IntFunction, LongFunction, DoubleFunction, etc).

La razón detrás de estas interfaces adicionales radica en el hecho de que en Java no tenemos un concepto de tipos valor (en inglés Value Types) como sí sucede en otros lenguajes como C#. Esto significa que los parámetros de tipos que usamos en las clases, interfaces y métodos genéricos de Java solo pueden ser sustituidos por tipos de referencia (reference types) y no por tipos primitivos.

En otras palabras, esto no es posible en Java

List<int> numbers = asList(1,2,3,4,5);

Pero esto otro sí lo es:

List<Integer> numbers = asList(1,2,3,4,5);

Este segundo ejemplo, sin embargo, incurre en el costo de convertir el tipo primitivo int en un objeto Integer antes de poder almacenarlo en la colección. De la misma manera, al recuperar el entero y usarlo en una operación aritmética, incurriríamos en el costo opuesto de extraer el valor entero encapsulado por el objeto. En inglés a esto se le suele llamar boxing y unboxing. Y este proceso se puede volver realmente caro en términos de desempeño cuando estamos lidiando con colecciones de datos primitivos.

Evidentemente el grupo de expertos necesitaba encontrar una manera de mejorar el desempeño de las nuevas APIs de colecciones en Java 8 para esos casos en donde se usan valores primitivos de datos. Así que para solucionar este problema el grupo de expertos decidió crear aún más interfaces para lidiar con cada uno de los posibles escenarios. En vista de que Java tiene alrededor de 8 diferentes tipos primitivos la explosión de interfaces hubiera sido una locura, y por eso los diseñadores decidieron solo lidiar con los tipos básicos int, long, y double, asumiendo que cualquier otro tipo puede ser fácilmente promovido a alguno de estos otros.

Una vez más recurro a una cita de de Brian Goetz en la lista de distribución del proyecto lambda para demostrar que los diseñadores encontraron grandes dificultades para lidiar con este problema:

[…] la filosofía tras el uso de streams especializados para tipos primitivos (p.ej. IntStream) está plagada de horribles compromisos. Por un lado todo esta horrible duplicación de código, polución de interfaces, etc. Por otro lado, cualquier clase de aritmética sobre tipos encajados (boxed types) apesta, y no tener un escenario para reducir valores de enteros sería terrible. Así que estamos contra la espada y la pared, y estamos tratando de no empeorar las cosas.

Truco #1 para no empeorar las cosas es: no vamos a soportar los ocho tipos primitivos. Solo vamos a soportar int, long, double; todos los demás se pueden simular con estos. Podría decirse que también nos podríamos deshacer de int, pero no creo que los desarrolladores de Java estén listos para eso. Sí, habrá peticiones para Character, y la respuesta es “mételo dentro de un entero” […]

Truco #2 es: estamos usando streams de primitivos para exponer cosas que se manejan mejor en el dominio de los primitivos (ordenamiento, reducción) pero no estamos intentando duplicar todo lo que existe en el dominio de tipos de rerencia. Por ejemplo, no existe IntStream.into(). (Si fuera el caso, la siguiente pregunta sería “Y dónde está el IntCollection? IntArrayList?, IntConcurrentSkipListMap?). La intención es que muchos streams puede comenzar con tipos de referencia y terminar como streams de primitivos, pero no a la inversa. Eso está bien y reduce el número de conversiones necesarias (p.ej. no hay sobrecarga de map para int-> T, no hay especialización de Function para int->T, etc).

Creo que pocos de nosotros pensaríamos que esta solución es deseable, pero creo que la mayoría estaría de acuerdo de que fue necesaria y no parece como que otra solución mejor pudiera haber sido desarrollada sin antes resolver el problema de la ausencia de tipos valor en Java. Al menos en estas citas encontramos la justificación para algo que muchos, de buenas a primeras, podrían considerar descabellado.

El Problema de los Checked Exceptions

Existe una tercera fuerza que pudo haber causado que esta polución de interfaces fuera inclusive peor. Se trata del hecho de que Java soporta dos tipos de excepciones llamados checked y unchecked.  El compilador exige que hagamos algo respecto a un método que puede lanzar una excepción de tipo checked, en esos casos debemos manejar la excepción (p.ej. con un try..catch) en el lugar o declarar que nuestro método también arroja la excepción (p.ej. con una cláusula throws en la firma del método). Ejemplos de excepciones checked son SQLException y IOException para mencionar un par de las más conocidas.

El asunto es que este comportamiento de Java crea un escenario interesante de cara a las interfaces funcionales provistas en Java 8. Todas estas interfaces funcionales tienen un métodos que no declara ninguna excepción. Eso quiere decir que estas interfaces funcionales no se pueden implementar con operaciones que están sujetas al uso de excepciones de tipo checked (a menos que manejemos la excepción directament el cuerpo del método, y en el caso de las expresiones lambda, en el cuerpo de la expresión misma). Por ejemplo, esto no es posible en Java 8:

Writer out = new StringWriter();
Consumer<String> printer = s -> out.write(s); //error de compilación

No se puede hacer esto porque la operación write declara que puede arrojar una excepción de tipo checked, un IOException en este caso, pero la firma del método en la interfaz Consumer que está siendo implementado por la expresión lambda no declara que podría arrojar ningún tipo de excepción.

Como se podrán imaginar, la solución a este problema habría sido crear aún más interfaces funcionales, algunas declarando excepciones y otras no (o implementar alguna otra solución a nivel del lenguaje que permitiera soportar transparencia de excepciones). Y una vez más, para tratar de “no empeorar” las cosas el grupo de expertos decidió no hacer nada en este caso.

Brian Goetz escribió en la lista de distribución del proyecto lambda:

Sí, tendrás que implementar tus propios SAM types excepcionales. Pero entonces la conversión lambda funcionará con ellos.

El grupo de expertos discutió sobre agregar soporte adicional al lenguaje y librerías para solucionar este problema, pero al final sentimos que no se justificaba el costo/beneficio.

Soluciones de librería causaría una explosión de 2x en los tipos SAM (excepcional vs no excepcional), lo cual no interactúa bien con las explosiones de combinaciones existentes para especializaciones de primitivos.

Las soluciones a nivel de lenguaje perdieron valor de cara a su complejidad/valor. Aunque hay algunas soluciones alternativas que vamos a continuar explorando – aunque claramente no para la versión 8 y probablemente tampoco para la 9.

De momento, tienen las herramientas que necesitan. Comprendo que ustedes preferirían que hiciéramos es milla extra por ustedes (y, además su solicitud sería una mampara para decir “por qué no se rinden de una vez con las excepciones tipo checked”), pero creo que el estado actual les permite hacer su trabajo.

Así las cosas, somos nosotros los desarrolladores los que debemos crear nuestras propias explosiones de interfaces para lidiar con este problema caso por caso:

interface IOConsumer<T> {
   void accept(T t) throws IOException;
}

static<T> Consumer<T> exceptionWrappingBlock(IOConsumer<T> b) {
   return e -> {
	try { b.accept(e); }
	catch (Exception ex) { throw new RuntimeException(ex); }
   };
}

A fin de lograr algo como:

Writer out = new StringWriter();
Consumer<String> printer = exceptionWrappingBlock(s -> out.write(s));

Probablemente en el futuro (quizás en el JDK 9) cuando tengamos Soporte para Tipos Valor en Java, y cuando se haya corregido el problema del borrado de tipos, entonces quizás nos podremos deshacernos también (o al menos no necesitar ya más) de toda esta polución de interfaces.

En resumen, podemos ver que el grupo de expertos tuvo que tomar difíciles decisiones de diseño para lidiar con diferentes tipos de problemas en Java a fin de poder implementar muchas de las nuevas características en Java 8 hoy. Aspectos como la carencia de tipos valor, el borrado de tipos y las excepciones tipo checked influenciaron poderosamente el diseño y si Java no hubiera tenido ninguna de estos problemas, la solución final habría sido, probablemente, muy diferente, quizás más parecida a la de otros lenguajes como C#. Pero el grupo de expertos tenía que pintar la raya en en algún lugar. Para bien o para mal, este es el diseño actual, y el tiempo se encargará de demostrar si estas decisiones de diseño fueron buenas o malas o si algo más podría haberse hecho al respecto.

Lectura Adicional

Cómo Declarar Módulos de Node.js

Creo que una de las cosas que me causó un poco de confusión cuando comencé a trabajar con Node.js fue entender exactamente cómo se pueden exponer diferentes tipos objetos utilizando módulos. Al principio pareciera intuitivamente sencillo, pero más tarde se da uno cuenta de que puede exponer funciones, constructores, objetos, o simplemente nuevas propiedades dentro del módulo y para cada caso existen diferentes formas de hacerlo. Veamos unos cuantos ejemplos.

Sobre los Módulos, Importar y Exportar

Empecemos por lo más obvio y sencillo y lo que probablemente todo el mundo entiende desde el primer día de trabajo con Node.js. En node un archivo de código se considera un módulo. Las variables, propiedades, funciones o constructores que se declaren en él son privadas al módulo y otros módulos no pueden verlas ni usarlas a no ser que el programador explícitamente las exponga al público; es decir todo lo que declaramos dentro de un módulo está encapsulado y oculto al mundo exterior de forma predeterminada a no ser que el programador exponga algo explícitamente. Para este propósito cada módulo tiene acceso a un objeto especial llamado module, y este objeto module, a su vez, tiene una propiedad llamada exports. Todo lo que el programador coloque en el objeto exports se vuelve público para ese módulo. Por ejemplo, el módulo foo.js expone una propiedad llamada bar que contiene una cadena de caracteres 'Hello World'. Cualquier otra declaración dentro del módulo sería privada y solo accesible al módulo mismo. Por ejemplo, la variable pi declarada abajo es inaccessible para cualquier otro módulo diferente de foo.js, mientras que la propiedad bar es públicamente accessible para otros módulos que importen el módulo foo.js.

//foo.js
var pi = 3.14; 
module.exports.bar = 'Hello World';

Así las cosas, un segundo módulo baz.js podría “importar” el módulo foo.js y tener acceso a la propiedad bar. En node, conseguimos tal efecto por medio de usar la función require. Por ejemplo de la siguiente manera:

//baz.js
var foo = require('./foo');
console.log(foo.bar); //imprime Hello World

Técnica 1 – Extender exports con Funcionalidad Adicional

Así las cosas, una técnica para exponer la funcionalidad de un módulo consiste en agregar nuevas funciones y propiedades a module.exports. Cuando este es el caso, node.js provee un acceso directo al objeto exports y no es necesario acceder a él a través de la forma un poco más larga module.exports. Por ejemplo:

//foo.js
exports.serviceOne = function(){ };
exports.serviceTwo = function(){ };
exports.serviceThree = function(){ };

Y como se podrán imaginar los usuarios de este módulo, al importarlo, obtendrán una referencia al mismo objeto exports y entonces podrán consumir toda esta misma funcionalidad.

//bar.js
var foo = require('./foo');
foo.serviceOne();
foo.serviceTwo();
foo.serviceThree();

Técnica 2 – Sustituir el Objeto exports Predeterminado por Otro Objeto

Probablemente ustedes podrán intuir, llegados a este punto, que dado que module.exports es el objeto que se expone públicamente para un módulo, entonces perfectamente podríamos sustituir el objeto que nos da node.js de forma predeterminada por nuestro propio objeto. Por ejemplo:

//foo.js
var service = {
   serviceOne: function(){ },
   serviceTwo: function(){ },
   serviceThree = function(){ }
};

module.exports = service;

El código en este último ejemplo se comporta exactamente igual que el código del ejemplo anterior. Es solo que esta vez hemos definido nuestro propio objeto a exportar.

Técnica 3 – Sustituir el Objeto exports Predeterminado por un Constructor

En todos los ejemplos hasta el momento, siempre exponemos una instancia de un objeto, pero nada nos impide sustituir al objeto module.exports por otros tipos de objetos como funciones y constructores. En el ejemplo siguiente exponemos un constructor, que el usuario del módulo puede utilizar más tarde para crear una instancia de este tipo de objeto.

//Foo.js
function Foo(name){
   this.name = name;
}

Foo.prototype.serviceOne = function(){ };
Foo.prototype.serviceTwo = function(){ };
Foo.prototype.serviceThree = function(){ };

module.exports = Foo;

Así las cosas, el usuario que importa este módulo recibe un constructor de Foo que puede usar para crear tantas instancias de este tipo como considere apropiado.

//bar.js
var Foo = require('./Foo');
var foo = new Foo('Obi-wan');
foo.serviceOne();
foo.serviceTwo();
foo.serviceThree();

Técnica 4 – Sustituir el Objeto exports Predeterminado por una Función

Es fácil imaginar ahora que, si podemos exponer un constructor, entonces también podemos usar una función común y corriente como el objeto que se expone a través de module.exports. El siguiente ejemplo expone una función que nos permit obtener acceso a un objeto privado del módulo, en este caso un objeto de servicio.

//foo.js
var serviceA = {};
serviceA.serviceOne = function(){ };
serviceA.serviceTwo = function(){ };
serviceA.serviceThree = function(){ };

var serviceB = {};
serviceB.serviceOne = function(){ };
serviceB.serviceTwo = function(){ };
serviceB.serviceThree = function(){ };

module.exports = function(name){
   switch(name){
      case 'A': return serviceA;
      case 'B': return serviceB;
      default: throw new Error('Unknown service name: ' + name);
   }
};

Y ahora el usuario del módulo, al importarlo, recibirá una referencia a nuestra función anónima, y al invocarla pues termina obteniendo acceso al objeto encapsulado de servicio. Por ejemplo:

//bar.js
var foo = require('./foo');
var obj = foo('A');
obj.serviceOne();
obj.serviceTwo();
obj.serviceThree();

Algunos programadores acostumbran invocar la función de inmediato en vez de almacenarla en una referencia e invocarla después. Por ejemplo:

//bar.js
var foo = require('./foo')('Obi-wan');
foo.serviceOne();
foo.serviceTwo();
foo.serviceThree();

Así que como podemos ver, todo lo que exponemos en module.exports es lo que se hace disponible al usuario que importa el módulo directamente y como vimos podemos exponer toda clase de objetos, funciones y constructores usando diferentes técnicas.

Sobre los Módulos y el Uso de Estado Global

Un aspecto significativo de los módulos en node.js es que todo lo que declaramos dentro del módulo es privado a dicho módulo. Esto marca una diferencia significativa de JavaScript en node en comparación con JavaScript en el browser en donde existe un objeto global (p.ej. window) que es el receptáculo final de todo lo que declaramos dentro de un archivo de JavaScript (a menos que lo coloquemos dentro de un alcance de función). En node existe también un objeto global pero nada se coloca en este objeto de manera automática como en el caso de JavaScript en el browser.

Sin embargo, es raro que sea necesario colocar algo en el objeto global en node.js dado que los módulos de Node.js se evalúan una sola vez y después esto se retorna el mismo objeto que module.exports exponga. Por eso es mejor utilizar un módulo para compartir estado global que colocar datos en el objeto global. Por ejemplo, el siguiente código expone un objeto con la configuración de una conexión a una base de datos de Mongo.

//config.js

dbConfig = {
  url:'mongodb://foo',
  user: 'anakin',
  password: '*******'
}

module.exports = dbConfig;

Ahora cada vez que necesitemos tener acceso a nuestra configuración de la base de datos podemos importar el módulo config.js. El código del módulo se evaluará sola la primera vez, así que podemos tener la garantía de que todos los demás módulos que usen nuestro módulo de configuración obtendrán una referencia al mismo objeto que se publico en module.exports la primera vez que este módulo sea requerido.

//foo.js
var dbConfig1 = require('./config');
var dbConfig2 = require('./config');
var assert = require('assert');
assert(dbConfig1==dbConfi2);

10 Apps de Apple para Programadores y Alfa Geeks

 

Lisping

Si te gusta la programación funcional y particularmente los lenguajes de programación de la familia Lisp, entonces te va a encantar Lisping. Esta aplicación te permite crear programas para los lenguajes de programación Scheme y Clojure y está disponible tanto para iPhone como para iPad. A fin de facilitar la edición de código entre paréntesis (como es común en esta familia de lenguajes) este editor provee un mecanismo intuitivo de edición que permite expandir  o colapsar un context dado o editar el contenido de un contexto por separado. Ahora sí que puedes probar tus ejemplos de Estructura e Interpretación de Programas de Computadora mientras miras por quinta vez la edición extendida de El Señor de los Anillos.

 

 

Raskell

Entonces ¿no eres aficionado de los lenguajes funcionales dinámicamente tipificados? Bueno, si prefieres la seguridad de la tipificación estática entonces seguramente apreciarás un editor como Raskell. Puedes editar y ejecutar tu código para el lenguaje de programación Haskell. Al igual que la aplicación anterior está disponible para iPhone y iPad. Ahora sí que puedes divertirte probando esos ejemplos de código de Aprenda Haskell Por el Bien de Todos mientras lees confortablemente desde tu cama tratando de ver si esta vez, de una vez por todas, terminas de entender que carajos es un monad.

 

Pythonista

Si lo tuyo son los lenguajes de programación multiparadigma, y particularmente si eres aficionado del lenguaje de programación Python entonces Pythonista es la aplicación que has estado buscando. Como es de esperarse, esta aplicación permite editar y ejecutar programas escritos con el lenguaje de programación Python, pero no solo eso, sino que ademas provee soporte para multi-touch, animación y sonido. Así que tus programas le pueden sacar el mejor provecho a tu dispositivo móvil. Al igual que las anteriores, está disponible tanto para tu iPhone como para iPad. Deja tu espíritu Pythonico correr libremente mientras esperas que terminen esos cinco minutos de comerciales durante la transmisión del último episodio de The Big Bang Theory.

 

Textastic

La verdad es que no todos nuestros lenguajes de programación favoritos tienen un editor especializado. Para aquellos de nosotros que somos aficionados a otros lenguajes de programación tenemos este gran editor llamado Textastic el cual provee resaltado de sintaxis para más 80 diferentes lenguajes de programación y con algunas características de edición realmente sobresalientes, particularmente que al ser usado desde un dispositivo móvil como el iPhone o el iPad el teclado se extiende con un conjunto de teclas comúnmente usadas por programadores. Con esta aplicación podrás editar tu código open source mientras tu novia te cuenta todo sobre su día durante la cena en ese lujoso restaurante.

Penultimate

¿Qué hay del diseño de software? No he tenido la suerte de encontrarme con una aplicación decente para hacer diagramas UML o diagramas de flujo. Sin embargo, si tienes un stylus, puedes crear diagramas y diseño a mano utilizando Penultimate. De hecho yo mismo he definido una buena colección de diagramas de flujo y diagramas UML de esta manera. Es una herramienta genial para discutir ideas de diseño, algoritmos o simplemente para registrar tus ideas y mantenerlas apropiadamente ordenadas.

 

Dash (Docs & Snippets)

¿Te has casando de buscar los JavaDocs para cierta clase? ¿Olvidaste los métodos de uno de los servicios de Angular? ¿Cuáles eran los parámetros de ese comando de git? Caray que cada vez que quiero leer los JavaDocs para una clase particular tengo que comenzar por Google para encontrar el enlace. Bueno, con Dash esos días se terminaron. Es como el Google de los programadores. Ofrece acceso fuera de línea a la documentación de decenas de APIs. Simplemente escribe el término que deseas y Dash encontrará la respuesta en un abrir y cerrar de ojos.

 

Code Runner

¿Te sucede que a veces quisieras tan solo correr un pequeño ejemplo de código Java o C# y te vez en la necesidad de crear todo un proyecto en tu IDE tan solo para probar una idea? Bueno, con Code Runner puedes evitar todas estas complicaciones. Esta herramienta provee un mecanismo para correr, de forma instantánea pequeños ejemplos de código en cualquier lenguaje de programación. La ejecución de ese programa está a un solo click de distancia. La herramienta viene con un conjunto de lenguajes de programación predefinidos, pero se puede extender la configuración con muchos más, a gusto del usuario.

Instapaper

Es difícil mantenerse al día con las últimas noticias en tecnología. Cada semana encuentro decenas de artículos interesantes que me gustaría leer y a menudo me veo ante la necesidad de recorder algunos que había leído en el pasado. A fin de conservar todas esa información útil a mano para mí no hay herramienta como Instapaper. Guardo los artículo que me interesan y los puedo leer mas tarde, fuera de línea, y cuando tengo más tiempo. Así que esta herramienta es perfecta para esos días en que tu novia te dice que tiene dolor de cabeza. Te ayudará a concentrarte en algo más y créeme serás el geek más informado de tu comunidad.

Evernote

Finalmente tengo una herramienta para mantener todas mis ideas organizadas y disponibles en todo momento. He estado en decenas de proyectos y siempre pierdo información valiosa con el paso del tiempo. Apenas si recuerdo detalles de requerimientos del pasado, or la dirección IP de cierto servidor de pruebas, o la ubicación de todos los repositorios en los que he trabajado, o el FQDN de aquel servidor de desarrollo al que tengo que ingresar vía SSH. Además es importante registrar todas esas ideas geniales para proyectos, investigaciones e incluso artículos por escribir en tu blog. Todo esto se facilita ahora con Evernote. Y si se lo combina con Evernote Web Clipper sirve un propósito similar que Instapaper, mencionado arriba.

SSH Term Pro

Estás viendo la versión remasterizada de Star Wars cuando sientes curiosidad de saber si ese script que dejaste corriendo el viernes en la noche antes de salir de la oficina terminó exitosamente. Bueno, ya no tienes que levantarte de tu asiendo, con SSH Term Pro puede acceder a ese servidor via SSH y volverte loco en la terminal. A tu jefe le va a encartar que descargues esta herramienta.

Bueno, eso es todo lo que tengo de momento. Si alguno sabe de otras aplicaciones de este tipo no dude en dejar un comentario y la incluiremos en esta grandiosa lista de aplicaciones para alfa geeks.

Ensayo sobre la Importancia del Encapsulamiento

El encapsulamiento consiste en más que simplemente definir métodos de acceso y de mutación para una clase. Es un concepto mucho más amplio de programación (no exclusivamente vinculando con la orientación a objectos) que consiste en minimizar la interdependencia entre los módulos y típicamente se implementa mediante el ocultamiento de información. Para comprender el concepto de encapsulamiento es de primordial importancia darse cuenta de que tiene dos objetivos fundamentales: ocultar la complejidad y ocultar el origen de los cambios.

Sobre Ocultar la Complejidad

El encapsulamiento está inherentemente relacionado con los conceptos de modularidad y abstracción y es por eso que, en opinión de este escritor, es necesario primero comprender estos dos conceptos.

Consideremos, como ejemplo, el nivel de abstracción en el concepto de un automóvil. Un automóvil es complejo en su funcionamiento interno; posee varios subsistemas, como el sistema de transmisión, el sistema de frenos, el sistema de abastecimiento de combustible, etcétera.

No obstante hemos llegado a simplificar este concepto dentro de una simple abstracción,  e interactuamos con todos los automóviles en el mundo a través de lo que denominaremos su interfaz pública: por ejemplo, sabemos que todos los automóviles tienen un rueda de volante a través de la cual controlamos la dirección del movimiento del vehículo, tenemos varios pedales, uno de los cuales sirve para acelerar y controlar la velocidad y otro con el que controlamos el frenado, y tenemos una palanca de cambios con la que controlamos si deseamos ir hacia adelante o hacia a atrás. Todas estas características constituyen la interfaz pública de la abstracción de un automóvil. Una mañana podemos conducir un sedán y por la tarde conducir un automóvil deportivo como si fueran exactamente la misma cosa. Incluso si son de diferentes fabricantes.

demaged carSin embargo, pocos de nosotros conocemos los detalles del funcionamiento de todos los sistemas bajo el capó del vehículo. Esta simple analogía demuestra que los seres humanos lidiamos con la complejidad de los objetos del mundo real mediante definir abstracciones con interfaces públicas que usamos para interactuar con ellos y ocultar todos los detalles innecesarios bajo el capó de estas abstracciones. Y es importante enfatizar la palabra “innecesarios” en esta última oración, porque la belleza de las abstracción es no tener que comprender todos los detalles para ser capaz de interactuar con un objeto internamente complejo, sino que basta con entender un concepto abstracto general, una idea simple de cómo funciona y cómo utilizarlo para realizar una tarea.  Es por ese motivo que la mayoría de nosotros no sabemos, o no nos interesa saber a detalle, cómo un automóvil acelera o frena o cambia de velocidad, pero el desconocimiento de estos detalles no nos impide conducir uno.

De una forma similar el software a menudo se construye con decenas de otros componentes más pequeños y especializados. Estas piezas se agrupan para formar componentes cada vez más complejos y a menudo no tenemos necesidad de comprender el funcionamiento interno de todas las piezas para interactuar con ellas. Basta con comprender cuál es el estímulo o entrada que acepta un objeto y la respuesta esperada del mismo. Es decir, cada componente se convierte en una caja negra para su usuario.

En su libro Code Complete, Steve McConnell usa una analogía de un iceberg: solo una pequeña porción de un iceberg es visible en la superficie del océano, la mayor parte de su verdadero tamaño esta oculto bajo el agua. De manera similar, en nuestros diseños de software las partes visibles de nuestros clases y módulos son su interfaz pública, y ésta está expuesta al mundo exterior, el resto debería estar oculta a los ojos del usuario. En palabras del mismo McConell

“La interfaz de una clase debería revelar tan poco como sea posible sobre su funcionamiento interno”.

Claramente, si nos basamos en nuestra analogía del automóvil podemos ver que el encapsulamiento es algo bueno puesto que oculta detalles de complejidad innecesaria para los usuarios. Hace que los automóviles sean simples de utilizar y de entender.

Sobre Ocultar el Origen de los Cambios

Ahora bien,  continuando con la analogía, pensemos en aquellos días en la que los automóviles no tenían un sistema hidráulico de dirección. Un día los fabricantes de autos finalmente lo inventaron y decidieron que, a partir de ese día, los vehículos debería tenerlo. Aun así esto no cambio en nada la manera como los conductores debían interactuar con sus vehículos. A lo sumo los conductores llegaron a disfrutar una mejor experiencia de manejo. El punto es que un cambio como este fue posible porque la implementación interna del automóvil estaba encapsulada, es decir, oculta a los ojos de los usuarios. En otras palabras, se podían realizar cambios de forma segura sin afectar la interfaz pública de esta abstracción.

De forma similar, si alcanzamos niveles apropiados de encapsulamiento en nuestros diseños de software podemos fomentar de forma segura el cambio y la evolución de nuestas APIs sin afectar a los usuarios de las mismas. Al hacer esto minimizamos el impacto de los cambios y fomentamos la independencia de los módulos. Así las cosas, el encapsulamiento es un medio para conseguir implementar otros importante atributo de un buen diseño de software: el acoplamiento débil.

En su libro Effective Java, Joshua Bloch resalta el poder del ocultamiento de información y el acoplamiento débil cuando dice:

“El ocultamiento de la información es importante por muchas razones, la mayoría de las cuales se derivan del hecho que mediante ésta se logra el desacoplamiento de los módulos que comprometen el sistema, permitiendo que éstos puedan ser desarrollados, probados, optimizados, utilizados, comprendidos y modificados de manera independiente. Esto acelera el desarrollo de sistemas porque los módulos pueden ser desarrollados de forma paralela. Alivia el costo del mantenimiento porque los módulos pueden ser comprendidos más fácilmente y depurados sin el temor de dañar otros módulos […] facilita el afinamiento del desempeño [ya que] estos módulos pueden ser optimizados sin afectar la correctitud de otros módulos [e] incrementa la reutilización del software porque los módulos que no están fuertemente acoplados suelen resultar útiles en otros contextos además de aquellos para los cuales fueron desarrollados”

Así que una vez más podemos ver que el encapsulamiento es un atributo muy deseable que facilita la introducción de cambios y que fomentan la evolución del software. Siempre que respetemos las interfaz pública de nuestras abstracciones tenemos la libertad de cambiar lo que queramos de su funcionamiento encapsulado.

Cuando se Rompe la Interfaz Pública

Entonces ¿qué sucede cuando no alcanzamos los niveles apropiados de encapsulamiento en nuestros diseños?

Bueno, imaginemos que un día los fabricantes de autos deciden poner la tapilla del combustible debajo del automóvil en vez de en uno de sus costados. Digamos que compramos uno de estos nuevos automóviles modificados y cuando se nos termina el combustible vamos a la gasolinera más cercana y entonces nos damos cuenta de que no sabemos en donde está la tapilla del combustible. Tras buscar un rato finalmente descubrimos que está debajo del auto, pero la manguera del combustible no llega hasta ella y la boquilla de la manguera no se corresponde con la nueva interfaz de la tapilla. En ese momento el mundo entero se cae a pedazos, todo porque hemos desobedecido el contrato de la interfaz pública de nuestra abstracción. Un cambio como este costaría millones. Necesitaríamos cambiar todas las bombas de combustible del mundo, sin mencionar talleres mecánicos y repuestos.

Cuando no alcanzamos niveles apropiados de encapsulamiento terminamos rompiendo la interfaz pública de nuestras abstracciones y entonces hay que pagar un precio: el costo del cambio. Asimismo, esta última parte de nuestra analogía revela que el no definir abstracciones con niveles apropiados de encapsulamiento terminará causando dificultades cuando los cambios ocurran.

Es digno de mencionar que los cambios siempre ocurrirán, lo importante es poder minimizar el impacto de esos cambios. Cuando los cambios ocurren en la interfaz pública de una abstracción el cambio es sumamente caro, pues afecta a todos sus usuarios.

Entonces el objetivo del encapsulamiento es reducir la complejidad de las abstracciones por medio de proveer un mecanismo para ocultar detalles de implementación. También ayuda a minimizar la interdependencia y facilitar el cambio. Maximizamos el encapsulamiento por medio de minimizar la exposición de los detalles de implementación.

Sin embargo, el encapsulamiento no servirá de nada si no definimos abstracciones apropiadas. Puesto de manera sencilla, no hay forma de cambiar la interfaz pública de una abstracción sin afectar y probablemente interrumpir a los usuarios. Así, el diseño de abstracciones apropiadas es de primordial importancia para facilitar la evolución del software y el encapsulamiento es solo una de muchas herramientas que contribuyen a la creación de dichas abstracciones. No obstante, ningún nivel de encapsulamiento va a hacer que una mala abstracción funcione bien.

Encapsulamiento con Java

Una de esas cosas que siempre queremos tener encpsuladas es el estado de una clase. El estado de una clase solo debería ser accesible a través de su interfaz pública.

En un lenguaje orientado a objetos, como Java, conseguimos esto por medio de los modificadores de accesibilidad (public, protected, private, etc). Con estos niveles de accesibilidad controlamos el nivel de encapsulamiento. Entre menos restrictivo el nivel, más caro será el cambio cuando este ocurra y más acoplada está la clase con otras clases dependientes (clases de usuario, subclases, etc).

En los lenguajes que soportan la herencia existen dos interfaces públicas: la interfaz pública compartida con los usuarios de la clase, y la interfaz protegida compartida con sus subclases, es decir, las subclases son un tipo especial de usuario.  De ahí que cuando se introduce la herencia en el diseño de software entonces se vuelve de mayor relevancia que diseñemos niveles apropiados de encapsulamiento para las interfaces públicas y protegidas a fin de facilitar los cambios futuros y la evolución de las APIs.

¿Por qué Getters y Setters?

Muchas gente se pregunta por qué necesitamos métodos de acceso y de mutación en Java (popularmente conocidos como getters y setters). Muchos dicen, ¿por qué no podemos simplemente acceder a los datos directamente?

El propósito del encapsulamiento aquí no es ocultar los datos mismos, sino los detalles de implementación de éstos y la manera en cómo son manipulados para presentarlos al usuario. Así que el propósito de los getters y setters es ofrecer una interfaz pública a través de la cual podemos obtener acceso a la información encapsulada en un objeto. Más tarde podemos alterar la representación interna de los datos sin comprometer la interfaz pública de la clase. Por el contrario, si se exponen los datos directamente comprometemos el encapsulamiento y, por lo tanto, la capacidad de cambiar la forma de manipularlos en el futuro sin afectar a sus usuarios. Estaríamos creando una dependencia con los datos directamente y no con la interfaz pública de la clase. Es un coctél perfecto de problemas y nos lo tendremos que beber cuando el cambio finalmente no encuentre.

Hay varias razones de peso por las cuales queremos encapsular acceso a nuestros campos. El mejor compendio de razones que he encontrado fue formulado, una vez más, por Joshua Bloch en Effective Java.

  • Se pueden limitar los valores almacenados en un campo(p.ej. género debe ser F o M).
  • Se pueden tomar acciones cuando el campo es modificado (desencadenar un evento, validar, etc).
  • Se puede proveer seguridad de hilos por medio de sincronizar los métodos.
  • Se puede cambiar la representación interna de los datos (p.ej. campos calculados, tipos de datos diferentes).
  • Se puede hacer que un campo sea de solo lectura.

Sin embargo, una vez más, queremos apuntar a que el encapsulamiento es más que solo ocultar campos. En Java, como ejemplo, podemos ocultar clases completas, y en el futuro inclusive módulos completos.

Lectura Adicionales