Práctica: Persistencia con Prowl/Zero

    Zero es una máquina virtual orientada a objetos y basada en prototipos. La orientación a objetos ofrecida en Zero no es como la de C++, sino la de otros lenguajes como Self. Se crean directamente objetos (prototipos), los cuales se copian, para crear las objetos-instancia que realmente se emplearán. El lenguaje de programación, así como otros muchos lenguajes basados en prototipos, es débilmente tipado. Esto quiere decir que no se hacen comprobaciones de tipos, como en C++ o Java. Una última capacidad de la máquina virtual es que ofrece persistencia, esto es, no es necesario dedicar esfuerzo a indicar cómo guardar la información almacenada en los objetos, sino que éstos pueden guardarse y recuperarse automáticamente.

Bibliografía y links de la práctica

    Los programas para Prowl, para macroensamblador y la propia máquina virtual Zero, necesarios para compilar y ejecutar los objetos creados, más la documentación. Hay binarios para Linux y para Windows.

Conceptos necesarios para entender la práctica

Texto de la práctica

      La máquina virtual Zero es persistente por defecto. Los programas se componen de objetos, y estos se ejecutan después de ser colocados en diferentes contenedores. Así, en un programa como el que sigue a continuación, existen al menos tres contenedores: el contenedor raíz (psRoot), el contenedor donde se ejecutan los programas (Exe), y el contenedor gonde se guarda la librería estándar (IntStdLib).

    // Programa Hola, Mundo
     object HolaMundo : ConsoleApplication
        method + doIt()
        {
           System.console.write( "¡Hola, Mundo!" );
           System.console.lf();
           return;
        }
    endObject

    De todos los contenedores anteriores, el contenedor Exe es el único que no es persistente. Esto quiere decir que los objetos situados en él no van a ser guardados para posteriores ejecuciones, por lo que cualquier programa puede ser ejecutado sin temer llenar el almacenamiento persistentes de objetos sin utilidad.

    Es posible ejecutar la máquina virtual Zero sin soporte para persistencia. Esto se hace con la opción nops.
    $ zvm -nops HolaMundo
    De esta manera todos los contenedores son no persistentes, por lo que, incluso si un programa emplea objetos persistentes, se encontrará sin ellos, y si los guarda, no se podrán encontrar para la siguiente ejecución.

    Así, si se quiere que un objeto sea guardado para posteriores ejecuciones (es decir, hacerlo persistente), es necesario guardarlo en un contenedor que ofrezca persistencia. Sería posible guardarlo en el mismo psRoot, pero es recomendable crear subcontenedores de manera que se pueda organizar el espacio en lugar de llenar la raíz persistente de objetos sin relación entre sí.

El punto de la persistencia

Recuperemos el objeto Punto, que ya hizo su primera aparición en la introducción a Zero.

object Punto
    attribute + x = 0;
    attribute + y = 0;
   
    method + set(a, b)
    {
        x = a;
        y = b;
       
        return;
    }
   
    method + toString()
    {
        reference toret = x.toString();
       
        toret = toret.concat( ", " );
        toret = toret.concat( y.toString() );
       
        return toret;
    }
   
endObject

Es posible escribir dos programas con este objeto: uno que lo guarde, y otro que lo recupere. Al fin y al cabo, es de lo que se trata, de que el punto persista. Para guardar el objeto, debemos crear, por lo explicado antes, un contenedor:

object GuardaPunto: ConsoleApplication
    method + doIt()
    {
        reference p = Punto.createChild( "p" );
        reference c = ContainerInstance.copy( "PruebaPunto" );
       
        // Rellenar el punto
        p.set( 100, 150 );
       
        // Hacerlo persistente
        c.add( p );
        psRoot.add( c );
       
        return;
    }
endObject

Así, se crea un contenedor llamado PruebaPunto, donde se guardará una instancia del punto llamada p. Como la instancia tiene un atributo parent que señala a Punto, este también será guardado. Otro programa será necesario para poder recuperarlo.

object RecuperaPunto: ConsoleApplication
    method + doIt()
    {
        System.console.writeLn( psRoot.PruebaPunto.p );
       
        return;
    }
endObject

El acceso al almacenamiento persistente es simple: cada contenedor se accede desde el contenedor en donde reside, ya que al fin y al cabo, sólo son objetos, unos dentro de otros. En el caso de que el contenedor no se encuentre, se lanza una excepción EObjNotFound. Esto se puede probar utilizando la opción nops de la máquina virtual:

$ zvm --nops RecuperaPunto

Uncaught exception:

RecuperaPunto threw EObjectNotFound: in
RecuperaPunto.<a_method>(): Object not found:
'psRoot.PruebaPunto.p' 
TopLevel executing TopLevel.doIt() RecuperaPunto executing RecuperaPunto.doIt() SystemStack executing SystemStack.toString()
__Zero_Stack__ executing __Zero_Stack__.toString()

Es posible aprovechar esta circunstancia para conjuntar ambos programas en uno solo. En el caso de que se lance la excepción, el objeto es creado. En otro caso, se accede a él:


