jueves, 26 de junio de 2008

Team Skill 3: Definiendo el Sistema

En el Team Skill anterior (Team Skill 2: Entendiendo las Necesidades del Usuario), mencioné que todo lo que el libro trata (Managing Software Requirements - A Unified Approach) está orientado a una metodología orientada al plan, como RUP por ejemplo. Una metodología orientada al plan en general sirve para proyectos grandes, donde el negocio no es muy cambiante, donde se sabe que no van a haber cambios radicales en los requerimientos durante el desarrollo del proyecto (si vamos a construir un edificio, necesitamos los planos primero y, una vez aprobado estos planos, a nadie se le va a ocurrir pedirme una pileta de natación en el segundo piso, cuando el edificio ya está construido), y lo más importante: donde necesitamos documentación, calidad y seguridad desde la primera vez que el producto se implanta en producción. A nadie se le ocurriría usar una metodología ágil donde hay vidas en juego (imaginen un marcapasos hecho con Scrum o un cohete; imaginen que en dos semanas presentamos la primera versión de un respirador y lo ponemos a funcionar en una persona), o donde el proyecto es muy grande, o donde contamos con un equipo que en su mayoría son programadores juniors.

Por eso, es importante tener en cuenta el contexto al que apuntan estas Habilidades Grupales.

Sin embargo, nada impide que pueda llevar a cabo un proyecto con una metodología propia que pueda ser un mix de varias metodologías. Por ejemplo, en una Plainning Meeting, el Scrum Master podría aplicar alguna de las técnicas de relevamiento vistas en el post anterior con el Product Owner, ¿por qué no?, para entender algunos ítems del backlog, para ayudar al Product Owner a priorizar los ítems del backlog, para ayudar a relevar los requerimientos, que es a lo que apuntan sobre todo las primeras dos Habilidades Grupales.

Dean Leffingwell y Don Widrig son un poco rígidos a veces en la forma de presentar los temas. Tengamos presente que lo que ellos ofrecen son herramientas, que podrían ser usadas en otros contextos que no sean RUP.

¿Qué tenemos hasta ahora?

Hemos analizado el problema del usuario y hemos entendido sus necesidades. Tenemos un conjunto de funcionalidades anotadas por ahí. Si estamos en RUP, estamos acabando la fase de Inicio. Tenemos gran parte del Business Modeling, tenemos gran parte de los requerimientos, estamos por empezar con el Análisis y el Diseño.


Nos movemos del entendimiento del problema y la comprensión de las necesidades del usuario a empezar a definir la solución. Al hacerlo, daremos el primer paso fuera del Dominio del Problema, la tierra del usuario, y nos adentramos en el Dominio de la Solución, donde nuestro trabajo es definir un sistema para solucionar el problema. (¿Se acuerdan de esta pirámide que les mostré en el primer post?)


Los sistemas complejos requieren estrategias globales para la gestión de requerimientos de información y hay un montón de manera de organizarlos. La pirámide muestra una jerarquía de información, comenzando por las necesidades del usuario, siguiendo por las funcionalidades (features) y luego internándonos en los requerimientos de software más detallados, tal como se expresan con el modelado de casos de uso o con las especificaciones tradicionales.

Es hora de "bajar" esto a papel y plasmarlo en documentos, lo que para RUP significa llenar templates.

Los Artefactos de RUP

RUP, entre otras cosas, ofrece una serie de una serie de plantillas/documentos que "obliga" a llenar (el "obliga" depende de que tanto recortemos la metodología y que tanta documentación el auditor nos obligue a entregar). Los documentos son muchos. El libro, en este Team Skill, menciona cinco que considera cruciales para definir el sistema:
  • Documento de Visión
  • Especificación de Requerimientos de Software (SRS)
  • Visión de la Familia del Producto
  • Documento de Requerimientos de Marketing (MRD)
  • Documento "delta" de Visión

El Documento de Visión combina elementos del MRD con el SRS. Incluye las necesidades del usuario, las funcionalidades del sistema y los requerimientos comunes del proyecto. Cada proyecto debe tener un Documento de Visión.


Dean Leffingwell y Don Widrig proponen que para la primera versión del software, el Documento de Visión contenga:
  • Información introductoria
  • Descripción de los usuarios del sistema y de los mercados servidos
  • Funcionalidades para la versión 1.0
  • Otros requerimientos no funcionales
  • Funcionalidades relevadas que no serán inclui das en la versión 1.0
Los Documento "delta" de Visión son aquellos que haremos en las siguientes versiones, en los que deberemos especificar qué ha cambiado y cualquier otra información a incluir por propósitos de contexto.

El Documento de Requerimientos de Marketing (MRD) contiene, entre otras cosas:
  • Ventanas en el mercado
  • Targets de mercado
  • Packaging
  • Licenciamiento
  • Canales de Distribución
  • Costos de Marketing
  • Márgenes
  • Disponibilidad de Recursos
No hay mucho que hablar de los otros documentos, salvo del SRS, que lo trataré en el Team Skill 5: Refinando el Sistema.

El Sponsor del Producto

Otro de los temas que se trata en esta Habilidad Grupal es el Sponsor. Dean Leffingwell y Don Widrig reconocen que sin alguien que sea el Sponsor de los requerimientos para nuestra aplicación y que soporte las necesidades del cliente y del equipo de desarrollo, no tendríamos forma de saber si las decisiones difíciles y conflictivas se resolverán. Sin Sponsor es probable que tengamos requerimientos a la deriva, retrasos y decisiones poco óptimas forzadas por el deadline del proyecto.

