Arquitectura en 3 capas y modelos ricos en AngularJS

Madrid , 9 de Mayo de 2015

¿Quien soy yo?

Lorenzo, un profesor de FP

 

¿Quieres que mis alumnos hagan las prácticas en tu empresa?
¡Ponte en contacto conmigo!

¿De que va realmente esta charla?

De abrir los ojos a

nuevas posibilidades 

El código de los ejercicios

está en

  • Repaso Arquitectura


  • Repaso de AngularJS


  • Diseño de una capa genérica


  • Modelos Ricos


  • Validaciones del Modelo

Contenido

Repaso Arquitectura

Cliente - Servidor

Web Backend

AngularJS Básica

Frontend

Backend

AngularJS con Servicios

Frontend

Backend

AngularJS con Persistencia

Frontend

Backend

¿Lo obvio no es añadir una capa de persistencia?

Repository y DAO

Repository vs DAO

Repository

Evita repetir código en los DAO

Realiza la coordinación entre DAOs

Enriquece el modelo

Unificar comportamiento entre DAOs

DAO

Solo realizan una única tarea:

HTTP,

HTML5 Storage,

etc.

Ejemplo de Repository y DAOs

NOTA:Obviamente en JavaScript no hay interfaces, pero es para que se entienda

¿Y para que sirve la 

capa de servicios?

Solo para cuando tengas que llamar a

mas de un "Repository"

Inicialmente no hace nada solo pasa datos pero sirve para

poder modificarla libremente

La capa de Service coordina diversos Repository

Frontend y Backend

INCISO

¿Hay tanta diferencia?

Un poquito de repaso de AngularJS

Crear un objeto  e inyección


MiClase.$inject=['$http','$log']
function MiClase($http,$log) {


}
var miClase = $injector.instantiate(MiClase);

Definir dependencias en una clase

Crear una instancia

var locals = {
    $log: miNuevoLog
};
var miClase = $injector.instantiate(MiClase,locals);

Sobreescribir inyecciones (realmente pasar parámetros)

Ejecutar una función e inyección


var miFuncion=['$http','$log',function($http,$log) {


}]
$injector.invoke(miFuncion,elThis);

Función con inyección de dependencias

Ejecutar la función

var locals = {
    $log: miNuevoLog
};
var resultado = $injector.invoke(miFuncion,elThis,locals);

Sobreescribir inyecciones (realmente pasar parámetros)

Interpolar Strings


    var urlExpression=$interpolate("/api/{{entityName}}/{{id}}");
    
    var urlUsuario=urlExpression({
        entityName:"Usuario",
        id:3
    });

    var urlEmpresa=urlExpression({
        entityName:"Empresa",
        id:12
    });
urlUsuario="/api/Usuario/3"

urlEmpresa="/api/Empresa/12"

El resultado es:

La expresión se puede reusar

Ejercicio 1

Modifica la rama "master" para hacer que se usen 3 serviciós de AngularJS:

  • usuarioService
  • usuarioRepository
  • usuarioRemoteDAO

La solución está en la rama "tres_capas_horror"

Diseño de una

 capa genérica

Service             Repository         RemoteDAO 

Una aplicación suele tener gran cantidad de entidades

  • Usuario
  • Compra
  • Movimiento
  • Rol
  • Tarjeta
  • Alumno
  • Centro
  • Profesor
  • Etc..

Se genera mucho 

código repetido

angular.module("name").service("usuarioRemoteDAO", UsuarioRemoteDAO);
angular.module("name").service("compraRemoteDAO", CompraRemoteDAO);
angular.module("name").service("movimientoRemoteDAO", MovimientoRemoteDAO);
angular.module("name").service("rolRemoteDAO", RolRemoteDAO);
angular.module("name").service("tarjetaRemoteDAO", TarjetaRemoteDAO);
angular.module("name").service("alumnoRemoteDAO", AlumnoRemoteDAO);


angular.module("name").service("usuarioRepository", UsuarioRepository);
angular.module("name").service("compraRepository", CompraRepository);
angular.module("name").service("movimientoRepository", MovimientoRepository);
angular.module("name").service("rolRepository", RolRepository);
angular.module("name").service("tarjetaRepository", TarjetaRepository);
angular.module("name").service("alumnoRepository", AlumnoRepository);


