jueves, 29 de agosto de 2013

Solución de Software utilizando UML y OOP basada en una extensión en Yii Framework

Nivel 1

El Caso

Mi hermano tiene un CallCenter, donde se hace una llamada a cada cliente de una lista, la cual llamamos campaña, cada lista es una campaña),  para hacer una cobranza en linea a nombre de un "Cliente".


El Problema

Me indican que "El cliente" enviará un "listado de morosos" en formato CSV, es decir un archivo de texto, y, a veces si esta disponible usarmos una conexión directa a una fuente de datos via VPN, usaremos el VPN o el CSV, no ambos a la vez, esto debemos configurarlo en algun lado.

Sin importar cual sea la fuente el sistema debe ofrecer al operador una caracteristica común: Consultar datos de un Cliente por su cédula, Consultar sus Cuotas por pagar, Reportar un Pago

Los Sustantivos y las Acciones

En el texto anterior encontramos varios sustantivos que he coloreado en Rojo , también hay varias acciones a tomar por parte del operador, las cuales he coloreado en Azul.

Las acciones son Casos de Uso,  Los sustantivos se convertirán en Clases. (si..he marcado en rojo algunas palabras pero también algunas frases que no son sustantivos pero que representan a "algo" del sistema).

De aquí porqué es tan importante escribir el problema en un papel, para determinar objetos, instancias, listas, acciones y cualquier cosa "que el operador va a usar en el sistema".

El futuro

Me refiero a "El Futuro" con el hecho de pensar en qué pasaría mas adelante en el sistema, dadas las circunstancias presentadas aqui, desde el punto de vista de programación.

1. El Cliente puede tener requisitos especiales de conectividad, quiza la conexión hoy sea posible mediante un objeto "Yii::app()->db", o quizá mañana sea necesario conectarse a un Webservice que el cliente nos de.
por tanto, no podemos limitar la programación a "matarlo todo con un CActiveRecord". Hay que modelar una solución abierta capaz de "amoldarse al cliente" (y no al contrario como muchos piensan, tu no eres Microsoft)

2. El formato del CSV puede variar dependiendo de "El Cliente" o "La Campaña", quizá hoy traiga algunos campos, mañana otros, así que no tendremos un método que lea "UN;TEXT;FIJO" sino que en cambio podamos leer distintos datos dependiendo de la campaña y el cliente, para los cuales haremos un componente que hace algo específico, haciendo en ese componente una implementación específica de  "impotarCSV", si en el futuro, otra campaña tiene otros datos, haremos otro componente con otras reglas de negocio, por ende otra implementación del método: "importarCSV"

Podriamos llamar a esto "Las Reglas del Negocio", aunque no son todas, aqui pongo las que importan para el modelado. "Vamos por partes" (como dijo jack el destripador)

El Diseño Preliminar sin Detalles


No hay una campaña a cada rato sino en cambio una campaña se repite con nuevos archivos durante meses, por tanto usamos el mismo componente ya que serán las mismas reglas de negocio.  Ya esto es parte de la solución específica de "éste software".

A la aplicación web no le compete saber de dónde vino la data, a ella solo le importa que:

"el componente responda con datos cuando invocamos a los métodos que éste implementa"

La Interfaz

Aquí es donde entra "la temida Interfaz", ya que ésta asegurará que "ese componente" "se puede usar" para satisfacer la necesidad de la aplicacion web quien actuará como "consumidor de la interfaz", mientras que "DataSourceCampanaX" es un "realizador", porque "realiza la interfaz", le da vida a esos métodos que la aplicacion requiere.

La interfaz virtualiza los requerimientos de un consumidor, los hace presentes, aunque no operativos, en otras palabras, el consumidor (la aplicacion web en este caso) confia en que si una clase cualquier implementa la interfaz "IDataSourceCampana" entonces dispone de los métodos indicados por la interfaz y por tanto puede usarla para gestionar esa campaña.

Porqué la interfaz se debe llamar "IDataSourceCampana" ?

Respondo con una pregunta.  Qué pasaria si en un desarrollo de software todos los coders involucrados nombrasen las clases a su mejor antojo ? Tendriamos un nombre como "Dscam", el desastre. Entonces, como somos coders ordenados y decentes, le atecedemos una "I" a los archivos que representan Interfaces, y las almacenamos en un directorio llamado "interfaces", por ser ordenados, además de llamar a la interfaz con un nombre completo y coherente. No hay normas técnicas que obligen cómo dar el nombre. Ojalá lo hubiese.

Mas abajo definiré el cuerpo de la interfaz, una vez que conozcamos los casos de uso, que serán los que dicten que métodos debe tener.

Los Casos de Uso

Los métodos que la interfaz expone dependen del análisis de los "Los Casos de Uso" (el texto en azul al inicio de este documento), aunque no es una norma estática que esto sea así, en este caso dependerá de "los casos de uso" que queremos darle:

Los Casos de Uso (lo que queremos que la aplicación web nos permita cumplir)

  1. buscar un cliente por su cédula y leer sus datos.
  2. listar los numeros de contrato de un cliente,
  3. listar las cuotas en deuda de un contrato.

Por tanto la interfaz debería ser:

Yii::app()->dsx es una instancia de DataSourceCampanaX mediante un componente YII, pero bien puede ser tambien:  $dsx = new DataSourceCampanaX();

Yii::app()->dsx->listarClientesDataProvider();  // entrega un DataProvider para usar en un CGridView
Yii::app()->dsx->listarContratos($cedula); // entrega un array con numeros de contrato
Yii::app()->dsx->listarCuotas($numero_contrato); // entrega un array de objetos que definen una cuota.

Cómo hacemos para que CUALQUIER componente que a futuro hagamos siempre tenga estos métodos, de modo que a la aplicación web no le importe nada NUNCA, sino que en cambio solo consuma lo que el componente le de ? Respuesta:  haciendo una interfaz.

// IDataSourceCanpana.php
public function IDataSourceCampana {
     public function listarClientesDataProvider();
     public function listarContratos($cedula);
     public function listarCuotas($numero_contrato);
     public function importarCsv($filename);
}


Nota el nuevo metodo: "importarCsv", quiza no sea usado por "la aplicacion WEB" (es decir el triste operador) sino en cambio por un operador avanzado que se mete por consola y hace:
" ./yiic callcenter importcsv --filename=/mi_archivo2013-08-28.csv"
donde el comando "importcsv" esta programado en una aplicación de consola que hará:

