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
Mas en: http://www.cursoangularjs.es

Arquitectura en 3 capas y modelos ricos en AngularJS
By Lorenzo Gonzalez
Arquitectura en 3 capas y modelos ricos en AngularJS
- 6,723