angular.module("name").service("usuarioService", UsuarioService);
angular.module("name").service("compraService", CompraService);
angular.module("name").service("movimientoService", MovimientoService);
angular.module("name").service("rolService", RolService);
angular.module("name").service("tarjetaService", TarjetaService);
angular.module("name").service("alumnoService", AlumnoService);

Coding Horror

Solución:Capas Genericas


angular.module("name").service("serviceFactory", ServiceFactory);


angular.module("name").service("repositoryFactory", RepositoryFactory);


angular.module("name").service("remoteDAOFactory", RemoteDAOFactory);

ServiceFactory

RepositoryFactory

RemoteDAOFactory

Únicamente 3 servicios de AngularJS

UsuarioRepository.$inject=['remoteDAOFactory'];
function UsuarioRepository(remoteDAOFactory) {

    var usuarioRemoteDAO=remoteDAOFactory.getRemoteDAO("Usuario");

    var promise=usuarioRemoteDAO.get(3);

}

Ejemplo de uso de remoteDAOFactory

Se llama a getRemoteDAO(entidad)

RemoteDAO.$inject = ['$http', 'entityName'];
function RemoteDAO($http, entityName) {

   this.entityName=entityName

   this.get = function (id) {
        var promise=$http({
          method:"GET",
          url:"/api/" + entityName + "/" + id;
        }); 

        //Implementar el método

   };
}

RemoteDAO

RemoteDAOFactory.$inject = ['$injector'];
function RemoteDAOFactory($injector) {

   var remoteDAOs = {
   };

   this.getRemoteDAO = function (entityName) {
      if (!remoteDAOs[entityName]) {
         var locals = {
            entityName: entityName
         };
         remoteDAOs[entityName] = $injector.instantiate(RemoteDAO,locals);
      }


      return remoteDAOs[entityName];
   };
}

RemoteDAOFactory

El resto de capas pueden hacerse con el mismo patrón

Service              Repository       LocalDAO 

Ejercicio 2

Modifica la rama "tres_capas_horror" para hacer que se usen los 3 servición de AngularJS en vez de los anteriores:

  • serviceFactory
  • repositoryFactory
  • remoteDAOFactory

La solución está en la rama "simple_factory"

Problemas y mejoras

Cual es el nombre de la clave "primaria"

Soluciones

  • Siempre se llamada "id"
  • Se llama "id" + entityName
  • Hay que pasarsela en getRemoteDAO
  • Usar metadatos que pedimos al servidor  

<--- Mi solución

Para el PUT y el DELETE   /entidad/id

Y si queremos añadir métodos específicos al remoteDAO

Solución

El DAOFactory es un "provider" de AngularJS

Añadimos funciones para personalizar cada

objeto remoteDAO

remoteDAOFactoryProvider.setExtendRemoteDAO("Usuario",['remoteDAO',function(remoteDAO) {

   remoteDAO.deshabilitar=function(usuario) {
       //Aqui el código específico del nuevo método del DAO
   };

}]);

Usando 

RemoteDAOFactoryProvider

Añadimos el método "deshabilitar" al remoteDAO de Usuario

Cuando se cree un nuevo UsuarioRemoteDAO se llamará a la función para que pueda añadir los métodos que quiera al RemoteDAO 

RemoteDAOFactory.$inject = ['$injector', 'extendRemoteDAO'];
function RemoteDAOFactory($injector, extendRemoteDAO) {

   var remoteDAOs = {
   };

   this.getRemoteDAO = function (entityName) {
      if (!remoteDAOs[entityName]) {
         var locals = {
            entityName: entityName
         };
         remoteDAOs[entityName] = $injector.instantiate(RemoteDAO,locals);

         if (extendRemoteDAO[entityName]) {
            var locals = {
               remoteDAO: remoteDAOs[entityName]
            };

            $injector.invoke(extendRemoteDAO[entityName], undefined, locals);
         }

      }


      return remoteDAOs[entityName];
   };
}

RemoteDAOFactory

Al crear el RemoteDAO llamamos a una función para que lo "extienda" como quiera