Al Sponsor se lo nombra Dueño del Documento de Visión y de las funcionalidades en él contenidas. A su vez, el Sponsor y el equipo de trabajo deben proveer un Tablero de Control de Cambios (Change Control Board) que soporte las decisiones y asegure que los cambios en los requerimientos sean aprobados antes de ser aceptados.

Entradas Relacionadas

viernes, 20 de junio de 2008

Team Skill 2: Entendiendo las Necesidades del Usuario

Éste es el tercer post dedicado a Managing Software Requirements - A Unified Approach de Dean Leffingwell y Don Widrig (ISBN: 0-201-61593-2), el libro que propone una metodología ordenada para la Administración de Requerimientos. Hoy nos toca la Habilidad Grupal 2: Entender las necesidades del usuario. Ya pasamos por la Habilidad Grupal 1, en la que analizamos el problema, entendimos las causas principales mediante técnicas como el diagrama de Ishikawa, identificamos a los participales (stakeholders) y usuarios clave, definimos a grandes rasgos el alcance de la solución (diagramando la perspectiva del sistema con un gráfico parecido al diagrama de contexto del mundo de análisis de Yourdon) e identificamos las restricciones que serán impuestas en la solución. También hablamos un poco de Business Modeling, la primera de las disciplinas de RUP, que puede facilitarnos la tarea a la hora de especificar procesos y entender las reglas de negocio del mundo que estamos relevando.

Los síndromes

Managing Software Requirement propone entender el problema a ser resuelto antes de comenzar a desarrollar la aplicación. Todo de lo que habla el libro se orienta a una metodología orientada al proceso, como puede ser RUP por ejemplo, como puede ser CMMI. El libro es de 1999, por lo menos la edición que yo tengo, y en ese momento no eran para nada populares los ambientes ágiles.
El Team Skill 2 habla de tres "síndromes" que complican la obtención de requerimientos, que aumentan el desafío de entender las necesidades reales de los usuarios y stakeholders y que deben superarse:
  • El síndrome de "sí, pero": Se refiere a que el usuario no ha visto nada del sistema aún y probablemente no haya entendido lo que el desarrollador le explicó que sería. Después de una larga espera, al enseñarle una beta o una release candidate resulta que no era lo que esperaba, resulta que no obtuvo lo que necesitaba. Se debe obtener los PEROs lo antes posible.
  • El síndrome de los "Requerimientos Ocultos": Este síndrome se basa en que, a medida que el analista va encontrando requerimientos, otros muchos más quedan sin descubrir. Por eso, se debe identificar a todos los participantes del sistema. Ellos son en general la fuente de los "requerimientos ocultos". También es importante en algún momento decir: "basta, con lo que tenemos es suficiente, empecemos a construir".
  • El síndrome del "Usuario y Desarrollador": Parte de la idea de que el usuario y el desarrollador en general provienen de ambientes distintos y existe un gap comunicacional importante entre ellos. Esto implica que hablen distinto idioma, tengan distintos objetivos, intereses y motivaciones.
Estos síndromes son usados como metáforas para comprender el desafío de entender las necesidades del usuario y proveer un contexto para las Técnicas de Relevamiento que se desarrollarán más adelante en este mismo post.

Leffingwell y Widrig han descubierto que muy rara vez se obtienen especificaciones y requerimientos efectivos. En general, cuando se entrevista a los participantes respecto a sus necesidades o requerimientos para un nuevo sistema, éstos no describen ninguna de estas cosas, sino que se expresan en función del comportamiento que piensan tiene que tener el sistema para resolver la necesidad real. Así, sin darse cuenta, suelen reemplazar el qué ("yo necesito") por el cómo ("lo que creo que el sistema debe hacer para resolver mi necesidad").

Para aplacar los síndromes, se proponen varias Técnicas de Relevamiento, una variedad de técnicas que ayudarán a ganar una mejor comprensión de las necesidades reales del usuario y otros stakeholders.

Técnicas de Relevamiento

  • Entrevistas y Cuestionarios
  • Talleres de Requerimientos (workshops)
  • Brainstorming y Reducción de Ideas
  • Storyboarding
  • Role Playing
  • Prototyping
Aunque ninguna de estas técnicas es perfecta aplicada a cualquier circunstancia, cada una representa un medio proactivo de impulsar el conocimiento de las necesidades del usuario, convirtiendo aquellos requerimientos "difusos" en requerimientos "mejor conocidos".

1 - Entrevistas y Cuestionarios

La entrevista es una técnica simple y directa. Un cuestionario no es sustituto para una entrevista. Es muy importante el tener "cara a cara" al usuario y poder hacerle preguntas y prestar atención a sus modos de hablar y a sus gestos. Es importante establecer el perfil del participante, evaluar con él el problema, entender su entorno, relevar y validar supuestos.

En general, lo ideal sería que pudieran asistir a la entrevista dos analistas; uno para dialogar y observar al usuario trabajar en su ambiente, y el otro para que constantemente esté tomando notas textuales de todo lo que ve y oye.

2 - Talleres de Requerimientos

El libro la recomienda como una de las técnicas más poderosas para captura de requerimientos. Se trata de reunir a un grupo de usuarios clave, juntarlos con algún incentivo y extraerles información. En general, en este tipo de workshops hay un moderador experimentado, que se encarga de llevar a cabo la reunión de forma ordenada. Una técnica muy popular es la de Focus Group.