// callcenterConsoleCommand.php
public function actionImportCsv($filename){
      $micomp = new DataSourceCampanaX();
      $r = $micomp->importarCsv($filename);
      printf("archivo importado. resultado fue: %s\n", $r);
}

Este método "importarCsv" moverá datos de un archivo CSV a una "tabla" análoga en la base de datos local, para no hacer busquedas secuenciales lentas sobre registros en un archivo de texto, en cambio, al pasarlas a la base de datos se limpiará, y será mas veloz. Solo es un método análogo, conceptualmente equivale a **"leer del csv" (mediante una tabla)**.

Te habrás dado cuenta que usando esa consola podriamos hacer pruebas de todo el componente "a punta de comando".

Cómo queda el gráfico al agregar la interfaz




Con este gráfico sabemos que "un componente" nos dará la funcionalidad, es decir, en cualquier parte de la aplicación web o aplicación de cónsola pudiésemos escribir:

    // dsx es un componente instalado en config main que hace instancia 
    // de DataSourceCampanaX
    //
    foreach(Yii::app()->dsx->listarContratos("cedulaxx") as $numero_contrato) 
        foreach(Yii::app()->dsx->listarCuotas($numero_contrato) as $cuota){
            list($cuota, $num_contr, $valor_sin_iva, 
                 $valor_con_iva, $fecha) = $cuota;
            printf("MOROSO EN CUOTA: %s, MONTO: %s, FECHA: %s\n", 
                 $cuota, $valor_con_iva, $fecha);
        }

El Datasource

El "datasource" será para la aplicación web o la aplicación de consola cualquier cosa que le provea datos a esos métodos impuestos por la interfaz. El "datasource" puede ser un archivo CSV (que por optimización hemos vaciado a una tabla análoga en el modelo de datos relacional) o podria ser la conexión a datos que el cliente nos ha dado via VPN (en ese caso).

Qué vamos a leer de ese datasource ? vamos a leer una Identidad:

    "..datos de una persona que debe algo en algun documento(s) de pago.."

Estos datos tienen varios campos en un solo renglón, esto es así porque YO le pedí al cliente un formato específico.

    "cedula; nombres; direccion; telefono; cuotas;"

El archivo CSV va a servir cuando la conexión a base de datos por via VPN no aplique o no sea posible hacerla, en tal caso el cliente envia un CSV, este se importará a una tabla analoga y se usará como fuente de datos. A la aplicación web esto no debe importarle en lo absoluto.

Resumen hasta aquí.

Hasta aqui tenemos un sistema conceptual, fundamentado en un componente, el cual implementa una interfaz, lo que hace que cumpla ciertos requisitos de la aplicación cliente (la aplicacion web o la consola de pruebas).


Nivel 2

En este nivel mas avanzado ya sabemos porque hay una interfaz, sabemos que es DataSourceCampanaX, en fin conocemos "el modelado". Ahora, vamos a cubrir algunos puntos específicos.


Importando un CSV para usarlo como datasource

Supongamos que estamos trabajando en la modalidad de usar un CSV como fuente de datos para la aplicación web, no vamos a leer datos desde el CSV porque esto implica lectura secuencial sobre un archivo de texto y ahi el mundo se nos pone lento.

Lo que haremos será hacer "un análogo":  copiar cada registro del CSV en una "tabla" llamada "csvcampanax", porque se llama con el mismo nombre del componente ? por orden, y recordando que este componente sirve de instancia para un propósito específico de un caso de negocio de un cliente que siempre nos manda el mismo tipo de archivo.  Lee la primera parte de este documento para conocer las razones.

El siguiente gráfico UML nos indica que en "una aplicación de consola" se ejecutará el comando "importcsv", el cual pide una ruta local de archivo, del cual se sacaran los datos para insertarlos en una table.  Quién escribe en la "tabla" ? Solo el componente, nadie mas. (flecha WRITE), quien lee el archivo CSV ? solo el componente (flecha READ).  Esto nos ayuda a comprender mas a futuro que SOLO EL COMPONENTE es quien tiene dependencias sobre este archivo CSV, no tendremos dependencias "espaguetti" por doquier.



El siguiente gráfico ahora muestra el uso del método "clientesDataProvider()" el cual devuelve un DataProvider que podriamos usar en un CGridView. Si os fijáis hay dos flechas READ que salen del componente.  La primera flecha READ apunta a "vpn connection" y la segunda apunta a "sql table".

Como dije antes, la aplicación web no le importa "de donde" vino los datos, le importan "los datos". También verás una flecha punteada etiquetada como "READ METHOD", esto significa "el método en como leeremos dependerá de un atributo en config/main para este componente llamado -datasource-, el cual si es vpn entonces leeremos de la vpn, sino leeremos de la tabla sql."



Los demás métodos de la interfaz se comportarán igual, solo que en vez de listar clientes, leeremos contratos de un cliente, cuotas de un cliente etc, todo dependiendo de cómo configuramos al componente.

Y porqué hay que "configurar" al componente para que sepa de donde leer ?

Porque un dia el cliente nos dará un CSV, otro día nos dara la VPN, dependerá de lo que mejor funcione bajo las circunstancias "del negocio".

Ahora me comprenden mejor cuando siempre digo en el blog:

"el software se hace para cumplir exigencias del cliente, no del programa, por tanto se diseña en base a objetos de negocio, no en base a diagramas datos del workbench sql"

El error del diseño de software basado en modelado de datos (diagrama de tablas).

Mucha gente hace el software pensando en cómo el workbench sql se las va a llevar, pensando en la normalización. Has visto aquí algun diagrama SQL ? no. y no lo verás, es algo terciario e insignificante para el modelado, el "modelo de datos" (el diagrama de tablas) es la proyección de éste proceso de modelado, es a dónde vamos a guardar las cosas que aquí tenemos, y no al reves: "..como usar las cosas que tenemos guardadas..".


TDD