RemoteDAOFactoryProvider.$inject = ['$injector'];
function RemoteDAOFactoryProvider() {
   var extendRemoteDAO = {
   };

   this.setExtendRemoteDAO = function (entityName, fn) {
      extendRemoteDAO[entityName] = fn;
   };

   this.$get = ['$injector', function ($injector) {
      var locals = {
         extendRemoteDAO: extendRemoteDAO
      };
      return $injector.instantiate(RemoteDAOFactory, locals);
    }];

}

RemoteDAOFactoryProvider

¿Y si solo queremos modificar las URL del RemoteDAO?

Solución

Hacer personalizables las URL

RemoteDAO.$inject = ['$http', '$interpolate', 'entityName'];
function RemoteDAO($http, $interpolate, entityName) {

   this.entityName=entityName
   this.urlGet="/api/{{entityName}}/{{id}}";
   this.urlGetExpression=$interpolate(this.urlGet);

   this.get = function (id) {
        var url=urlGetExpression({
            entityName:this.entityName,
            id:id
        });

        $http({
          method:"GET",
          url:url
        }); 

        //Implementar todo el método

   };
}

RemoteDAO con URLs personalizables

remoteDAOFactoryProvider.setExtendRemoteDAO(
    "Usuario",['remoteDAO','$interpolate',function(remoteDAO,$interpolate) {

        remoteDAO.urlGetExpression=$interpolate("/api/v2/{{entityName}}/{{id}}");

}]);

Modificando únicamente la URL del RemoteDAO

¿Y si solo queremos añadir funcionalidad?

Solución

Añade "triggers" o puntos de extensión

RemoteDAO.$inject = ['$http', '$interpolate', 'entityName'];
function RemoteDAO($http, $interpolate, entityName) {

   this.entityName=entityName
   this.urlGet="/api/{{entityName}}/{{id}}";
   this.urlGetExpression=$interpolate(this.urlGet);
   this.preGet=function() {};
   this.postGet=function() {};

   this.get = function (id) {
        var url=urlGetExpression({
            entityName:this.entityName,
            id:id
        });

        this.preGet();

        $http({
          method:"GET",
          url:url
        }); 

        this.postGet();  //OJO:Debe estar en la promesa

        //Implementar todo el método

   };
}

RemoteDAO con "triggers"

remoteDAOFactoryProvider.setExtendRemoteDAO(
    "Usuario",['remoteDAO','$interpolate',function(remoteDAO,$interpolate) {

          remoteDAO.postGet=function() {
              alert("Leido usuario");
          }

}]);

Añadiendo un "trigger" al RemoteDAO

Otra forma de personalizar los RemoteDAO