3 - Brainstorming y Reducción de Ideas

Suelen hacerse durante los talleres de requerimientos. En toda sesión de Brainstorming hay dos partes principales:
  • Generación de Ideas
  • Reducción de Ideas
Hay varias formas de hacer un Brainstorming. Una de ellas es ésta:

En la primera parte:
  • Se juntan los participantes
  • Se reparten post-it (o algo similar para anotar) y marcadores negros
  • Se explican las reglas
  • El moderador explica el objetivo del Proceso (por ejemplo: "mejorar la funcionalidad de X módulo del sistema")
  • Los post-it son escritos con las ideas
  • El moderador junta los post-it y los pega en la pared (Es muy importante que nadie critique ninguna idea. Lo importante es generar un número populoso de ideas sin importar su credibilidad o aplicación práctica.)
  • Esta parte termina cuando los participantes se quedan sin ideas
En la segunda parte:
  • Poda de ideas: discutimos cada una de las ideas y nos quedamos con las más viables
  • Agrupamiento de ideas: las podemos agrupar por categorías
  • Definición de funcionalidades: el creador de la idea puede desarrollar un poco más qué fue lo que se le ocurrió
  • Priorización de ideas
El Brainstorming es un tema muy interesante en el que otro día prometo explayarme más (quizá le dedique uno o más posts a esto). Un libro muy bueno, que trata este tema, es el de Edward Bono - El Pensamiento Lateral: Manual de Creatividad (libro fundamental sobre Pensamiento Lateral) (ISBN 978-950-12-9069-1).

4 - Storyboarding

El Storyboarding sirve mucho para mitigar el síndrome del "sí, pero". Es económico, user-friendly, fácil de crear y modificar. Permite una rápida revisión de las GUI y de la visualización de los datos.

El libro menciona tres tipos de Storyboarding:
  • Activos: Pueden ser slides. Lo que se intenta es mostrar la "película" aún no producida.
  • Pasivos: Cuentan una historia al usuario. Puedes ser sketch, fotos, impresiones de pantalla, etc.
  • Interactivos: Simulaciones, demos interactivos, etc... (Similar a un Prototipo.)

5 - Role Playing

Cada miembro del equipo toma el lugar del usuario y ejecuta las actividades laborales del cliente. Hay varias variantes de esto, por ejemplo: Recorrido Guiado o Tarjetas CRC (Clase-Responsabilidad-Colaboración), muy útil en el mundo de los objetos.

6 - Prototyping

Un prototipo es muy útil para mitigar los síndromes del "sí, pero" y de los "requerimientos ocultos". Lo ideal es escoger la funcionalidad más nebulosa, la más complicada de entender o la que más extraña parezca y prototiparla.

Principalmente hay dos tipos de prototipos:
  • Prototipos Desechables: Se usan y se tiran. Sólo se guarda el conocimiento adquirido.
  • Prototipos Evolutivos: El código se reutiliza para comenzar a construir el sistema encima de él.
Los prototipos son muy útiles para validar requerimientos o para establecer viabilidad de una tecnología.

Entradas Relacionadas

viernes, 13 de junio de 2008

Java a nivel de Bits

Para este post, quisiera tratar un tema que es algo tabú en la comunidad Java (y entiendo por qué). Cuando un programador Piensa en Java, en general, se imagina a esos monstruos gigantes que son los EJBs, que viven en contenedores terroríficos de componentes que manejan transacciones, seguridad y miles de incumbencias transversales de forma transparente y casi mágica. Casi automáticamente aparecen los millones de frameworks de aplicaciones enterprise, que se pueden sumar todos y combinar, y además todos los componentes son capaces de serializarse por sí­ solos, de viajar a través de la red y comunicarse a través de diferentes servidores de aplicaciones y diferentes Virtual Machines, en un mundo enteramente distribuido en que las clases viven ofreciendo sus servicios y conversando entre sí­ a través de mensajes.

Suena extraño cuando uno vuelve a releer la historia, después de saber todo esto, y recuerda que originalmente Java nació como un lenguaje multiplataforma para microcontroladores, un lenguaje para sistemas integrados o empotrados en pequeños dispositivos electrónicos (sí­, como electrodomésticos, por ejemplo).

Sun Microsystems lanzó la primera versión de Java en agosto de 1995 y, desde 1995 hasta hoy, gracias al aporte de las inmensas y poliformes comunidades Open Source ha crecido de manera increí­ble. Por eso muchos programadores se han olvidado que también se puede programar aplicaciones de bajo nivel en Java y hay mucha gente que usa Java para dispositivos de recursos limitados. JavaME (microedition), por ejemplo, es el conjunto de librerí­as que Sun ha publicado para trabajar con una Virtual Machine mucho más liviana y reducida, conocida como KVM.

Por eso, a la hora de trabajar con estas plataformas de recursos limitados (como celulares, microcontroladores, etc), es importante ser económico con el espacio en el guardado de datos. En aplicaciones de este tipo, en general, uno persiste parámetros de configuración y muchos valores que son pequeños, que habitualmente ocupan unos escasos bits. Entonces, si tenemos un dato booleano, por ejemplo, ¿por qué guardarlo en un byte, desperdiciando 7 bits que podrí­an ser valiosos para guardar otros 7 booleanos más, o algún otro dato?