El famoso TDD entra acá.  No sin antes aclarar que haremos una versión simple de TDD basada en probar mediante cónsola a este componente que hemos creado aqui, por tanto cada componente expone un metodo llamado "test()" el cual será llamado mediante una implementación de TDD, cuando el metodo test() sea invocado haremos las pruebas pertinentes. No quisiera extenderme a este punto porque saco totalmente de foco al documento, tdd es extenso y requiere un libro especializado, solo quiero apuntar a que este componente puede ser probado en un banco de pruebas tdd con facilidad cuando los metodos de la clase son claros y precisos.


El diseño del componente y el despliegue (Deployment)

Vamos a programar quirurgicamente, podriamos estimar el tiempo de desarrollo cuando estamos en este punto tras haber analizado lo que hay que hacer:

1. crearemos un componente, derivado de CApplicationComponent, para que se pueda usar de este modo: Yii::app()->dsx->listarClientesDataProvider(),  o de este modo:  $dsx = new DataSourceCampanaX();

2. este componente será desplegado (deployment) en forma de "extensión yii", de este modo quedará aislado de la aplicación web, pudiendo reutilizarse en otro proyecto.



En el diagrama se muestra que "la interfaz" ha sido guardada dentro de la aplicación web y no dentro
del directorio de la extensión que vamos a crear, esto es debido a que la aplicación web cubrirá varias campañas para diversos clientes y muy probabemente se requiera atender las necesidades muy específicas de un cliente-campaña mediante un nuevo componente que también deberá implementar la interfaz que la aplicación web necesita, por tanto la interfaz esta ubicada en un sitio que todos los componentes puedan ver.


Nivel 3

Programando con precisión quirúrgica.

1. he creado un nuevo repositorio en bitbucket, en el cual se alojará la nueva extensión.

2. la codificación tardó dos horas, incluidas las pruebas TDD.

Tras un par de ajustes el código ha quedado asi:


Archivo:  protected/interface/IDataSourceCampana.php

  1. <?php
  2. interface IDataSourceCampana {
  3.         public function leerCliente($cedula);
  4.         public function listarContratos($cedula);
  5.         public function listarCuotas($cedula, $numero_contrato);
  6.         public function importarCsv($filename);
  7. }


