Archive for September 2006

Ahí queda eso #2

5 Dirty Old Town (The Pogues)

“Existen dos formas de diseñar software: una es hacerlo tan simple que obviamente no existen deficiencias, y la otra es hacerlo tan complicado que no existen deficiencias obvias”

C.A.R. Hoare

Subversion y Regreso al Pasado

5 A forest (The Cure)

Introducción

A veces, más de las que quisiéramos, hacemos un commit en nuestro scm que resulta, después de haberse hecho efectivo, un commit no deseado. Y todas esas veces uno se pregunta: ¿cómo puedo deshacer los cambios que he subido al repositorio? Veamos cómo se hace con la herramienta scm que utilizo en mi día a día: subversion. Nota: se presuponen conocimientos previos del uso y funcionamiento de esta herramienta.

Ejemplo

Para intentar ilustrar esto de la mejor forma posible, disponemos de un árbol de directorios y ficheros representando los contenidos tanto del repositorio (shared copy) como de la copia local (working copy). La revisión actual asociada a dicho árbol es la número 30, y tanto la copia local como el repositorio se encuentran sincronizados, por tanto, ambos en la revisión 30.

myproject tree

En la figura se puede ver que las fuentes del proyecto son muy sencillas, una clase java bajo src/ y un archivo de configuración de Ant, ambos bajo la rama principal del repositorio (trunk). También, siguiendo las convenciones de subversion, se dispone de un directorio para las tags del proyecto (tags/) y otro para las ramas (branches/).

Merging en subversion

Realmente el término merge no tiene en subversion su significado clásico: aplicar los cambios de una rama en otra. En subversion funciona de una forma un poco distinta, y básicamente lo que hace es comparar dos ramas y aplicar las diferencias a una copia local destino. Más formalmente, en una operación merge entran en juego 3 participantes:

  • El árbol del repositorio inicial (left side)
  • El árbol del repositorio final (right side)
  • Una copia local (target) sobre la que se aplican los cambios obtenidos de comparar leftside y rightside

Un aspecto que tiene que quedar claro, es que después del merge, tendremos en la copia local la diferencia entre ambos árboles en la forma de CAMBIOS SALIENTES. Posteriormente, deberemos hacer un commit de dichos cambios, o si no estamos de acuerdo, realizar una operación revert en la copia local.

El siguiente comando obtendría la diferencia entre la revisión número 15 de la rama experimental1 en el repositorio y la revisión número 30 -la última- de la rama principal, y posteriormente aplicaría dicha diferencia a la copia local myproject/:

$ svn merge http://localhost/svn/myproject/branches/experimental1@15
            http://localhost/svn/myproject/trunk@30 myproject/

Lo más importante aquí es comprender que podemos comparar cualesquiera versiones de cualesquiera dos árboles en el repositorio y aplicar la diferencia resultante a una copia local. Imaginemos que queremos comparar dos revisiones de un mismo árbol y aplicar el resultado de la comparación a nuestra copia local. Podríamos hacer:

$ svn merge -r29:30 http://localhost/svn/myproject/trunk myproject/

Mi principal objetivo no es dar una explicación exhaustiva del comando merge, y si tienes más interés en esto, recomiendo una visita a la sección de enlaces más abajo.

Haciendo cambios

Durante tu trabajo del hoy has tenido que cambiar la clase MyApp.java. El cambio consiste simplemente en que has puesto explícitamente un directorio asociado exclusivamente a tu máquina. La clase compila y pasa los tests manuales (sic). Y haces un commit de la clase.

$ svn commit -m "Changed directory path"
Sending        src/MyApp.java
Transmitting file data ...
Committed revision 31.

Ahora es demasiado tarde para no hacer saltar las alarmas, y a los cinco minutos ya tienes a un compañero en tu puesto quejándose de tu “imprudencia”.

Ouch! Deshaciendo cambios

¿Y qué puedo hacer? Lo primero que se te ocurre es obtener una copia de la revisión anterior (30) del fichero MyApp.java, sustituirlo en tu copia local y realizar un nuevo commit. Esto funciona para un fichero, para dos, para tres. Cuando nos encontramos con que el commit consiste en decenas de cambios, con ficheros añadidos, eliminaciones, reemplazos, modificaciones, etc el parche manual no parece tan atrayente, ¿verdad? Debe de existir una forma más sencilla de hacerlo. Lo que se explicó en las secciones anteriores nos servirá ahora para comprender cómo se deshace un commit no deseado. Para ello nos servimos de la capacidad del comando merge para comparar dos revisiones cualesquiera de dos árboles del directorio cualesquiera.