Mediante dos pequeños ejemplos escritos en una aplicación JavaSE con Java6, mostraré la forma de guardar una colección de unsigned shorts (sí­, no es un error, ya sé que en Java no existen los unsigned) ocupando 16 bits para cada uno de ellos y una colección de SmallBytes de tan sólo 4 bits sin desperdiciar ni un bit de espacio. Lo que quiero mostrar con el primer ejemplo es que es posible burlar el bit de signo que la Virtual Machine reserva para un tipo de dato primitivo numérico, guardando en disco un short que va de 0 a 65535 (y no de -32768 a 32767 como serí­a por defecto para un short signado). Luego, usando los conceptos aprendidos en el primer ejemplo, mostraré cómo grabar estos SmallBytes de 4 bits, uno pegado al otro, sin desperdiciar espacio intermedio. Para esto, abriré un archivo y en ellos guardaré bytes que contengan dos SmallBytes cada uno.

EjemploUnsignedShorts

Para empezar, vamos a ver dos consideraciones importantes, que son caracterí­sticas fundamentales de la Virtual Machine de Java.



Tipos de Datos Primitivos:


TipoTamañoRango
char16 bitsde 0 a 65535
byte8 bitsde -128 a 127
short16 bitsde -32768 a 32767
int32 bitsde -2147483648 a 2147483647
long64 bitsde -9223372036854775808 a 9223372036854775807
float32 bitsde 1.40239846e-45f a 3.40282347e+38f
double64 bitsde 4.94065645841246544e-324d a 1.7976931348623157e+308d.

A diferencia de C/C++, Java no cuenta con tipos de datos unsigned. Por eso el rango de un short, por ejemplo, que como vemos en el cuadro anterior ocupa 16 bits, es de -32768 a 32767. Si tuviéramos que tratar con un tipo de datos entero positivo que se fuera del rango de 2^15-1, tendríamos que declarar un int. Si estamos trabajando en JavaME para un celular de bajos recursos (imaginen un Nokia 6010, que sólo puede cargar aplicaciones de 64K de jar), desperdiciar un bit de signo sabiendo que no lo vamos usar es un pecado mortal.

En el ejemplo voy a trabajar de la siguiente forma: en memoria voy a manipular un int, pero un int que sé que va a caer en el rango de 0 a 65534 (me reservo el último número: 65535 para el End Of File). Pero a la hora de persistir, voy a manipular el dato a nivel de bytes y voy a guardar sólo dos bytes, que son todo mi número.

Y aquí llegamos a la segunda consideración importante de la Java Virtual Machine:


Big-endian


Este ejemplo funciona, ya que la JVM organiza la memoria en formato big-endian. Si estuviéramos trabajando en C o cualquier otro lenguaje que compile directamente sobre el procesador de una PC tendríamos exactamente el caso opuesto, ya que la arquitectura de una PC aloja las tiras de bytes en formato little-endian. Lo que implica que sea más fácil el ejemplo, ya que leeremos el número de la forma natural, sin tener que invertir los bytes.



La escritura

Empecemos escribiendo el método que se encargará de guardar el unsigned short en un OutputStream:


private OutputStream salida;

public void writeUnsignedShort(int datoDe16bits) throws IOException {
        byte[] codeBytes = new byte[2];
        codeBytes[0] = (byte)(datoDe16bits & 0xFF);
        codeBytes[1] = (byte)((datoDe16bits & 0xFF00)>>8);       
        salida.write(codeBytes);
}

Se recibe por parámetro un int, como ya había adelantado, pero como se trata de un número positivo más chico que 65534, sólo los dos primeros bytes de los cuatro que ocupa el int van a estar llenos. La "parte alta" del parámetro tendrá basura que no me interesa persistir.

Primero me creo un array de bytes donde voy a guardar los dos bytes. Luego, lo cargo, primero con la parte más baja del entero (el primer byte), después con la segunda parte (el segundo byte). Eso es lo que hago en estas dos líneas:

codeBytes[0] = (byte)(datoDe16bits & 0xFF);
codeBytes[1] = (byte)((datoDe16bits & 0xFF00)>>8);

Hagamos un ejemplo a nivel de bits para entender mejor como funciona estas máscaras:

datoDe16bits = 00000000.00000000.01100110.11011011

Para obtener el primero de los 4 bytes del int, simplemente hago un and con la máscara 0xFF (00000000.00000000.00000000.11111111 en binario).

00000000.00000000.01100110.11011011 & 00000000.00000000.00000000.11111111 = 00000000.00000000.00000000.11011011

Lo que me devuelve la operación del AND es un entero, cuando lo casteo a byte, simplemente me quedo con: 11011011.

En la siguiente línea hago parecido, pero con la máscara 0xFF00:

00000000.00000000.01100110.11011011 & 00000000.00000000.11111111.00000000 = 00000000.00000000.01100110.00000000

A continuación hago un shifteo de 8 posiciones hacia la derecha, para quedarme con: 00000000.00000000.00000000.01100110. Y al castear: 01100110.

Estos dos bytes, así como fueron separados van a parar a la salida, un OutputStream.


La lectura


private InputStream entrada;

public int readUnsignedShort() throws IOException {
        int datoDe16bits = (entrada.read() & 0xFF);
        datoDe16bits += (entrada.read() & 0xFF) << 8;
        return datoDe16bits;
}