Archivo:  protected/extensions/datasourcecampanametro/DataSourceCampanaMetro.php

  1. <?php
  2. /***
  3.         install as a component in: protected/config/main.php
             'components'=>array(
  4.                 'dsx'=>array(
  5.                         'class'=>'ext.datasourcecampanametro.DataSourceCampanaMetro',
  6.                         'datasource'=>'vpn',
  7.                         'vpn_view_name'=>'nombre_de_la_vista_que_el_cliente_da',
  8.                         'vpn_db'=>array(
  9.                                 'connectionString' => 'mysql:host=localhost;dbname=vpndbname',
  10.                                 'emulatePrepare' => true,
  11.                                 'username' => 'bla',
  12.                                 'password' => 'xxblaxx',
  13.                                 'charset' => 'utf8',
  14.                         ),
  15.                 ),
  16.         ),
  17. * @author Christian Salazar H. <christiansalazarh@gmail.com>
  18. */
  19. class DataSourceCampanaMetro
  20.         extends CApplicationComponent
  21.         implements IDataSourceCampana {
  22.        
  23.         public $datasource; // "vpn" or "csv"
  24.         public $vpn_db; // db settings. required when datasource is "vpn"
  25.         public $vpn_view_name;  // when vpn, the client must supply it
  26.         private $_db;
  27.         private $_last_key;
  28.         private $_last_record;
  29.                
  30.         public function leerCliente($cedula){
  31.                 if($this->_last_key == $cedula)
  32.                         return $this->_last_record;//LAZY
  33.                 if($this->_last_record = $this->getDb()->select()->from($this->_Tablename())   
  34.                         ->where("cedula=:c",array(":c"=>$cedula))->queryRow()){
  35.                         return $this->_last_record;
  36.                 }else{
  37.                         $this->_last_record = null;
  38.                         return null;
  39.                 }
  40.         }
  41.         public function listarContratos($cedula){
  42.                 if($row = $this->leerCliente($cedula)){
  43.                         $contracts = array();
  44.                         foreach($this->decodeQuotes($row['cuotas']) as $quote){
  45.                                 list($qn, $contract, $val1, $val2, $date) = $quote;
  46.                                 if(!in_array($contract, $contracts))
  47.                                         $contracts[] = $contract;
  48.                         }
  49.                         return $contracts;
  50.                 }else
  51.                 return array();
  52.         }
  53.         public function listarCuotas($cedula, $numero_contrato){
  54.                 if($row = $this->leerCliente($cedula)){
  55.                         $quotes = array();
  56.                         foreach($this->decodeQuotes($row['cuotas']) as $quote){
  57.                                 list($qn, $contract, $val1, $val2, $date) = $quote;
  58.                                 if($contract == $numero_contrato)
  59.                                         $quotes[] = $quote;
  60.                         }
  61.                         return $quotes;
  62.                 }else
  63.                 return array();
  64.         }
  65.         public function importarCsv($filename){
  66.                 if(!is_file($filename))
  67.                         return "ERR_NOT_A_FILE";
  68.                 // force db connection to be "CSV" when importing data
  69.                 $this->_db = null;
  70.                 $this->datasource = "csv";
  71.                 $this->getDb()->createCommand()->delete($this->_Tablename());
  72.                 $f = fopen($filename,"r");
  73.                 while($data = fgetcsv($f, 0, ",")){
  74.                         list($ced, $nom, $dir, $tel, $fec, $cuotas)=$data;
  75.                         $this->getDb()->createCommand()->insert($this->_Tablename(),
  76.                                 array(
  77.                                         "cedula"=>$ced,
  78.                                         "nombres"=>$nom,
  79.                                         "direccion"=>$dir,
  80.                                         "telefono"=>$tel,
  81.                                         "fecha_act"=>$fec,
  82.                                         "cuotas"=>$cuotas
  83.                                 ));
  84.                 }
  85.                 fclose($f);
  86.                 return "OK";
  87.         }
  88.         private function getDb(){
  89.                 if($this->_db == null){
  90.                         if($this->datasource == "csv"){
  91.                                 $this->_db = Yii::app()->db;
  92.                         }else{
  93.                                 // build a db connection.
  94.                                 $this->_db = new CDbConnection(
  95.                                      $this->vpn_db['connectionString'],
  96.                                      $this->vpn_db['username'],
                                         $this->vpn_db['password']);
  97.                                      if(isset($this->vpn_db['charset']))
  98.                                         $this->_db->charset = $this->vpn_db['charset'];
  99.                                      if(isset($this->vpn_db['emulatePrepare']))
  100.                                         $this->_db->emulatePrepare = 
                                                  $this->vpn_db['emulatePrepare'];
  101.                                 $this->_db->active = true;
  102.                         }
  103.                 }
  104.                 if($this->_db == null)
  105.                         throw new Exception("invalid database connection");
  106.                 return $this->_db;
  107.         }
  108.         private function _Tablename(){
  109.                 if($this->datasource=="csv")
  110.                         return "dscampanametro";
  111.                 if($this->datasource=="vpn")
  112.                         return $this->vpn_view_name;
  113.                 return "";
  114.         }
  115.         private function decodeQuotes($quotes){
  116.                 $out=array();
  117.                 foreach(explode(";",$quotes) as $quote){
  118.                         if($quote != ""){
  119.                         list($qn,$items) = explode("=",$quote);
  120.                         list($contract, $val1, $val2, $date) = explode(",",$items);
  121.                         $dt=explode("/",$date);
  122.                         $out[] = array($qn,$contract, $val1, $val2,
  123.                                 date("Y-m-d",strtotime(
                                         sprintf("%s-%s-%s",$dt[2],$dt[1],$dt[0]))));
  124.                         }
  125.                 }
  126.                 return $out;
  127.         }
  128.         private function _sampleRecord(){
  129.                 $quotes =
  130.                          "Q1=021546,75.12,84.1344,26/12/2004;"
  131.                         ."Q2=021546,88.17,98.7504,26/12/2005;"
  132.                         ."Q3=021546,102,114.24,26/12/2006;"
  133.                         ."Q4=021546,117,131.04,26/12/2007;"
  134.                         ."Q5=021546,143.13,160.306,26/12/2008;"
  135.                         ."Q6=021546,189.9,212.688,26/12/2009;"
  136.                         ."Q1=021547,241.08,270.01,26/12/2010;"
  137.                         ."Q2=021547,308.04,345.005,26/12/2011;"
  138.                         ."Q1=021548,388.41,435.019,26/12/2012;"
  139.                         ."Q2=021548,575.88,644.986,26/12/2013;";
  140.                 return array(
  141.                         "cedula"=>'TEST',
  142.                         "nombres"=>'PORCUPINE TREE',
  143.                         "direccion"=>'ARRIVING SOMEWHERE, BUT NOT HERE',
  144.                         "telefono"=>'555-9-123456;555-8-876543',
  145.                         "fecha_act"=>'2013-08-20',
  146.                         "cuotas"=>$quotes
  147.                 );
  148.         }
  149.         public function test($vpn_db){
  150.                 // called by consolecommand to test features
  151.                 $this->_db = null;
  152.                 $this->datasource = "csv";
  153.                 printf("tableName is: %s\n",$this->_Tablename());
  154.                 printf("getDb call when datasource is: %s...",$this->datasource);
  155.                 $this->getDb();
  156.                 printf("OK\n");
  157.                 $this->_db = null;
  158.                 $this->vpn_db = $vpn_db;
  159.                 $this->datasource = "vpn";
  160.                 printf("viewName is: %s\n",$this->_Tablename());
  161.                 printf("getDb call when datasource is: %s...",$this->datasource);
  162.                 $this->getDb();
  163.                 printf("OK\n");
  164.                 $this->_last_key = 'TEST';
  165.                 $this->_last_record = $this->_sampleRecord();
  166.                 $quotes = $this->_last_record['cuotas'];
  167.                 printf("testing quote object decode:\n%s\ninto:\n",$quotes);
  168.                 foreach($this->decodeQuotes($quotes) as $quote=>$fields){
  169.                         list($contract, $val1, $val2, $date)=$fields;
  170.                         printf("%s = %s%s%s%s\n",
                                 $quote,$contract, $val1, $val2, $date);
  171.                 }
  172.                 printf("\ndone\n");
  173.                 printf("test leerCliente()\n%s\nOK\n",
                             json_encode($this->leerCliente('TEST')));
  174.                 printf("test listarContratos()\n%s\nOK\n",
                             json_encode($this->listarContratos('TEST')));
  175.                 printf("test listarCuotas(test,021546)\n%s\nOK\n",
                             json_encode($this->listarCuotas('TEST','021546')));  
  176.                 printf("test listarCuotas(test,021547)\n%s\nOK\n",
                             json_encode($this->listarCuotas('TEST','021547')));  
  177.                 printf("test listarCuotas(test,021548)\n%s\nOK\n",
                             json_encode($this->listarCuotas('TEST','021548')));  
  178.          }
  179. }


Archivo:  protected/extensions/datasourcecampanametro/mysql-dscampanametro.sql


  1. SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
  2. SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
  3. SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL,ALLOW_INVALID_DATES';
    -- -----------------------------------------------------
  4. -- Table `dscampanametro`
  5. -- -----------------------------------------------------
  6. DROP TABLE IF EXISTS `dscampanametro` ;
  7. CREATE  TABLE IF NOT EXISTS `dscampanametro` (
  8.   `cedula` CHAR(11) NOT NULL ,
  9.   `nombres` VARCHAR(100) NULL ,
  10.   `direccion` VARCHAR(100) NULL ,
  11.   `telefono` VARCHAR(100) NULL ,
  12.   `fecha_act` CHAR(16) NULL ,
  13.   `cuotas` BLOB NULL ,
  14.   PRIMARY KEY (`cedula`) )
  15. ENGINE = InnoDB;
  16. SET SQL_MODE=@OLD_SQL_MODE;
  17. SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
  18. SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;



Las Pruebas

Se hicieron las pruebas bajo consola, solo un comando es de pruebas: "actionTestData...", el otro comando no es "una prueba", es un comando para importar el archivo CSV que el cliente nos da cuando la VPN no esta disponible.