// Recupera el punto

object Punto
    attribute + x = 0;
    attribute + y = 0;
   
    method + set(a, b)
    {
        x = a;
        y = b;
       
        return;
    }
   
    method + toString()
    {
        reference toret = x.toString();
       
        toret = toret.concat( ", " );
        toret = toret.concat( y.toString() );
       
        return toret;
    }
   
endObject

object RecuperaPruebaPersistenciaPunto: ConsoleApplication
    method + crea()
    {
        reference p = Punto.createChild( "p" );
        reference c = ContainerInstance.copy( "PruebaPunto" );
       
        // Rellenar el punto
        p.set( 100, 150 );
       
        // Hacerlo persistente
        c.add( p );
        psRoot.add( c );
       
        return;
    }
   
    method + recupera()
    {
        return psRoot.PruebaPunto.p;
       
        onException( e ) {
            System.console.writeLn( "Creando punto..." );
            this.crea();
            return psRoot.PruebaPunto.p;
        }
    }
   
    method + doIt()
    {
        System.console.writeLn( this.recupera() );
        return;
    }
   
endObject

La familia Disney

    Un ejemplo podría ser el siguiente programa en PROWL sobre la familia Disney. Tras crear varios prototipos como Persona y Pareja, se crean varios objetos descendientes de estos, como se ve a continuación:

                reference donaldYdaisy = Pareja.copy( "" );
                reference donald       = Persona.copy( "" );
                reference daisy        = Persona.copy( "" );
                reference jorgito      = Persona.copy( "" );
                reference juanito      = Persona.copy( "" );
                reference jaimito      = Persona.copy( "" );
 
                donald.ponNombre( "Donald" );
                donald.ponEmail( "donald@disney.com" );
                daisy.ponNombre( "Daisy" );
                daisy.ponEmail( "daisy@disney.com" );
 
                jorgito.ponNombre( "Jorgito" );
                jaimito.ponNombre( "Jaimito" );
                juanito.ponNombre( "Juanito" );
 
                donaldYdaisy.ponPareja( donald, daisy );
                donaldYdaisy.ponDependiente( jorgito );
                donaldYdaisy.ponDependiente( jaimito );
                donaldYdaisy.ponDependiente( juanito);
 
                mascotas.add( "Pluto" );
                mascotas.add( "Goofy" );
                donaldYdaisy.ponDependiente( mascotas );

    Esta es una estructura relativamente compleja. Una vez que los objetos se han creado, sólo es necesario crear un contenedor, e introducir en él el objeto que relaciona a todos los demás (donaldYdaisy). Una vez que la ejecución termina, todos los objetos que son alcanzables desde el almacenamiento persistente son también hechos persistentes.
                 reference disney = ContainerInstance.copy( "Disney" );
                 disney.addRenamedObject( "donaldYdaisy", donaldYdaisy );
                 psRoot.add( disney );

    La gran diferencia con otros sistemas de persistencia no ortogonal (ampliamente basados en serialización), es que no es necesario tener en el programa de recuperación los objetos Persona o Pareja, puesto que, al ser necesarios (cosa que es denunciada por el cierre persistente de cada objeto, al estar relacionados por herencia), son automáticamente guardados. Así, el programa en PROWL de recuperación de los datos de la familia Disney es tan simple como sigue:
		  object RecuperaFamiliaDisney: ConsoleApplication
			method + doIt()
		        {
		            this.prepare();
		            System.console.write( "Recuperando info persistente:\n" );
		            System.console.lf();
		            System.console.write( psRoot.Disney.donaldYdaisy );
		            System.console.lf();
		            return;
		        }
		  endObject

    Un ejemplo de ejecución se puede ver a continuación:
$ ./zvm GuardaFamiliaDisney
Preparado para guardar...
Pareja formada por: Donald: donald@disney.com, y Daisy: daisy@disney.com
Dependientes: Jaimito, Juanito, Pluto, Goofy.
Guardado de manera persistente
$ ./zvm RecuperaFamiliaDisney

Recuperando info persistente:

Pareja formada por: Donald: donald@disney.com, y Daisy: daisy@disney.com
Dependientes: Jaimito, Juanito, Pluto, Goofy.
    Tal y como se comentaba anteriormente, la máquina virtual Zero puede trabajar, de manera transparente, sin almacenamiento persistente. De esta manera, todos los contenedores son no persistentes, y ningun objeto es guardado. Así:
$ ./zvm --nops RecuperaFamiliaDisney
Recuperando info persistente: 
Uncaught exception:

