Arquitectura en 3 capas y modelos ricos en AngularJS

12 de Marzo 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 

  • 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

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

¿Soy desarrollador de Frontend?

------

INCISO

------

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

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 

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.urlGet="/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.

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(objeto.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é

Validaciones

del Modelo

Justificación

Puedo tener varios controladores 

para la misma entidad

Poner validaciones en el propio objeto

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

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

        $validators:  [
           {
              propertyName:  function() {
                  return "confirmPassword"
              },
              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 "DomainValidator"

¿Quien llama a "DomainValidator"?

Dos opciones

Service o Repository

 

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

Estructura Final

Gracias a todos por

vuestra atención

Preguntas

Arquitectura en 3 capas y modelos ricos en AngularJS

By Lorenzo Gonzalez Gascón

Arquitectura en 3 capas y modelos ricos en AngularJS

  • 8,721