Archivo:  protected/commands/CallcenterCommand.php
class CallcenterCommand extends CConsoleCommand {
 public function actionTestDataSourceCampanaMetro() {
  printf("test dataSourceCampanaMetro\n");
              
                // una simulacion de vpn usando nuestro propio servidor
                // no va a leer datos de aqui, solo probara la conexion 
                // desde el componente
  $vpn_db = array(
   'connectionString' => 
                            'mysql:host=localhost;dbname=callcenter',
   'emulatePrepare' => true,
   'username' => 'un usuario',
   'password' => 'una clave',
   'charset' => 'utf8',
  );
  $api = new DataSourceCampanaMetro();
  $api->test($vpn_db);
  printf("\n");
 }

 public function actionImportCsv($localfile){
  printf("importando archivo: [%s] ...\n",$localfile);
  $api = new DataSourceCampanaMetro();
  $ok = $api->importarCsv($localfile);
  printf("resultado importando el CSV: %s\n",$ok);
 }
}

Resultado de las Pruebas

El resultado tras ejecutar el comando:

./yiic callcenter testDataSourceCampanaMetro

fue:
test dataSourceCampanaMetro
tableName is: dscampanametro
getDb call when datasource is: csv...OK
viewName is: 
getDb call when datasource is: vpn...OK

testing quote object decode:
Q1=021546,75.12,84.1344,26/12/2004;Q2=021546,88.17,98.7504,26/12/2005;Q3=021546,102,114.24,26/12/2006;Q4=021546,117,131.04,26/12/2007;Q5=021546,143.13,160.306,26/12/2008;Q6=021546,189.9,212.688,26/12/2009;Q1=021547,241.08,270.01,26/12/2010;Q2=021547,308.04,345.005,26/12/2011;Q1=021548,388.41,435.019,26/12/2012;Q2=021548,575.88,644.986,26/12/2013;
into:
0 = Q1, 021546, 75.12, 84.1344
1 = Q2, 021546, 88.17, 98.7504
2 = Q3, 021546, 102, 114.24
3 = Q4, 021546, 117, 131.04
4 = Q5, 021546, 143.13, 160.306
5 = Q6, 021546, 189.9, 212.688
6 = Q1, 021547, 241.08, 270.01
7 = Q2, 021547, 308.04, 345.005
8 = Q1, 021548, 388.41, 435.019
9 = Q2, 021548, 575.88, 644.986

done
test leerCliente()
{"cedula":"TEST","nombres":"PORCUPINE TREE","direccion":"ARRIVING SOMEWHERE, BUT NOT HERE","telefono":"555-9-123456;555-8-876543","fecha_act":"2013-08-20","cuotas":"Q1=021546,75.12,84.1344,26\/12\/2004;Q2=021546,88.17,98.7504,26\/12\/2005;Q3=021546,102,114.24,26\/12\/2006;Q4=021546,117,131.04,26\/12\/2007;Q5=021546,143.13,160.306,26\/12\/2008;Q6=021546,189.9,212.688,26\/12\/2009;Q1=021547,241.08,270.01,26\/12\/2010;Q2=021547,308.04,345.005,26\/12\/2011;Q1=021548,388.41,435.019,26\/12\/2012;Q2=021548,575.88,644.986,26\/12\/2013;"}
OK
test listarContratos()
["021546","021547","021548"]
OK
test listarCuotas(test,021546)
[["Q1","021546","75.12","84.1344","2004-12-26"],["Q2","021546","88.17","98.7504","2005-12-26"],["Q3","021546","102","114.24","2006-12-26"],["Q4","021546","117","131.04","2007-12-26"],["Q5","021546","143.13","160.306","2008-12-26"],["Q6","021546","189.9","212.688","2009-12-26"]]
OK
test listarCuotas(test,021547)
[["Q1","021547","241.08","270.01","2010-12-26"],["Q2","021547","308.04","345.005","2011-12-26"]]
OK
test listarCuotas(test,021548)
[["Q1","021548","388.41","435.019","2012-12-26"],["Q2","021548","575.88","644.986","2013-12-26"]]
OK

Resumen

Este componente permite que se pueda consultar un cliente aunque la fuente de datos sean datos provenientes de un archivo CSV (tras importacion) o aunque los datos provengan de una conexión a datos que el cliente nos ofrece via VPN.

Sin importar el origen de datos, la aplicación web puede realizar sus casos de uso, la aplicación web nunca sabrá de donde vinieron los datos, solo le importa saber "cuanto debe un moroso, y en que cuotas y contratos"

El cliente nos ha dado datos en forma compactada, esto es parte de otra interfaz que no he nombrado aqui, pero que ayudara al cliente a ofrecer una vista de su sistema para que otro sistema (éste) pueda conectarse y usar sus datos sin necesidad de exponer relaciones de su ORM.

No nos importó que tipo de ORM tiene el cliente. (El usa Oracle)  pero a nosotros no nos interesa, solo nos importa que nos da datos en alguna forma y que nosotros debemos trabajar con eso.

La programación esta desplegada en forma de "Extensión" (deployment), eso ayuda al mantenimiento del código, a no tener código monolítico, y a justificar el costo de todos esos libros de OOP que le dijiste a tu papa que te comprara cuando eras pequeño.

Bienvenido a la OOP.


Nivel 4

Refactory.  El Refactory es necesario cuando un nuevo ciclo de desarrollo esta en curso sobre un mismo proyecto, ayuda a refinar el sistema, quiza podamos llamarlo "la version 2.0". Por qué ha sido necesario un refactory en este proyecto ? Porque las nuevas reglas de negocio "no fueron consideradas al inicio" (ver el tema "El Futuro" en este doc).  Es un descuido ? Posiblemente, pero uno no es mago ni ultragenio para saberselas todas respecto a que ocurrirá en un sistema, lo que si sabemos bien es que el Refactory es una posibilidad siempre, lo que esta mal hecho es: No hacerlo, en cambio hacer parches.

Vuelvo a escribir acerca de los sustantivos (coloreados en rojo al inicio de este documento), eso produce un diagrama de clases mínimo, sin relaciones, como sigue a continuación:


No hay relaciones en este diagrama, solo agregamos "las clases que sujetan cosas", las cosas son lo que son y no cambian, es decir, "El Pago" tiene sus atributos: "banco, a nombre de, monto, tipo de pago, fecha", eso nunca va a cambiar, pero "quien y cuando se crea el pago ?", los siguientes diagramas ilustrarán eso.