Cuando leo lo que hago es leer los dos bytes por separado y los cargo en el entero, ubicándolos en la sección que les corresponde. El shifteo esta vez es hacia la izquierda, para que el segundo byte se posicione donde debe. No hay mucho que explicar acá. Es casi lo mismo que la escritura, pero el proceso inverso.

El lector más atento quizá se ha preguntado por qué estoy usando una máscara de 0xFF cuando a primera vista parece estar de más. Recordemos cuanto valía el byte de la parte más baja del ejemplo anterior: 11011011. Como el operador << (corrimiento de bits) es un operador aritmético y no lógico, como sí es >>>, si yo hiciera esto directamente:

datoDe16bits += entrada.read() << 8;

Sin aplicar la máscara me vería en graves problemas, porque si fuera el número 11011011, Java interpretaría que se trata de un número negativo (porque el primer bit de signo está en 1), y luego tomaría la parte entera de los siete bits restantes como el complemento a la base. Por ende, como podrán ver, obtendría cualquier número (encima negativo) menos el que yo había almacenado. Como el operador & (and) es un operador lógico, después de efectuar esta operación, obtengo una serie de bits que Java sabe que son bits lógicos y que no debe tratar de interpretar como número.

La prueba de que esto funciona se hace grabando 16 unsigned short en un archivo llamado shortsFile.bin. Luego se cierra el archivo, se vuelve a abrir, se recuperan los números y se muestran en la consola para verificar efectivamente que se devuelven los mismos números que se habían cargado (16 números que son potencias de dos menos dos).

El código se puede descargar de:


Hay dos cosas nada más que me gustaría destacar de este código. Una, que, como podrán verificar si lo corren, les genera un archivo de 32 byes. Si lo dividimos por la cantidad de números que cargamos, 16, obtenemos que cada número está pesando exactamente 2 bytes (Gualá!). Dos: al final del archivo guardo un número extra: 65535 (EOF). En realidad esto no hace falta, ya que Java avisa cuando se alcanza el fin de archivo. Esto podría removerse para no desperdiciar esos dos bytes finales. Este EOF me quedó porque primero elaboré el ejemplo de los SmallBytes en donde sí es necesario tener mi propio EOF, y el que pasaré a explicar.

EjemploSmallBytes


Si en el ejemplo anterior trabajamos a nivel de bytes, en este vamos a ir un poco más a bajo nivel y trabajar a nivel de bits. El objetivo: guardar números de 4 bits que van de 0 a 14 (el 15 lo guardo para el EOF), juntando dos de ellos en un byte.

Para ser un poco elegante (y sólo porque estamos en JavaSE y no en JavaME donde tener clases de más también hace aumentar el tamaño del jar final), voy a usar una clase llamada SmallByte que hereda de Number, que sea hermanita de Integer, Short, etc...

public class SmallByte extends Number {
   
    private static final long serialVersionUID = 1L;       
    private final byte EOF = 15;
   
    private byte number;

    public SmallByte() {
        number = 0;
    }
   
    public SmallByte(byte number) {
        setValue(number);
    }
   
    public SmallByte(int number) {
        setValue((byte) number);
    }
   
    public void setAsEOF() {
        number = 15;
    }
   
    public boolean isEOF() {
        if (number == EOF) {
            return true;
        }
        return false;
    }
   
    public void setValue(byte number) {
        if (number >= 16) {
            throw new IllegalArgumentException("Un SmallByte no puede ser mayor que 15");
        }
        if (number < 0) {
            throw new IllegalArgumentException("Un SmallByte no puede ser negativo");
        }
        this.number = number;
    }

    public void setValue(int number) {
        setValue((byte) number);
    }
   
    @Override
    public byte byteValue() {       
        return number;
    }

    @Override
    public double doubleValue() {
        return number;
    }

    @Override
    public float floatValue() {
        return number;
    }

    @Override
    public int intValue() {
        return number;
    }

    @Override
    public long longValue() {
        return number;
    }

    @Override
    public short shortValue() {
        return number;
    }   

}

El ejemplo es un poquito más complicado que el anterior, pero tampoco tanto. Se los voy a dejar a ustedes para que lo miren si quieren porque ya estoy cansado (jajaja). Además la entrada se está haciendo muy larga.

Lo que hago en este ejemplo es abrir un archivo llamado smallbytes.bin en un OutputStream, le cargo 15 Smallbytes (más el Smallbyte EOF de fin de archivo), lo cierro (noten que el archivo pesa exactamente 8 bytes: 8/16 = 0.5, medio byte por número), lo vuelvo a abrir de vuelta en un InputStream, leo los números, los tiro en la consola y verifico que sean los mismos que cargué.

El procedimiento es bastante parecido en realidad. El SmallByte, en memoria, en realidad es un byte y al guardar y leer lo hago a nivel de bytes, leyendo la parte alta y la parte baja del byte y haciendo un corrimiento de cuatro posiciones a la derecha o a la izquierda, dependiendo de si estoy leyendo o escribiendo. En este caso el EOF sí es indispensable porque si la cantidad de números que tengo que guardar es impar, como en este caso, el último byte del archivo va a guardarse medio lleno. Como todos sabemos, un byte medio lleno no existe. Un byte medio lleno significa que la mitad tiene un dato que nos interesa y la otra mitad es basura. Java avisa que se encontró el fin de archivo, pero una vez leído el byte entero. De alguna forma tenemos que darnos cuenta al leer información útil que la parte alta de ese byte no es un número que se haya cargado. Al leer EOF, en este caso 15, sabremos que la lectura de datos ha terminado.