$ svn merge -r31:30 http://svn.example.com/repos/calc/trunk
U  src/MyApp.java

Por si se ha escapado: -r31:30 ¡Estamos obteniendo el resultado de comparar la revisión 31 con la 30! Es decir, la revisión origen es más reciente en el tiempo que la revisión destino. A esto se le llama una comparación backwards. Ahora tienes a MyApp.java marcado como modificado en tu copia local, de forma que puedes hacer un commit, y obtener el mismo árbol asociado a la antigua revisión número 30.

$ svn commit -m "Undoing commit"
Sending        src/MyApp.java
Transmitting file data ..
Committed revision 32.

Evidentemente, dado que subversion lleva un control de todo el historial de cambios, si alguien hace un checkout de la revisión 31 de la rama principal, obtendrá el fichero MyApp.java con la ruta del fichero escrita explícitamente. Pero ahora en el HEAD de la rama principal tenemos lo que realmente queremos, y dado que en la mayoría de los desarrollos lo que interesa es la estabilidad de la rama principal, es un efecto secundario sin gran repercusión.

Enlaces

Registros y Strategy Pattern

5 The kids aren´t alright (Offspring)

Introducción

No sé si será un problema común al que un programador se enfrente a lo largo de su vida, pero durante mi carrera ya me he topado con él un par de veces y, a raíz de un artículo refiriéndose al tema en cuestión, he decidido escribir un algo acerca de ello. Hablo del parseo de registros (record parsing). En concreto, del parseo de registros en archivos de textos, los cuales pueden contener varios tipos de de estos registros sin un orden prefijado y uno por línea.

Registros, registros

Por ejemplo, imaginemos que necesitamos recopilar información acerca de personas físicas. Para ello tenemos dos tipos de registros, uno que contiene el nombre y apellido de la persona, y otro que contiene el nombre, apellido, dirección y ciudad de residencia. Una de las formas más comunes de abordar el problema de cómo diferenciar los distintos tipos de registros es utilizar una cabecera en cada uno, indicando de qué tipo es. Veamos un ejemplo de posible entrada:

01234567890123456789012345678901234567890123456789...
————————————————–…
ONEMichael Smith
TWOTeddy Richardson New York Fifth Avenue

Como se puede apreciar, se utilizan los 3 primeros caracteres de la línea para definir el tipo de registro. En este caso tenemos el tipo ONE, y el tipo TWO. A parte de eso, los diferentes tipos de registros no tienen más campos en común. El objetivo final es obtener el conjunto de registros que recibimos en el fichero, de forma que luego podamos hacer con ellos lo que necesitemos. Muchas veces, proyectos de integración con sistemas antiguos -mainframes especialmente- se abordan compartiendo la información entre éstos y los sistemas nuevos mediante este tipo de ficheros basados en un registro por línea.

Una vez que ya tenemos algo parecido a una especificación informal de la definición del problema y qué necesitamos hacer para resolverlo, vayamos con el cómo. La primera forma que se te ocurre de resolver esto es de sentido común: un bucle que recorre el archivo de texto línea por línea, comprueba qué tipo de cabecera es (un if por cada tipo nos servirá), y obtiene los campos en base a ese tipo. Visto gráficamente:

Parsing flow

La siguiente será la representación de nuestro modelo, del cual queremos crear objetos a partir de los valores presentes en los registros del archivo de texto. Consta de una clase abstracta y dos concretas que extienden de la primera. Se han llamado RecordXXXXX, pero podrían haberse llamado Person o PersonAddress. Hay que recordar que de líneas de texto lo que obtenemos finalmente son objetos reales cuyas propiedades corresponden a los valores parseados en forma de registros. Un aspecto interesante aquí es que las clases no contienen un miembro type que defina qué tipo de registro es. Esto entra en el dominio del parseo y es por tanto responsabilidad de la clase parseadora. Simplemente, ¡no estaría bien aquí!

Basic Model

Y el código asociado:

public abstract class Record
{
}

public class RecordTypeONE extends Record
{
  public String firstName;
  public String lastName;
}

/* I am not a RecordTypeONE, hence do not attempt to make me inherit from it */
public class RecordTypeTWO extends Record {
  public String firstName;
  public String lastName;
  public String city;
  public String address;
}

Nota: para el que se haya alarmado, los campos de las clases los he mantenido public, sin encapsular, con fines de no añadir más complejidad al ejemplo (echa un vistazo al método setValue() de la clase FieldExtractor más abajo, y sabrás por qué).