Por favor, insisto a gritos: ESTO NO ES UN DIAGRAMA DE DATOS (las tablitas de la base de datos), aqui pueden haber clases que "no son Entidades", por ejemplo la siguiente:



¿ Qué dice este diagrama ?

El diagrama dice que tenemos una clase llamada ViewListaIdentidades, que nos presentará la información aqui indicada (ver atributos), esa vista depende de una lista, por eso la flecha punteada. La lista es parte de una campaña, es decir existe para una campaña y además está asignada a un operador mediante el uso de una "clase de asociación" llamada ListaOperador.

Aquí ya sabemos varias cosas.  Sabemos que los datos de ViewListaIdentidades tiene algunos "{flag}" que le indicarán al operador algunos datos acerca de la identidad, esos "flags" son solo indicadores booleanos, pero para poner un indicador booleano a true o false debemos sacar esa información de algun lado, no compete en este momento saber de dónde, cuando llegue el momento veremos cual será la mejor vía, anticipadamente sabemos "mas o menos" que esos valores vendrán de: "o una vista sql hechar con algun left join" o "mediante atributos creados con magic getters que examinan el modelo para una fila dada y de ahi conocen el valor del flag".

La clase de asociación ListaOperador.

"ListaOperador, clase de asociación"


La clase de asociación ListaOperador dice mucho aca,asocia "un operador a una lista", siendo la lista parte de una campaña.   De nuevo esto arroja mas información:

"La campaña tiene listas de indentidades, cada lista se puede asociar a un operador", y de aqui profundizamos: "una lista puede ser asignada a varios operadores o solo a uno ?", ya eso toca la logica del negocio, que "por ahora" dice: "una lista se asigna a un operador a a vez, para tener control sobre lo que hace el operador".

¿ De dónde sale la necesidad de la clase de asociación ListaOperador ?

Tras conversación con el cliente se entendió que:
"..una lista se asigna a un operador a a vez, para tener control sobre lo que hace el operador.."

lo que la mente analítica traduce en:
"..una lista se asigna a un operador a a vez, para tener control sobre lo que hace el operador.."

entonces, es una relación de una a uno, y aqui se puede ilustrar el grave error que aplican los que hacen sistemas basados en normalización de datos, quienes diran que por ser una relacion de "uno a uno" entonces por leyes de normalización una parte es absorbida por la otra, lo cual hubiese generado que la lista tuviera dependencia sobre el operador, y asi de fácil el sistema se os a dañado en el acto, volviéndose tosco y estático.    De nuevo, gracias a que aqui no estamos haciendo sistemas basados en normalización sino en objetos (OOP) con ayuda de UML entonces nuestro sistema se vuelve dinámico, pudiendo aceptar que un día el cliente diga: "ahora quiero que la misma lista sea compartida por dos operadores".

¿ Cómo hacemos persistir a la clase de asociación ?

Es muy simple, para que un objeto de clase ListaOperador (ver figura arriba)  exista necesita de dos instancias: un operador y una lista. Por tanto, un modelo para hacerle la persistencia formal de datos "entidad-relacion" sería creando una "tabla" en el modelo relacional, con dos campos: "idoperador" y "idlista", ambos formando una llave única.

Mas clases, mas relaciones.

Comprendiendo un poco mejor el significado de las flechas en un diagrama UML, podemos avanzar a una vista un poco mas detallada del diagrama de clases con sus relaciones, las flechas de linea intermitente ( - - - - > ) son dependencias, quieren decir que para que una clase exista otras deben existir (aquellas a donde llega la flecha), esto ayuda a comprender requerimientos de una clase para poder crear una instancia de ella.
Las líneas sólidas normalmente se usan para decir que: "algo hace alguna acción acá", las lineas con rombo en la punta dicen: "es parte de de".



La Persistencia - ¿ Por qué no vemos diagramas de datos en este diseño ?

¿ Por qué no vemos diagramas de datos en este diseño ? al modelado no le interesa ni se lleva bien con la persistencia, una cosa es la persistencia y otra el modelado, podriamos almacenar todo en un objeto BLOB en forma JSON y el sistema funcionaría de maravilla desde el punto de vista operacional, quizá un poco lento, pero es aquí en donde decidimos cómo es mejor almacenar, en base a que criterios de optmización, no se trata de crear tablas por crearlas, una tabla se crea para almacenar un modelo no una clase, es decir, en una tabla pudiesemos almacenar información mixta que represente a una parte del modelo. Mucha gente confunde "Entidades" con "Relaciones" y con "Clases", llegandolas a nombrar a todas como "el modelo" creyendo que todas son "cosas" que se almacenan con un CActiveRecord (caso YiiFramework ORM).

Como ejemplo de esto pongo este caso, aqui en este proyecto se le ha pedido datos al cliente respecto a sus morosos.  Cada "registro" de "su tabla" que el nos envía por medio de una conexión a datos o mediante un CSV no representa a una clase en si, representa una mezcla de objetos:

"cedula, nombres, direccion, telefonos, cuotas"

siendo "cedula, nombres, direccion, telefonos" datos para una instancia de clase Persona (la cual acá llamamos "Identidad") y siendo "cuotas" una tira de datos que representa la morosidad, es un string que tiene esta forma:

"Qn=Atributo, Atributo, ... , Atributo; Qn=Atributo, Atributo,.. ;"

Su representación formal sería:

"Q<n>=<attr>[,<attrb>][;<*>]"  (siendo el * un simbolo de repetición de objeto)

Pues bien, es un objeto mixto el que recibimos del cliente, venga de una conexión directa por VPN o venga en forma de registro en un archivo CSV.  De esto se encarga el componente que hicimos al inicio de este documento: "DataSourceCampana".

Pues bien, el cliente tiene su propio ORM, con su normalización de grado 3, del cual sale los datos que nos da en el campo "cuotas". Muy bien, pero, necesitamos nosotros todo ese grado de purificación en un sistema al cual solo le importa saber cuanto debe un cliente ? respuesta: NO, no lo necesitamos, por tanto le pedimos al cliente una parte muy puntual, solo lo mínimo necesario.

Los diagramas de estados


¿ Qué dice este diagrama ?

Dice que un actor (valga decir cualquiera quien inicie una llamada al API.iniciarLlamada)  a crear un objeto de clase Llamada inicialmente teniendo el estado "llamando", por tanto un supervisor podría saber que llamadas estan en siendo atendidas "en este momento".