RemoteDAOFactory.$inject = ['$injector'];
function RemoteDAOFactory($injector) {

   var remoteDAOs = {
   };

   this.getRemoteDAO = function (entityName) {
      if (!remoteDAOs[entityName]) {

        if ($injector.has(entityName + "RemoteDAO") {
           remoteDAOs[entityName] = $injector.get(entityName + "RemoteDAO");
        } else {
           var locals = {
              entityName: entityName
           };
           remoteDAOs[entityName] = $injector.instantiate(RemoteDAO,locals);
        }

      }

      return remoteDAOs[entityName];
   };
}

Crear directamente el servicio del RemoteDAO de una entidad

Si queremos personalizar el RemoteDAO de "Usuario" creamos el servicio "UsuarioRemoteDAO"

Identity Pattern

Solución

No he encontrado ninguna solución que sea:

buena, bonita y barata

:-(

Si sabes lo que estás haciendo puedes tener soluciones sencillas para la casuística de tu proyecto.

Ejercicio 3

Modifica la rama "simple_factory" para hacer que ahora los "Service", "Repository" y "RemoteDAO" se puedan extender.

La solución está en la rama "factory_con_providers_completo"

Modelos ricos

El JSON que obtienes del servidor

 

NO es un objeto de dominio

 

es un DTO (Data Transfer Object)

Es necesario enriquecer el DTO y obtener el modelo rico

var dtoUsuario1 = {
    nombre: "Marcos",
    ape1: "Salas",
    ape2: "Lopez",
    fechaNacimiento: 31548664546,
    tipoUsuario: "ALUMNO"
};

var dtoUsuario2 = {
    nombre: "Carlos",
    ape1: "Diaz",
    ape2: "Ortega",
    fechaNacimiento: 64874654646,
    tipoUsuario: "PROFESOR",
    departamento: 3
};

DTO de Usuario

var modelUsuario1 = {
    nombre: "Marcos",
    ape1: "Salas",
    ape2: "Lopez",
    getNombreCompleto: function () {
        return this.nombre + " " + this.ape1 + " " + this.ape2;
    },
    fechaNacimiento: new Date(31548664546),
    tipoUsuario: "ALUMNO"
};

var modelUsuario2 = {
    nombre: "Carlos",
    ape1: "Diaz",
    ape2: "Ortega",
    getNombreCompleto: function () {
        return this.nombre + " " + this.ape1 + " " + this.ape2;
    },
    fechaNacimiento: new Date(64874654646),
    tipoUsuario: "PROFESOR",
    departamento: 3,
    getNombreDepartamento: function () {
        return departamentoService.getNombre(this.departamento);
    }
};

Modelo Rico de Usuario

¿Como lo hemos enriquecido?

  • Añadir métodos: getNombreCompleto


  • Transformar datos:El número a objeto Date


  • Métodos optativos: getNombreDepartamento


  • Uso de Servicios: departamentoService


Quien los enriquece

Nuevo provider de AngularJS llamado 

RichModelProvider

Repository llama a RichModel

Funciones de RichModelProvider

Enriquecer una entidad concreta

addEntityTransformer(entityName,fn);

Enriquecer todas las entidades

addGlobalTransformer(fn);

app.config(['richModelProvider', function(richModelProvider) {

  richModelProvider.addGlobalTransformer(function() {

      var uniqueID=0;


      return function (object) {
        object.$uniqueID=uniqueID;
        uniqueID++;

      };

    });

}]);

addGlobalTransformer

La primera función se ejecuta una única vez y después de la fase de configuración por lo que permite inyectar servicios. En ella se define una única vez lo que se va a enriquecer.La  función que retorna es llamada para cada objeto a enriquecer

app.config(['richModelProvider', function(richModelProvider) {

  richModelProvider.addEntityTransformer('Usuario',['departamentoService',function(departamentoService) {

      function getNombreCompleto () {
        return this.nombre + " " + this.ape1 + " " + this.ape2;
      }

      function getNombreDepartamento () {
        return departamentoService.getNombre(this.departamento);
      }

      return function (object) {
        object.getNombreCompleto = getNombreCompleto;
        if (object.tipoUsuario === "PROFESOR") {
          object.getNombreDepartamento = getNombreDepartamento;
        }

        object.fechaNacimiento = new Date(object.fechaNacimiento);
      };
    }]);

}]);

addEntityTransformer

¿Que pasa con las relaciones?

Un mismo modelo de dominio puede pertenecer a N modelos distintos

¡No queremos repetir código ni preocuparnos por las relaciones!

El centro

pertenece a

un Usuario

var modelUsuario2 = {
    nombre: "Carlos",
    ape1: "Diaz",
    ape2: "Ortega",
    getNombreCompleto: function () {
        return this.nombre + " " + this.ape1 + " " + this.ape2;
    },
    fechaNacimiento: new Date(64874654646),
    tipoUsuario: "PROFESOR",
    departamento: 3,
    getNombreDepartamento: function () {
        return departamentoService.getNombre(this.departamento);
    },
    centro: {
       codigo:"46567",
       nombre:"CIFP Mislata",
       getCodigoProvincia: function() {
          return this.codigo.substring(0,2);
       }
    }
};

El centro

pertenece a

un Aula

var modelAula = {
    nombre:"2A5",
    piso:2,
    centro: {
       codigo:"46567",
       nombre:"CIFP Mislata",
       getCodigoProvincia: function() {
          return this.codigo.substring(0,2);
       }
    }
};

Una solución

poco adecuada

Añadir la función "getCodigoProvincia" tanto en el Usuario como en el Aula

Hay que repetir el código en cada sitio donde aparece una misma entidad

app.config(['richModelProvider', function(richModelProvider) {

  richModelProvider.addEntityTransformer('Usuario',function() {

       function getCodigoProvincia() {
          return this.codigo.substring(0,2);
       }

      return function (object) {
        object.centro.getCodigoProvincia=getCodigoProvincia;
      };
    });

}]);

Conocer la relación entre Usuario y Centro

Usuario y Centro

app.config(['richModelProvider', function(richModelProvider) {

  richModelProvider.addEntityTransformer('Aula',function() {

       function getCodigoProvincia() {
          return this.codigo.substring(0,2);
       }

      return function (object) {
        object.centro.getCodigoProvincia=getCodigoProvincia;
      };
    });

}]);

Conocer la relación entre Aula y Centro

Aula y Centro

app.config(['richModelProvider', function(richModelProvider) {

  richModelProvider.addEntityTransformer('Centro',function() {

       function getCodigoProvincia() {
          return this.codigo.substring(0,2);
       }

      return function (object) {
        object.getCodigoProvincia=getCodigoProvincia;
      };
    });

}]);

Enriquecer directamente la entidad "Centro"

Mi solución

¿Pero como se que que hay un "Centro" si en JavaScript no hay tipos?

¡Hago trampas!

Desde la parte Servidora (Java) si que tengo tipos así que mis JSON siempre incluyen el nombre de la clase

var modelUsuario2 = {
    nombre: "Carlos",
    ape1: "Diaz",
    ape2: "Ortega",
    fechaNacimiento: 64874654646,
    tipoUsuario: "PROFESOR",
    departamento: 3,
    $className:"Usuario",
    centro: {
       codigo:"46567",
       nombre:"CIFP Mislata",
       $className:"Centro"
    }
};
var modelAula = {
    nombre:"2A5",
    piso:2,
    $className:"Aula",
    centro: {
       codigo:"46567",
       nombre:"CIFP Mislata",
       $className:"Centro"
    }
};

Cada Objeto dispone de la propiedad $className

Ya puedo enriquecer "Centro" independientemente de donde esté

Ejercicio 4

Desde la rama "factory_con_providers_completo", crea el provider de AngularJS llamado "richModel" y enriquece el usuario con los métodos que se indicaban al inicio de esta sección

A "richModel" se llamará desde "repository"

La solución está en la rama "rich_domain"

Validaciones

del Modelo

¿Por qué?

Puedo tener varios controladores 

para la misma entidad y así evito repetir código

Poner validaciones en el propio objeto

app.config(['richModelProvider', function(richModelProvider) {

  richModelProvider.addEntityTransformer('Usuario',function() {

        var $validators = [
           {
              propertyName:  function() {
                  return "Confirmar Contraseña"
              },
              message:  'El valor de {{password}} no  es  igual  al de {{confirmPassword}}',
              rule:  function  ()  {
                 if  (this.password  ===  this.confirmPassword)  {
                    return  true;
                 }  else  {
                    return  false;
                 }
              }
           }
        ]

      return function (object) {
        object.$validators=$validators;
      };
    });

}]);

Array de validaciones llamado "$validators"

¿Quien ejecuta estas validaciones?

Nuevo servicio llamado "ModelValidator"

¿Quien llama a "ModelValidator"?

Dos opciones

Service o Repository

 

Yo uso Repository para dejar limpio Service para que se pueda modificar libremente

Estructura Final

Ejercicio 5

Desde la rama "rich_domain", crea el servicio de AngularJS llamado "modelValidator" y enriquece el usuario con la validación de confirmar la contraseña

A "modelValidator" se llamará desde "repository"

Tambien añade el método "update" en las 3 capas y muestra los mensajes de error en el HTML

La solución está en la rama "rich_domain"

Gracias a todos por

vuestra atención

Preguntas

JSDay: Arquitectura en 3 capas y modelos ricos en AngularJS

By Lorenzo Gonzalez Gascón

JSDay: Arquitectura en 3 capas y modelos ricos en AngularJS

Transparencias del JSDay sobre el taller deArquitectura en 3 capas y modelos ricos en AngularJS

  • 6,853