package elblogdelfrasco.javaSE.javaaniveldebits;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;

public class EjemploSmallBytes {

    private InputStream      entrada;
    private OutputStream     salida;   
   
    public void writeSmallBytes(SmallByte[] datos) throws IOException {
       
        byte[] datosToWrite = new byte[datos.length / 2 + 1];
        boolean byteLleno = true;
       
        for (int i = 0; i < datos.length; ++i) {
            if (byteLleno) {
                byteLleno = false;
                // Se va a llenar la parte baja de este byte
                datosToWrite[i/2] = (byte) (datos[i].intValue() & 0xF);               
            } else {
                // Se va a llenar la parte alta de este byte
                datosToWrite[i/2] += (byte) (datos[i].intValue() & 0xF) << 4;
                byteLleno = true;
            }
        }
       
        // El último byte que se guarda es el de fin de archivo
        SmallByte eof = new SmallByte();
        eof.setAsEOF();
       
        if (byteLleno) {
            // El EOF se guarda en la parte baja del último byte
            datosToWrite[datosToWrite.length - 1] = eof.byteValue();
        } else {
            // El EOF se guarda en la parte alta del último byte
            datosToWrite[datosToWrite.length - 1] += (byte) (eof.intValue() & 0xF) << 4;
        }
       
        // Se escribe la tira de bytes en el archivo
        salida.write(datosToWrite);
    }
   
    public SmallByte[] readSmallBytes() throws IOException {
       
        ArrayList datos = new ArrayList();
       
        // Del archivo de entrada se leerá de a un byte, osea: de a 2 SmallBytes
        try {
            SmallByte smallByte;
            boolean eof = false;
            while (!eof) {
                int dato = entrada.read();               
                // Tomo la parte baja del dato
                smallByte = new SmallByte(dato & 0xF);
               
                if (!smallByte.isEOF()) {
                    datos.add(smallByte);
                   
                    // Tomo la parte alta del dato
                    smallByte = new SmallByte((dato & 0xF0) >> 4);
                    if (!smallByte.isEOF()) {
                        datos.add(smallByte);
                    } else {
                        eof = true;
                    }                   
                } else {
                    eof = true;
                }
            }
        } catch (IllegalArgumentException iae) {
            iae.printStackTrace();
        }
       
        SmallByte[] res = new SmallByte[datos.size()];
        return datos.toArray(res);       
    }
   
    public void saveBytes(SmallByte[] datos) {
        try {
            salida = new FileOutputStream("smallbytes.bin");
            writeSmallBytes(datos);
            salida.close();
        } catch (IOException ioe) {
            ioe.printStackTrace();
        }       
    }
   
    public SmallByte[] readBytes() {
        try {
            entrada = new FileInputStream("smallbytes.bin");
            SmallByte[] datos = readSmallBytes();
            entrada.close();
            return datos;
        } catch (IOException ioe) {
            ioe.printStackTrace();
            return null;
        }
    }
   
    public void execute() {
               
        // Cargo una lista de SmallBytes de 0 - 14       
        SmallByte[] datos = new SmallByte[15];       
        for (int i = 0; i < 15; ++i) {
            datos[i] = new SmallByte(i);
        }
       
        // Los guardo en el archivo
        saveBytes(datos);
               
        // Leo los datos en un nuevo array de SmallBytes
        SmallByte[] datosRecuperados = readBytes();
       
        // Se muestran los bytes recuperados en la consola
        for (int i = 0; i < datosRecuperados.length; ++i) {
            System.out.println("SmallByte: " + datosRecuperados[i].byteValue());
        }
       
    }
   
}

Descargar el Código

El código completo lo he puesto para descargar en un repositorio SVN de Assembla:



Espero que les haya gustado/servido.

Por un mundo más libre!
Salud!

sábado, 7 de junio de 2008

Team Skill 1: Analizando el Problema

En el post anterior, presenté el libro Managing Software Requirements - A Unified Approach de Dean Leffingwell y Don Widrig (ISBN: 0-201-61593-2). El libro propone una metodología ordenada para la Administración de Requerimientos, enfocándose en 6 Habilidades Grupales (Team Skills) que abarcan desde analizar el problema del usuario en la fase temprana de Inicio, hasta la verificación y/o validación para corroborar que se sigue construyendo el sistema correcto en la fase de construcción.

En la introducción dada en el post anterior se habló de las causas principales de los fracasos y la pobre calidad en los proyectos de desarrollo de software, y se dieron las definiciones de Problema, Necesidades del Usuario, Funcionalidades del Sistema y Requerimientos de Software. Quiero destacar que la diferencia fundamental entre Funcionalidades del Sistema y Requerimientos del Software es que las primeras pertenecen a la Visión del Usuario y los segundos a la Visión del Desarrollador. Los features son descripciones de alto nivel, expresados en el lenguaje del usuario; en cambio, los requerimientos son descripciones que expresan dichas funcionalidades de un modo más detallado. Los requerimientos deben "testeables" y poder ser validados.

Pasos para entender el Problema

Ahora sí, metiéndonos en la Habilidad Grupal que hoy nos concierne, introduciremos un conjunto de habilidades que el equipo puede aplicar antes de comenzar con el desarrollo de la aplicación. Leffingwell y Widrig mencionan cinco simples pasos, una técnica de análisis que puede ayudar al equipo a obtener un mejor entendimiento del problema a ser resuelto.

1 - Acordar la definición del Problema