Cuando el actor cuelga la llamada se hace un cambio de estado, es decir una llamada cambia de estado "llamando" a algún otro estado, (el cambio de estado se grafica con la flecha etiqueda como "close", podria haber escrito ahi "cerrando llamada").

Una llamada se cierra y adquiere un nuevo estado, "colgado, no atiende, equivocado, prueba" o uno de "enviar cobrador,  deposito, transferencia, paga por callcenter, paga por oficina, llamar luego".

Algunos estados van a crear nuevos objetos, por ejemplo, si el nuevo estado de una llamada es "enviarCobrador" entonces tenemos que crear un nuevo objeto de clase VisitaDeCobranza, relacionado con una llamada (para saber que llamada originó la visita, por consecuencia que operador, para que identidad etc).  Igual sucede con otros estados, que generarán otros objetos, este diagrama ayuda a saber qué objetos se crean tras cerrar una llamada:


Notar que puse un semi-colon antes del nombre:  ":Pago", esto significa que es una instancia de Pago, y no la clase Pago ya que no tendría sentido decir que "la clase Pago es hija de Llamada", ahora, una instancia de Pago que pudo o no haber sido creada tras una llamada si tiene sentido, y no es una dependencia para que un pago exista, simplemente si hay una marca que diga "este pago viene de una llamada" es suficiente, si no tiene ningun indicador sería porque DIOS así lo quizo ? , no.., en cambio, es bueno saber de donde proviene el pago, respecto al flujo del negocio.


La Pantalla


Esta imagen representa a la pantalla del operador.  En UML estas pantallas se hacen frente a quien nos pide el sistema, en este caso ya conozco las necesidades y procedo a dibujar la mejor forma de dar operatividad al usuario final.

Pantalla vs Formulario

Debo aclarar que hay una diferencia entre "pantalla" y "formulario". El formulario es para pedir datos, validarlos y entregar un objeto asegurando que cumple normas. La pantalla es un contenedor: de formularios y controles. Por tanto la imagen arriba representa una pantalla, que contiene formularios y controles.

Pantallas Grandes y Moviles

Verás que hay formularios anidados, cómo hacerlos en un móvil ? si corre en un teléfono pues no habrá posibilidad práctica de hacer una pantalla tan grande y compleja por tanto el sistema deberá tener pantallas mas específicas pero que conceptualmente hacen lo que aquí se indica.

Si corremos esto en una pantalla grande entonces este diseño de pantalla (la imagen arriba) si podría hacerse, con diversas técnicas: ajax por ejemplo, jQuery, etc).

Lo importante aquí es que debemos darle esa operatividad al usuario final, por la via que sea.

Y cómo funciona todo en conjunto ?