RecuperaFamiliaDisney threw EObjectNotFound: in
RecuperaFamiliaDisney.<a_method>(): Object not found:
'psRoot.Disney.donaldYdaisy' 
TopLevel executing TopLevel.doIt() RecuperaFamiliaDisney executing RecuperaFamiliaDisney.doIt() SystemStack executing SystemStack.toString()
__Zero_Stack__ executing __Zero_Stack__.toString()
Así, si se elimina el almacenamiento persistente (basta borrar por completo el subdirectorio ZeroPS):
$ ./zvm --nops GuardaFamiliaDisney 
Preparado para guardar... Pareja formada por: Donald: donald@disney.com, y Daisy: daisy@disney.com Dependientes: Jaimito, Juanito, Pluto, Goofy. 
Guardando... Guardado de manera persistente
$ ./zvm  RecuperaFamiliaPersistente
Recuperando info persistente:
Uncaught exception:
RecuperaFamiliaDisney threw EObjectNotFound: in
RecuperaFamiliaDisney.<a_method>(): Object not found:
'psRoot.Disney.donaldYdaisy'
TopLevel executing TopLevel.doIt()
RecuperaFamiliaDisney executing RecuperaFamiliaDisney.doIt()
SystemStack executing SystemStack.toString()
__Zero_Stack__ executing __Zero_Stack__.toString()

  
    El almacenamiento persistente se guarda de manera jerárquica, representando los contenedores dentro de contenedores (por ejemplo, Disney dentro de Root) aprovechando los directorios de espacio de archivos:

$ cd ZeroPS
$ ls
Root.zbj
$ cd Root/
$ ls
Disney.zbj  IntStdLib.zbj

Un ejemplo más práctico

    A pesar de todo, lo anterior no demuestra cómo emplear persistencia en una aplicación más realista. En la aplicación de agenda electrónica, nombres, teléfonos y edades se guardan en una estructura tipo Map, de tal manera que se puedan acceder tanto secuencialmente como buscar por nombre.
object TraitsPersona
    attribute + nombre   = "Pepito perez";
    attribute + edad     = 50;
    attribute + telefono = "988310000";

    method + putNombre(n)
    {
        nombre = n;
        return;
    }

    method + putEdad(ed)
    {
        if ( ed isInstanceOf Int ) {
            edad = ed;
        } else {
            throw ETypeMismatch, "edad debe ser un entero";
        }

        return;
    }

    method + putTelefono(tlf)
    {
        telefono = tlf;
        return;
    }

    method + toString()
    {
        reference toret = nombre;

        toret = toret.concat( ": edad " );
        toret = toret.concat( edad.toString() );
        toret = toret.concat( ", tlf: " );
        toret = toret.concat( telefono );

        return toret;
    }
endObject

object
Persona: TraitsPersona
endObject
    Así, las instancias Persona serán objetos que deriven de este prototipo, TraitsPersona. Estas instancias, como ya se comentaba, se guardarán en un objeto derivado de Map.
object Agenda: Map
endObject
Para crear una nueva persona, será necesario copiar el prototipo Persona, rellenar los campos, y guardarlos en la estructura de la agenda:
        object AplicacionAgenda
        //...
            method + masPersonas()
            {
                // ...
                toret = Persona.copy( "" );
                toret.putNombre( nombre );
                toret.putEdad( edad );
                toret.putTelefono( telefono );
                agenda.add( nombre, toret );
                // ...
            }
        endObject

    Sólo un método "extra", en el objeto principal, es necesario para tomar ventaja de la posibilidad de utilizar el almacenamiento persistente, y que de esta forma la agenda pueda "recordar" las personas guardadas en ella.
	
object AplicacionAgenda
// ...
method + hazPersistente()
    /*
        	Solamente es necesario crear el contenedor la primera vez
	    */
	{
        	agenda = psRoot.InfoAplicacionAgenda.Agenda;
        System.console.write( "\nArrancando del container ...\n" );
	        return;

        onException( e ) {
	            System.console.write( "\nArrancando sin datos ...\n" );

            // Crearlo: no fue encontrado
	            reference cnt =  Container.createChild( "InfoAplicacionAgenda" );

            cnt.add( agenda );
            cnt.add( TraitsPersona );
            cnt.add( Persona );
	            psRoot.add( cnt );
        }
// ...
endObject      
    

    Así, el objeto principal llama a este método al comienzo de la aplicación. Se hace que la referencia agenda, en lugar de apuntar a éste, apunte a psRoot.InfoAplicacionAgenda.Agenda. Si está disponible, este es el objeto que se emplea (el cual probablemente, ya contiene datos). En otro caso, es necesario crearlo, de manera que se enlace la agenda ya presente con el nuevo contenedor (con lo que, al final de la ejecución, al ser alcanzable desde psRoot, se guardará como persistente).

Ejercicios

1. Prueba los programas, comprueba que funcionan, y aclara las dudas que tengas con la documentación para Zero.
2. Convierte el programa de la familia Disney, de manera que se guarde y recupere desde el mismo.
3. Crea tu propia aplicación persistente: una empresa guarda información sobre sus Clientes, y permite crear nuevos clientes y listarlos. Los Pedidos indican el número de unidades a pedir, el código de la pieza, el Cliente al que se le piden, y la fecha del pedido. Crea una aplicación en Prowl que permita: crear un nuevo cliente, listar los clientes, crear un nuevo pedido,  y listar todos los pedidos. La información debe persistir desde una ejecución a la siguiente. Recuerda no pensar en términos de claves fuertes, débiles o foráneas: el sistema de persistencia guarda todos los objetos y las relaciones entre ellos.