Una buena práctica, por ejemplo, sería anotar en un papel los siguientes datos:

Elemento
Descripción
El problema de
Describir del problema.
Afecta a

Identificar los participantes afectados
por el problema.
El resultado del cual

Describir el impacto del problema en los
participantes y en las actividades del negocio.
Beneficios de

Indicar la solución propuesta y una lista
de beneficios clave.

Los datos de esta tabla, por supuesto, se irán completando más detalladamente a medida que se completen los siguientes pasos.

2 - Entender las causas principales: El problema detrás del problema

Hay una variedad de técnicas para encontrar las causas principales de un problema, pero una de las más conocidas, y la que propone el libro, es la del diagrama de Ishikawa, también conocido como diagrama de causa-efecto, también conocido como espina de pescado.

Este diagrama permite identificar de forma gráfica las diferentes causas de un problema. En la punta de la flecha se escribe el problema y en las espinas que inciden sobre la recta se escriben las causas que al analista y al usuario van encontrando. (Otra variación más sofisticada de este diagrama involucra sub-espinas donde se ubican diferentes causas de un mismo tipo; cada espina identificaría una categoría y cada sub-espina las causas que pertenecen a esa categoría.)

Luego de completar el diagrama de Ishikawa, podemos pasar a una segunda etapa, que consiste en la priorización de las causas. No todas las causas merecen ser estudiadas, no todos los problemas merecen ser resueltos; a veces, el costo de arreglar un problema puede exceder el costo mismo del problema.

Podríamos usar un simple diagrama de Pareto para priorizar las causas.

3 - Identificar a los participantes y a los usuarios

Los usuarios son las personas que determinarán el éxito o fracaso del sistema. Son los interesados fundamentales de que el sistema funcione como ellos necesitan, porque son los que lo van a usar cuando el mismo esté implantado en producción. Los participantes, o el resto de los stakeholders, son cualquier persona materialmente afectada por la implementación del sistema. Algo así como usuarios indirectos. Son los más difíciles de encontrar.

4 - Definir el Alcance de la solución

Para definir el alcance se puede usar algo así como un viejo Diagrama de Contexto, como los que usábamos en análisis estructurado de Yourdon, pero bueno, el libro muestra uno un poco más aggiornado, por supuesto, reemplazando los insulsos rectángulos que representaban a las entidades externas por actores de UML.

El libro lo llama acertadamente: La Perspectiva del Sistema.

5 - Identificar las restricciones a ser impuestas en la solución

Una restricción es una disminución en el grado de libertad que tenemos al proveer la solución. Algunas restricciones pueden convertirse en requerimientos. Otras afectarán a recursos, planes de implementación o de proyecto.

Las restricciones podrían ser del tipo:
  • Económicas
  • Políticas
  • Técnicas
  • de Sistemas
  • de Medio Ambiente
  • de Cronograma
  • de Recursos
  • y muchas más...

Modelado de Negocio

Existe una gran variedad de técnicas que pueden ser usadas en el análisis del problema. En concreto, Managing Software Requirements hace incapié en el Modelado de Negocio , una técnica específica que funciona bastante bien en sistemas complejos de información que soportan las infraestructuras claves del negocio (supongo que su punto más fuerte es que se trata de una técnica que proviene del mundo del análisis orientado a objetos). El equipo puede usar Business Modeling para comprender la forma en que el negocio evoluciona y para definir dónde se podrá desplegar aplicaciones más productivas. El libro habla de un paralelismo entre el Modelo de Negocio y la construcción de la aplicación y dice que es posible usar estos puntos en común para dar origen a la etapa de diseño del software.

El Modelado de Negocio permite:
  • Proveer una definición consistente del negocio.
  • Soportar un análisis del negocio.
  • A los usuarios, hacer reingeniería o mejora de procesos de negocio.
  • Achicar la brecha entre el modelo real y el modelo del sistema.
Para esto, Leffingwell y Widrig se basan en conceptos que surgen del mundo de la Reingeniería de Procesos de Negocio y los combinan con el estándar UML para formar dos tipos de diagramas:
  • Modelo de Casos de Uso de Negocio: usado para identificar roles y entregables en la organización.
  • Modelo de Objetos de Negocio: usado para ver las entidades del sistema y cómo interactúan entre sí.
Estos modelos están compuestos por Casos de Uso del Negocio, Entidades del Negocio, Trabajadores (workers) del Negocio, Unidades Organizacionales, Responsabilidades y Relaciones. El mapeo de Casos de Uso del Negocio a Casos de Uso, de Objetos de Negocio a Clases o de Workers del Negocio a actores no es del todo trivial. Existe un paso intermedio de análisis en el que, por ejemplo, algún worker puede desaparecer y otro puede desdoblarse o automatizarse. Los trabajadores son los que manipulan entidades de negocio e interactúan con otros trabajadores; son los componentes candidatos de un sistema a ser automatizados.

La ingeniería de sistemas se usa como técnica para analizar problemas; nos ayuda a descomponer un sistema complejo en subsistemas. Mediante el Modelado del Negocio podemos tener una mejor comprensión de las aplicaciones de software que deben construirse y el propósito general al cual servirán.

Entradas Relacionadas

jueves, 5 de junio de 2008

Administrando Requerimientos de Software