Y, finalmente, éste es el método que sirve para parsear el conjunto de posibles registros que recibimos en el fichero de texto.

// dentro de una clase...
public List process()
{
  List records = new ArrayList<Record>();
  while (!EOF())
  {
    String line = getNextLine();
    Record record = null;
    if (line.substring(0, 3).equals("ONE")
    {
      record = new RecordTypeONE();
      record.firstName = line.substring(3, 11);
      record.lastName = line.substring(11, 29);
    }
    if (line.substring(0, 3).equals("TWO")
    {
      record = new RecordTypeTWO();
      record.firstName = line.substring(3, 9);
      record.lastName = line.substring(9, 30);
      record.city = line.substring(30, 39);
      record.address = line.substring(39, 60);
    }
    else
    {
      throw new InvalidRecordException("Record type not recognized");
      // or just remove this else block in order to ignore unknown record types
    }
    records.add(record);
  }
  return records;
}

Como vemos, esta solución es bastante sencilla. En principio, es suficiente, y la mayoría de las veces nos empecinamos -yo el primero- en intentar hacer las cosas más complicadas por nuestra tendencia natural a creer que las soluciones sencillas no son las mejores. Sin embargo, es todo lo contrario, las soluciones sencillas son las más efectivas y las más difíciles de dar con ellas. Esto es así porque la simplicidad da respuesta al mayor problema al que se enfrentan la computación en la actualidad: la gestión de la complejidad. Pero esa es otra historia.

Con lo que hoy nos toca, tengo que decir que, aunque sí que es cierto que esta solución al problema es correcta y hasta deseable, puede y debe mejorarse. Esto es así porque por naturaleza, el parseo de registros es cambiante. Estad seguros de que la probabilidad de que se produzca uno de los dos cambios siguientes en los requisitos es muy elevada:

  1. Gestión de los tipos de registros (nuevos, eliminaciones)
  2. Cambio en la composición de un registro (nuevos campos, eliminaciones, cambios de lugar)

Este es uno de esos casos en los que defenderse ante estos cambios es más económico que gestionarlos dentro del propio método, bien añadiendo o quitando bloques condicionales, modificando el contenido de éstos para hacer frente al punto 2 expuesto anteriormente, etc. Una de las herramientas más potentes con las que cuenta un programador utilizando un lenguaje orientado a objetos son los Patrones de Diseño. En pocas palabras,son soluciones documentadas a problemas de diseño conocidos que han funcionado en el pasado y siguen haciéndolo en la actualidad. Veamos un ejemplo de uno de los más sencillos y útiles, aplicándolo a nuestro problema de los cambios potenciales en el parseo de registros.

Introducción al Strategy Pattern

Este patrón es de los primeros que se explican en la mayoría de cursos. Básicamente se utiliza para aislar el comportamiento de un objeto del propio objeto. Se ve bastante claro en la siguiente figura:

Strategy Hierarchy

El comportamiento del que hablábamos, esto es, la funcionalidad que provee el método perform(), se ha externalizado a una nueva jerarquía simple de clases, de forma que Context simplemente llama a este método, sin saber cómo se ejecuta. Y aquí está la clave, como Context no sabe qué implementación se ejecuta (de hecho, le da igual), se puede cambiar la implementación en cualquier momento, incluso en tiempo de ejecución, entre las diferentes estrategias (Strategy1, etc).

Ventajas de hacerlo así, son varias:

  1. Como hemos dicho, se puede cambiar la implementación en cualquier momento
  2. Está más estructurado el conjunto de implementaciones, ya que cada una está en una clase. Esto
    falicita el mantenimiento. Se pueden añadir nuevas implementaciones y quitar o modificar las existentes de una forma muy limpia y eficiente
  3. Es útil para reducir el número de clases en el sistema. Si varias clases son prácticamente la misma
    pero difieren en el comportamiento, a lo que ayuda este patrón es a tener una única clase que utiliza
    las diferentes estrategias (comportamientos)
  4. Por supuesto, como veremos más adelante, nos ahorramos ese conjunto de bloques condicionales que
    siempre ensombrecen la elegancia de nuestro código

El Strategy Pattern en acción

Veamos ahora como encaja este patrón en nuestro ejemplo. La clase FieldExtractor enlaza una ristra de caracteres (con índices superior e inferior) con la propiedad correspondiente en un objeto Record. Un método interesante de esta clase es setValue(), ya que utiliza la Reflection API para establecer el valor de la propiedad del objeto Record, que como se verá después, el objeto FieldExtractor sólo conoce en tiempo de ejecución.

public class FieldExtractor
{
  protected int start;
  protected int end;
  protected String propertyName;
  public FieldExtractor(int start, int end, String propertyName)
  {
    this.start = start;
    this.end = end;
    this.propertyName = propertyName;
  }
  public void extractField(String line, Record record)
  {
    String value = line.substring(start, end + 1);
    setValue(record, value);
  }
  // Reflection API to the rescue!
  protected void setValue(Record record, String value)
  {
    Field field = record.getClass().getField(propertyName);
    field.set(record, value);
  }
}

La intefaz RecordReader define los métodos destinados al parseo de registros a partir de líneas de texto, usando objetos FieldExtractor para dicha función.

public interface RecordReader
{
  String getType();
  void addFieldExtractor(FieldExtractor fieldExtractor);
  Record processLine(String line);
}

La siguiente implementación básica da una idea más específica del funcionamiento del parseo de un registro, dada una línea de texto. Como se puede apreciar, mantiene referencias a un conjunto de objetos FieldExtractor, y cuando el método processLine() es invocado, los recorre todos, de forma que al final se obtiene un objeto Record con todos sus campos rellenos a partir de la línea de texto. También contiene referencias a la cadena de texto que define el tipo de registro que se encarga de parsear, así como su clase.

public class RecordReaderImpl implements RecordReader
{
  protected String type;
  protected Class recordClass;
  protected List fieldExtractors = new ArrayList();
  public RecordReaderImpl(String type, Class recordClass)
  {
    this.type = type;
    this.recordClass = recordClass;
  }
  public String getType()
  {
    return type;
  }
  public void addFieldExtractor(FieldExtractor fieldExtractor)
  {
    fieldExtractors.add(fieldExtractor);
  }
  public Record processLine(String line)
  {
    Record record = recordClass.newInstance();
    for (FieldExtractor extractor : fieldExtractors)
    {
      extractor.extractField(line, record);
    }
    return record;
  }
}

La ventaja de todo esto es que, para cada tipo de registro, no vamos a tener que definirnos una clase de forma separada, o cómo hacíamos antes, definir un nuevo bloque condicional asociado a su parseo. En cierto sentido, hemos cambiado creación por configuración. Es decir, en vez de crear un montón de clases parseadoras específicas a cada tipo de registro, simplemente tenemos una única clase, lo bastante flexible como para poder crear a partir de ellas objetos parseadores de forma dinánima. Y la de código -y mantenimiento- que nos hemos ahorrado.

Todo lo anterior no es muy útil sin un cliente que haga uso de ello. Para ello nos definimos una clase RecordProcessor, que será la encargada de recorrer línea por línea el archivo de texto y obtener finalmente el conjunto de objetos Record.

public class RecordProcessor
{
  RecordReaderConfiguration config;
  public RecordProcessor()
  {
    config.setUp();
  }
  public List process()
  {
    List records = new ArrayList();
    while (!EOF())
    {
      String line = getNextLine();
      RecordReader reader = config.getRecordReader(line);
      Record record = reader.processLine(line);
      records.add(record);
    }
    return records;
  }
}

Como vemos, RecordProcessor es bastante sencilla, invocando para cada línea a un objeto RecordReader, visto anteriormente. Lo interesante aquí es cómo se obtienen estos objetos RecordReader en tiempo de ejecución. Para ello, la clase RecordProcessor mantiene una referencia a un objeto RecordReaderConfiguration.

public class RecordReaderConfiguration
{
  protected Map recordReaders;
  public void setUp()
  {
    addRecordType1();
    addRecordType2();
  }
  public RecordReader getRecordReader(String line)
  {
    String type = line.substring(0, 3);
    RecordReader recordReader = recordReaders.get(type);
    if (recordReader == null) throw new IllegalArgumentException("Type does not exist");
    return recordReader;
  }
  protected addRecordType1()
  {
    RecordReader recordReader = new RecordReaderImpl("ONE", RecordTypeONE.class);
    recordReader.addFieldExtractor(new FieldExtractor(3, 11, "firstName"));
    recordReader.addFieldExtractor(new FieldExtractor(12, 28, "lastName"));
    recordReaders.add(recordReader.getType(), recordReader);
  }
  protected addRecordType2()
  {
    RecordReader recordReader = new RecordReaderImpl("TWO", RecordTypeTWO.class);
    recordReader.addFieldExtractor(new FieldExtractor(3, 11, "firstName"));
    recordReader.addFieldExtractor(new FieldExtractor(12, 28, "lastName"));
    recordReader.addFieldExtractor(new FieldExtractor(29, 40, "city"));
    recordReader.addFieldExtractor(new FieldExtractor(41, 59, "address"));
    recordReaders.add(recordReader.getType(), recordReader);
  }
}

Aquí es donde se hace el trabajo duro de parseo. El método setUp() -llamado por RecordProcessor en su constructor- se encarga de añadir los objetos RecordReader que parsearán el conjunto de todos los registros que pueden encontrarse en el fichero de texto. Estos objetos son almacenados en un Map, de forma que las keys del Map las forman los strings representando los distintos tipos de registro, y los values los forman a su vez los objetos RecordReader.

Finalmente, este es el aspecto que tiene el conjunto de clases e interfaces que he definido más arriba:

Final Structure

Gestión de cambios

Veamos cómo afectarían los cambios a esta nueva disposición de clases basadas en el Strategy Pattern.

1. Gestión de los tipos de registros (nuevos, eliminaciones)

Añadir un nuevo tipo, es tan sencillo como registrar un nuevo tipo de RecordReader en la clase RecordReaderConfiguration

2. Cambio en la composición de un registro (nuevos campos, eliminaciones, cambios de lugar)

Basta con modificar el cuerpo de los métodos en los que se crean los RecordReaders de la clase RecordReaderConfiguration -addRecordTypeXX()-

Notas finales
Supongo que todos aquellos fans -con razón- de los frameworks IoC (Spring, PicoContainer, MicroContrainer,etc), se habrán percatado de la gran ventaja de tener la configuración de los RecordReaders dentro de una clase separada (RecordReaderConfiguration). Os dejo para vosotros un asunto pendiente tan interesante como externalizar dicha configuración a una fuente externa (XML?) e inyectarla a la clase RecordProcessor.

Bibliografía

Ahí queda eso #1

5 Points of Authority (Linkin Park)

“El problema de esta empresa es que hay gente que tiene muy buenas ideas, y cuando después de dos años ven que no se ponen en práctica, se van a otro sitio”

Development Manager Anónimo

Barreras de Entrada

5 River of Sorrow (Antony & The Johnsons)

Nota: este post apareció originalmente en el blog que abrí cuando estuve en Nueva York trabajando, pero creo que encaja mejor en este.

Leyendo este libro, mira tú por donde que he descubierto por qué hay tanta gente en los establecimientos de Barnes & Noble (unas cadena de librerías aquí en los Estados Unidos) leyendo libros en pasillos, suelo y en cualquier rincón. Descaradamente, vamos.

¿Hobbits en Barnes & Noble?

Sí, hay una razón. Y una razón económica. Resulta que en el mundo de los negocios hay un término denominado barreras de entrada. Designa aquello que impide a la gente cambiarse del producto de tu competidor a tu producto. Sencillo. Por ejemplo, imagina que te propones lanzar tu producto estrella al mercado: el Lector/Grabador de discos VGD (Very Good Disc). Estos discos pueden almacenar series completas de televisión en un formato especial que permite disfrutar de una calidad excepcional de imagen y sonido. Además, como están fabricados con kryponita, pues no se rayan ni se rompen. ¿Cuáles son tus barreras de entrada? Jen es una consumidora de DVDs que normalmente compra una media de 4 películas a la semana, pagando religiosamente su cánon a la SGAE. Cuando un día se dispone a entrar en el videoclub para ver qué novedades se avecinan en los próximos días, ve por casualidad un cartel anunciando tu flamante nuevo lector/grabador. Ya recibió un folleto en su buzón hace unos días, pero recuerda que no le hizo mucho caso. Empieza a leer las nuevas características del aparato, y se queda asombrada. Siendo como es fan del cine, le encantaría poder disfrutar de todas esas nuevas características que publicitas en el cartel. Pero, antes de considerar comprarse uno, le surgen una serie de dudas. Piensa:

  1. Tendré que aprender a usar el nuevo lector/grabador
  2. Por supuesto, tengo que comprarlo
  3. Toda mi colección de DVDs (unos 2500, sí, hay gente muy apasionada), lo que me ha costado en tiempo y dinero reunirla, y no voy a poder verla en el VGD

Como véis, son cuestiones de lo más razonables. Son barreras de entrada hacia tu producto. Mientras existan, Jen seguirá fiel a su antiguo sistema DVD. ¡Pero si el mío es mucho mejor! Ya, pero para ella, mientras sigan existiendo esos obstáculos (sobre todo el 3), no le merece la pena cambiarse.
Y esto no es bueno para ti. No es lo que planeaste. Barreras de entrada. Ahora bien, como tú eres una persona muy espabilada en el mundo de los negocios, lo de antes es lo que hubiera pasado si no lo fueras, y pudiste vislumbrar lo que se avecinaba antesde invertir dinero en tu nueva creación, y estableciste (los puntos corresponden con los anteriores):

  1. El aspecto del aparato será igual que los actuales DVDs, con los mismos botones de play, atrás, adelante, grabar, etc etc. Los menúes como cualquier otro de los lectores actuales. Fácil para una experta como Jen.
  2. “Plan Prever VGD - Si al comprar tu nuevo VGD, nos traes tu viejo DVD, recibirás un descuento del 15% en el precio final”. Fantástico. Ahora, va a ser que Jen no se quiere desprender de su DVD por si acaso. Ver el siguiente punto.
  3. El nuevo lector de VGD podrá leer, además del flamante nuevo formato VGD, los antiguos y desfasados formatos de DVD.

Se ve por dónde va el asunto, ¿verdad? Ahora Jen tiene buenas razones para comprar tu producto. Ya no existen barreras de entrada. Y esto sí que es bueno para ti. Normalmente para todo el conjunto de potenciales usuarios hay que encontrar el conjunto de barreras de entrada que impiden que lances tu producto al mercado (sino quieres acabar siendo otro formato beta).
Sin embargo, para aumentar las posibilidades de éxito, no basta con eliminar las barreras. Hay que pensar estratégicamente, lo que significa que hay que ver más allá de lo obvio. Y esto es importante, porque existen barreras de entrada muy sutiles, que son las que en última instancia impiden que la gente se cambie a tu producto. Y la más importante de todas es:

No lo difícil que sea cambiarse a tu producto es lo que impide al usuario hacerlo, sino lo difícil que sería volver al producto que usaba anteriormente si el tuyo no le convence

Si lo piensas bien…tiene sentido. Jen piensa: “Estupendo, voy a poder leer y grabar DVDs antiguos. Ufff. Mis 2500 pelis siguen estando de actualidad. Pero, ¿qué pasa si grabo mis programas de TV favoritos en este nuevo formato tan maravilloso llamado VGD? Me parece que mi lector de DVD actual no podría leerlo, bueno, tampoco es problema, porque me desharía de él con el Plan Prever, y podría verlas en el nuevo. Pero lo que sí que me asusta es que no podría pasarle estos VGD a mi hermana, que tiene un lector de DVD. No podríamos charlar acerca de lo falso y engreído que es Marco Richardo -sí, sus programas de TV favoritos son culebrones. Ummm. Mejor sigo con mi lector de DVD”. Esta Jen piensa demasiado.

¿Y cómo solucionas esto? Así: tu nuevo lector/grabador de VGD puede grabar (no sólo leer) tus programas favoritos de TV en el antiguo formato DVD. Eso sí, perderás muchas de las ventajas audiovisuales que VGD te da. Pero, ¿sabes qué? Jen compra tu VGD. Y esto es bueno para ti.

¿Y os preguntaréis? ¿Qué tiene todo esto que ver con la cadena de librerías esta? Decía que había una razón para permitir, incluso rogar (sillones, cafeterías, lugares para leer) que leas en el establecimiento aunque no compres nada. ¿Pero para qué? ¡Yo lo que quiero es que la gente compre mis libros! Pues bien, tienes a toda esta gente que anda por la librería leyendo lo que le viene en gana durante horas y entonces uno se da cuenta de que …

La probabilidad de que encuentren algo que les guste, y compren finalmente, es linealmente proporcional al tiempo que pasan en el establecimiento

Cuando lo piensas bien, ¡tiene sentido! Tú empiezas a leer un libro, y si no te gusta, te arrepientes y lo vuelves a dejar en la estantería. Lo mismo con otro, y con otro. Hasta que te tiras leyendo un rato uno que acaba gustándote y te gustaría tenerlo, así que lo compras. Porque la cuestión final es, ¿y si lo que compro, va luego y no me gusta? Ahora Barnes & Noble es la cadena de libros más grande de Manhattan, que no es poco considerando que hay sólo aquí en la isla hay alrededor de 3 millones de habitantes, según me han dicho 9 millones en los días laborables.