NO, Esto no es un "diagrama de flujo", el diagrama de flujo se usaba en la época en que yo tenía 7 años y mi papá me llevaba a ver las computadoras de su oficina que trabajan con Cassettes ( que ponia a hacer sonar en un toca-cassetts...solo hacían un ruido: "T-rrr--T--rrrrr---T----PIIIIII" (eran los bits sonando).


Este diagrama es un "Diagrama de Estados del Operador".  El operador esta sentado en su puesto, haciendo nada, viendo un video de Youtube mientras debería estar trabajando, y de repente el sistema del callcenter le dispara una llamada asociada a una cédula, entonces, cambia de estado (la flecha) a "Digita en FormBuscaCedula", el sistema busca la cédula y cambia de estado al sistema mostrandole el formulario donde puede manipular a un moroso.

Los cuadros son explícitos, deberían serlo, si hago cuadros que nadie entiende entonces cómo pretendo que otro entienda en 6 meses mas adelante lo hecho aqui.

Es un diagrama de estados porque el operador pasa de un estado a otro (haciendo nada,  buscando cedula, operando llamada, etc.

Dónde iría un "diagrama de flujo" ? muy adentro de una operación muy específica, por ejemplo el detalle de un algoritmo de cálculo.

Una muestra de código

El siguiente código desplegado como un "componente en yii framework" tiene dos métodos muy importantes: crearLlamada y cerrarLlamada.  Ver el código.

Debido a que esto es un Callcenter que hace "llamadas" he decidido comenzar el desarrollo por "una llamada", y no por otro lado.  Ha sido una buena decisión, ya que de estos dos métodos salieron los detalles de las clases involucradas: Llamada, Pago y VisitaDeCobranza, las cuales cumplen los requisitos de dependencia diseñados al inicio (ver los diagramas UML del nivel 4).

A esto yo lo llamo "código quirúrgico" porque no hay inventos, se programa en forma muy exacta, sin pérdida de tiempo, gracias a un análisis previo, al lenguaje UML y a escribir el problema en un papel...

Por cierto, notarás que aún no he creado ni siquiera un mínimo bosquejo del modelo de datos, vuelvo a insistir que me importa en lo mas mínimo y como siempre digo: es la sombra del modelado, aunque sí lo usaremos para persistir los objetos aquí creados. La única forma de código aquí dependiente del ORM es la linea que dice: $inst = Llamada::model()->findByPk($llamada_id)de resto no hay dependencias duras  a un ORM específico. Voy a usar la clase CActiveRecord como gestor del ORM.



  1. <?php
  2. class CallcenterApi extends CApplicationComponent {
  3.         public function getDb(){
  4.                 return Yii::app()->db;
  5.         }
  6.         public function createLlamada($cedula, $campana_id, $lista_id){
  7.                 $inst = new Llamada();
  8.                 $inst->campana_id = $campana_id;
  9.                 $inst->lista_id = $lista_id;
  10.                 $inst->identidad = $cedula;
  11.                 $inst->estatus = Llamada::ESTATUS_LLAMANDO;
  12.                 $inst->fechahora_inicia = time();
  13.                 return $inst;
  14.         }      
  15.         public function closeLlamada($llamada_id, $estatus,
                $fechahora_nueva_llamada, $data=array()){
  16.                 $inst = Llamada::model()->findByPk($llamada_id);
  17.                 if($inst == null)
  18.                         throw new Exception("id llamada invalido");
  19.                 $tr = $this->getDb()->beginTransaction();
  20.                 $inst->fechahora_cierra=time();
  21.                 $inst->estatus = $estatus;
  22.                 $tipo_de_pago = ""; // usado para crear un pago, en caso de.
  23.                 // casos especificos segun estado
  24.                 if($estatus == Llamada::ESTATUS_PAGA_POR_OFICINA){
  25.                         $tipo_de_pago = "oficina";
  26.                 }elseif($estatus == Llamada::ESTATUS_PAGA_POR_CALLCENTER){
  27.                         $tipo_de_pago = "callcenter";
  28.                 }elseif($estatus == Llamada::ESTATUS_PAGA_CON_DEPOSITO){
  29.                         $tipo_de_pago = "deposito";
  30.                 }elseif($estatus == Llamada::ESTATUS_PAGA_CON_TRANSFERENCIA){
  31.                         $tipo_de_pago = "transferencia";               
  32.                 }elseif($estatus == Llamada::ESTATUS_ENVIAR_COBRADOR){
  33.                         $ivisita = new VisitaDeCobranza();
  34.                         $ivisita->llamada_origen = $inst->id;
  35.                         $ivisita->fechahora_creado = time();
  36.                         $ivisita->identidad = $inst->identidad;
  37.                         $ivisita->campana_id = $inst->campana_id;
  38.                         $ivisita->cobrador_id = $data['cobrador_id'];
  39.                         if(!$visita->insert()){
  40.                                 Yii::log(__METHOD__.", error al crear una VisitaDeCobranza"
  41.                                         ."llamada:\n".CJSON::encode($inst)
  42.                                         ."\ndata:\n".json_encode($data),"error");
  43.                                 $tr->rollback();
  44.                                 return false;
  45.                         }
  46.                 }else{
  47.                         // los otros estados no requieren acciones especiales:
  48.                         //      "llamar luego", "colgado", "equivocado", "prueba"
  49.                 }
  50.                 // debe crear un pago,
  51.                 if($tipo_de_pago != ""){
  52.                         $ipago = new Pago();
  53.                         $ipago->llamada_origen = $inst->id;
  54.                         $ipago->fechahora_creado = time();
  55.                         $ipago->identidad = $inst->identidad;
  56.                         $ipago->campana_id = $inst->campana_id;
  57.                         $ipago->tipo = $tipo_de_pago;
  58.                         $ipago->setAttributes($data);
  59.                         if(!$ipago->insert()){
  60.                                 Yii::log(__METHOD__.", error al crear pago"
  61.                                         ."llamada:\n".CJSON::encode($inst)
  62.                                         ."\ndata:\n".json_encode($data),"error");
  63.                                 $tr->rollback();
  64.                                 return false;
  65.                         }
  66.                 }
  67.                 // caso comun: todos podrian derivar en una nueva llamada programada
  68.                 // segun decision del operador o del negocio
  69.                 if($fechahora_nueva_llamada != null){
  70.                         // hay que crear un nuevo objeto de llamada para una fecha hora
  71.                         $llamadaprog = new Llamada();
  72.                         $llamadaprog->campana_id = $inst->campana_id;
  73.                         $llamadaprog->lista_id = $inst->lista_id;
  74.                         $llamadaprog->identidad = $inst->identidad;
  75.                         $llamadaprog->llamada_origen = $inst->id;
  76.                         $llamadaprog->estatus = Llamada::ESTATUS_PROGRAMADA;
  77.                         $llamadaprog->fechahora_programada =
  78.                                 strtotime($fechahora_nueva_llamada);
  79.                         if(!$llamadaprog->insert()){
  80.                                 Yii::log(__METHOD__.", error al crear llamada programada\n"
  81.                                         ."llamada:\n".CJSON::encode($inst)
  82.                                         ."\ndata:\n".json_encode($data),"error");
  83.                                 $tr->rollback();
  84.                                 return false;
  85.                         }
  86.                 }
  87.                 if(!$inst->save()){
  88.                         Yii::log(__METHOD__.", error al guardar llamada\n"
  89.                                 ."llamada:\n".CJSON::encode($inst)
  90.                                 ."\ndata:\n".json_encode($data),"error");
  91.                         $tr->rollback();
  92.                         return false;                                                          
  93.                 }else{
  94.                         // happy-day
  95.                         $tr->commit();
  96.                         return true;
  97.                 }
  98.         }
  99. }



9 comentarios:

  1. El objetivo de este tema es mostrar cómo se enfrenta una solución de código no monolitica, sin desastre e inprovisación, planificando primero en pizarra (electrica o mecanica) y luego hacer el código quirurgico, el cual es código que no tiene ni una linea mas ni una linea menos y se que escribe en forma secuencial.

    La solución la implemente en un caso real, fui escribiendo el blog mientras iba pensando como resolver el problema. Por tanto puede servir de guía para responder esa pregunta que siempre me hacen acerca de cómo implemento soluciones estructuradas con OOP y UML.

    ResponderEliminar
  2. debo añadir que el fruto de esta manera de enfrentar la solución a implementar es que debido a la planificación se puede implementar un TDD formal con mucha facilidad y "on the run", es decir..sin programar dos veces, ni "haciendo dos sistemas" como mucha gente dice. Es un solo sistema, por tanto lo que hacemos "debe ser verificable" en la medida que se construye.

    bienvenido a la TDD+OOP

    ResponderEliminar
  3. Felicidades por el artículo, viene bien! ;)

    ResponderEliminar
  4. Excelente artículo. Me llama mucho la atención el comentario que haces sobre el error que se comete al diseñar un Software con el modelado de datos, muchas personas (inclusive yo) lo realizan de esa forma.
    Sería grandioso si pudieses compartir un artículo ejemplificando a detalle sobre como sería la forma correcta de hacerlo, es decir, diseñando los modelos del Software correctamente con UML.

    Gracias por compartir tus conocimientos, un saludo!

    ResponderEliminar
    Respuestas
    1. gracias, aqui hay enlaces que hablan de eso.

      http://trucosdeprogramacionmovil.blogspot.com/p/software-modeling.html

      sobre todo este:

      http://trucosdeprogramacionmovil.blogspot.com/2013/02/clases-y-tablas-por-que-no-son-lo-mismo.html

      Eliminar
  5. muy bueno Christian, te leía en el foro de Yii y tus artículos son muy didácticos, un abrazo desde Argentina!

    ResponderEliminar