Managing Software Requirements - A Unified Approach, es un libro bastante interesante de Dean Leffingwell y Don Widrig, publicado en Octubre de 1999 por Addison Wesley (ISBN: 0-201-61593-2). El libro presenta una metodología ordenada que puede servir como guía a la hora de identificar, analizar y especificar formalmente los requerimientos de un sistema de software y que ayuda en la administración de cambios de los mismos. Basándose en la terrible verdad de que descubrir un error en los requerimientos se hace cada vez más costoso a medida que avanzan las etapas del ciclo de vida de un proyecto, Managing Software Requirements se enfoca en 6 Habilidades Grupales (Team Skills) que abarcan desde analizar el problema del usuario y capturar sus necesidades correctamente, hasta la verificación y/o validación mediante pruebas de aceptación de diferente profundidad y cobertura, para corroborar que se siga construyendo el sistema correcto, el sistema que el usuario necesita, durante todo el proyecto, a pesar de los cambios y los nuevos requerimientos que pudieron haber surgido.

Ya que me ha parecido muy interesante la lectura de este libro, he decidido dedicar ocho post, incluyendo éste, dedicados al mismo, que se dividirán de la siguiente forma:


PARTE I - TEORÍA
(7 entradas)

  • Introducción (se trata de esta misma entrada)
  • Team Skill 1: Analizando el Problema
  • Team Skill 2: Entendiendo las Necesidades del Usuario
  • Team Skill 3: Definiendo el Sistema
  • Team Skill 4: Administrando el Alcance
  • Team Skill 5: Refinando la Definición del Sistema
  • Team Skill 6: Construyendo el Sistema Correcto

PARTE II - SU "RECETA" PARA LA ADMINISTRACIÓN DE REQUERIMIENTOS
(1 entrada)

  • Supuestos
  • La receta
Basándome más que nada en la estructura del capítulo 35: "Getting Started", dividiré en dos grandes partes esta serie de post. Toda la primera parte, que la componerán las primeras siete entradas, incluyendo ésta, hablarán sobre la teoría de las 6 Habilidades Grupales. Lo que sería un resumen modesto del libro. La segunda parte será una "receta" proporcionada por el libro que puede servir como checklist para la administración de requerimientos en su proyecto. El octavo y último post estará dedicado a ella.

De más está decir que recomiendo fuertemente la lectura de este libro, ya que no tiene desperdicio. Hay muchos temas interesantes que trata que yo sólo nombraré, o quizá pase por alto. Esos temas que no profundizaré no quiere decir que no sean importantes, ni interesantes; simplemente que no puedo, ni tiene sentido, transcribir todo el libro. Digamos que mi resumen servirá para "abrir el apetito".

Introducción

En los capítulos introductorios, Managing Software Requirements muestra que la industria del desarrollo de software hace un trabajo muy pobre a la hora de entregar aplicaciones de calidad cumpliendo los tiempos y los presupuestos asignados.


Algunas de las causas principales de este problema son:
  • Falta de participación del usuario.
  • Requerimientos y especificaciones incompletos.
  • Cambios en los requerimientos y en las especificaciones.
Los desarrolladores y clientes tienen una actitud común que es: "a pesar de que no estamos seguros de los detalles de qué es lo que se quiere, es mejor comenzar con la implementación ahora, porque contamos con un calendario ajustado. Podemos detallar los requerimientos más tarde". Pero muy a menudo, este enfoque bien-intencionado degenera en un esfuerzo caótico de desarrollo, donde nadie está seguro de qué es lo que el usuario realmente quiere o qué es lo que realmente hace el sistema.

¿Cómo sabemos qué se supone que tiene que hacer el sistema? ¿Cómo tenemos un registro del estado corriente de los requerimientos? ¿Cómo determinamos el impacto de un cambio? Para manejar estas cuestiones, Dean Leffingwell y Don Widrig proponen una filosofía que abarca la administración de requerimientos, la cual han definido como:

Un enfoque sistemático para extraer, organizar y documentar los requerimientos de un sistema. Un proceso que establece y mantiene el acuerdo entre el cliente y el grupo del proyecto respecto a los cambios a lo largo del ciclo de vida del proyecto.

La historia del desarrollo de software ha demostrado que la construcción de sistemas es una disciplina compleja, que debe ser manejada por equipos bien estructurados y entrenados. Cada miembro del equipo estará eventualmente involucrado en ayudar a gestionar los requerimientos para el proyecto. Estos equipos deben desarrollar las aptitudes necesarias para comprender las necesidades de los usuarios, para gestionar el alcance de la aplicación y para construir sistemas que satisfacen las necesidades del usuario. El equipo debe trabajar como un equipo para hacer frente a los desafíos de la administración de requerimientos.

Algunas definiciones

Necesidades del usuario: son reflejos de un problema u oportunidad relacionada con aspectos del negocio, del personal u operativos que deben ser considerados para justificar el desarrollo, modificación, compra o uso de un nuevo sistema. Las necesidades son parte del Dominio del Problema.

Características, funcionalidad o features del sistema: son servicios que el sistema debe proveer para cubrir una o más necesidades de los participantes o stakeholders. Las funcionalidades son parte del Dominio de la Solución.


Requerimientos del software: un requerimiento es una condición o capacidad necesitada por el usuario para resolver un problema o alcanzar un objetivo. Es una condición o capacidad que debe poseer un sistema para satisfacer un contrato, estándar, especificación u otro documento formalmente impuesto. Los requerimientos son parte del Dominio de la Solución.

Problema: La diferencia entre cosas como son percibidas y cosas como son deseadas.