MEMORIA DDS
12-02 BLUEJ TITANS
Shiyi Cheng
Juan Pablo Cortés Hernández
Andrés De La Torre Mora
Francisco He Chi
1. Arquitectura del Front End
Se describe la estructura general (inicial) de la aplicación. Esta estructura define los
componentes principales, explicando brevemente su funcionalidad y cómo se relacionan unos
con otros. En caso de que se haya utilizado algún patrón arquitectónico se debe comentar al
respecto al patrón utilizado. Se recomienda utilizar algún tipo de metáfora gráfica para
representar los niveles, componentes principales y sus relaciones.
El "Frontend" está estructurado en distintos paquetes, cada uno dedicado a una
funcionalidad específica de la aplicación, como por ejemplo: auth (para inicio de
sesión y registro), product management o catalogue.
Dentro de estas carpetas, hemos aplicado el patrón arquitectónico MVVM (Modelo
Vista Modelo de Vista) para organizar la parte del "Frontend" correspondiente a cada
funcionalidad del proyecto. Este patrón, ampliamente utilizado en el desarrollo móvil,
tiene como objetivo principal separar la lógica visual del proyecto y mejorar la
capacidad de prueba. MVVM divide la parte del "Frontend" en tres capas principales:
● Modelo (Model): Es responsable de recuperar, almacenar y procesar los datos
de la aplicación.
● Vista (View): Representa la interfaz de usuario y muestra los datos al usuario.
En nuestro caso, esta vista está desarrollada con Jetpack Compose.
● Modelo de vista (ViewModel): Actúa como intermediario entre el modelo y la
vista. Aquí se implementa la lógica necesaria para el procesamiento de datos.
Un ejemplo de esta estructura se encuentra en la implementación de la carpeta
"auth", así como en el resto del frontend, donde se sigue el patrón MVVM. Sin
embargo, nuestras carpetas de funcionalidades tienen elementos adicionales que
explicaremos a continuación:
Observamos dos carpetas principales, junto con la carpeta de "view", que
explicaremos más adelante. Mientras tanto, detallaremos estas dos carpetas:
● data:
remote: Esta carpeta contiene archivos relacionados con la gestión de
○
datos vinculados a la autenticación, que interactúan con la base de
datos, realizan solicitudes a la API, entre otras funciones.
○ repository: Esta interfaz alberga los métodos utilizados por la capa
"remote" y se encarga del manejo de excepciones que pueden surgir
en la capa de datos.
● domain:
○ model: Aquí se definen las clases o estructuras de datos que
representan los conceptos del dominio de la aplicación. Por ejemplo,
tenemos una interfaz "User" que define los elementos comunes a todos
los usuarios, seguida de una clase para cada tipo de usuario específico
que la aplicación manejará en el momento actual (Sprint 1). Esta
carpeta corresponde al "Model" en MVVM, es decir, al modelo del
patrón MVVM.
○ repository: Es una interfaz intermedia entre "data" y "view" que define
las funciones utilizadas en la capa de datos.
Finalmente, la carpeta "View/Presentation" contiene tanto los View como los
ViewModel, las dos partes fundamentales del patrón MVVM.
Estas dos partes se dividen de la siguiente manera:
● view: Aquí se encuentran todos los elementos relacionados con la interfaz
pura, es decir, lo que finalmente se mostrará al usuario.
● viewmodel: En esta carpeta se almacenan todos los ViewModel asociados a
los View de esta funcionalidad.
Los ViewModel se encargan de toda la lógica, así como de las variables que se
utilizarán posteriormente en su View correspondiente.
Las clases que son relevantes en cada viewModel y que pueden llegar a tener
comportamientos en el “Back” hemos decidido almacenarlas en una carpeta state
como vemos en la imagen, para poder acceder de una mejor forma a los atributos
que nos interesan de cada viewModel.
Kotlin nos proporciona una especie de patrón observador mediante la clase
ViewModel de este lenguaje. Al crear el ViewModel, todas las variables se
almacenan como privadas dentro de él. Por lo tanto, creamos un "state" dentro del
ViewModel y su función correspondiente para cambiar los atributos de este estado.
Esto nos permite luego crear variables en los diferentes View que observan este
estado, lo que permite que se actualicen en tiempo real los campos que dependen
de variables definidas en la parte del ViewModel. Por ejemplo, tenemos una variable
en la parte del View que observa a otra almacenada en el ViewModel.
2. Arquitectura del Back End
A continuación se describe la arquitectura utilizada en el proyecto para desarrollar la
aplicación que constituirá la parte del backend.
Hemos optado por emplear Java como lenguaje de programación y Spring como
framework para crear una API REST que conecte nuestro frontend con la base de
datos, así como con la lógica de negocio que procesa toda la información
almacenada.
La arquitectura adoptada es multicapa y se compone de cuatro partes distintas:
1. Controlador: Esta capa se encarga de facilitar la comunicación entre los
clientes y nuestra aplicación. Aquí se encuentra la lógica relacionada con
HTTP, las rutas y el formateo de las solicitudes y respuestas que circulan por
la red.
2. Modelo: En esta sección se encuentra el código que modela nuestros objetos,
como usuarios, productos, entre otros.
3. Servicio: Esta capa, ubicada después del controlador, alberga toda la lógica
de negocio propia de nuestra aplicación. Se encarga de comunicar todos los
datos recibidos del controlador con la siguiente parte del proceso.
4. Repositorio: Esta parte del sistema es la más cercana a la fuente de datos, es
decir, a la base de datos. Actúa como una abstracción de la tecnología
utilizada para almacenar los datos, lo que permite una mayor modularidad en
la arquitectura.
Para organizar el código en funcionalidades o "features", hemos dividido el directorio
raíz, separando el código de cada parte lógica de la aplicación. Como ejemplo de
esta organización, se adjunta una imagen de la feature "Users", donde se ha
implementado todo el código relacionado con el registro y la autenticación de
usuarios.
Se observa la presencia de un paquete adicional que no había sido mencionado
previamente, llamado "http". Este paquete se utiliza para contener diversas clases
Java que modelan las solicitudes y respuestas HTTP. Su objetivo es simplificar y
estandarizar la interacción en nuestra aplicación, facilitando así la tarea del
programador al interactuar con los clientes.
3. Aplicación del Patrón Método Fábrica
Se documentará cómo se ha aplicado finalmente el patrón Método Fábrica al proyecto (en este
sprint). Siguiendo un estilo parecido a la presentación de los patrones de diseño, mostrando la
estructura final de clases y trozos de código representativos. Se comentaran las decisiones
tomadas, cambios o adaptaciones realizadas para integrar bien el patrón en vuestro código.
Para abordar la implementación de nuestra fábrica, es crucial comprender primero
una parte del componente controlador, específicamente la sección responsable de
gestionar las solicitudes de creación de productos mediante la API. Esto se debe a
que la fábrica solo se utiliza dentro de esta clase.
Código que maneja las peticiones POST de productos
Para crear un producto, el frontend debe enviar una solicitud POST a
/products/{productType}, donde productType representa la categoría del producto
que se desea crear. En el cuerpo de la solicitud, se deben incluir los valores para
cada atributo del producto. Posteriormente, se definirá una instancia de la clase
Product utilizando el método de fábrica para la creación de objetos. Este método
recibe como parámetros la categoría del producto y el cuerpo de la solicitud.
@PostMapping("/{productType}")
public ResponseEntity<String> createProduct(@PathVariable String
productType, @RequestBody String productJson){
Product product;
try {
product = (Product)
ProductFactory.createProduct(productType, productJson);
productService.createProduct(product);
} catch(JsonProcessingException e){
return ResponseEntity.badRequest().body("Invalid user data:
" + e.getMessage());
} catch(Exception e){
return ResponseEntity.badRequest().body("Unknown exception
occurred: " + e.getMessage());
return ResponseEntity.created(null).body(product.getName() +
“Created”);
}
Código de la fábrica de productos:
La estructura fundamental de la fábrica implica un switch statement, donde según la
categoría del producto, se invoca el constructor correspondiente de la clase del
producto. No obstante, dado que cada categoría de producto posee atributos
distintos y estos atributos son recibidos en formato JSON como un String, hemos
necesitado ajustar la fábrica haciendo uso de la herramienta Jackson. El
ObjectMapper de Jackson nos permite mapear el cuerpo de la solicitud a los
atributos respectivos de las clases, lo que simplifica considerablemente el código.
package ourProducts.Factory;
import com.fasterxml.jackson.core.JsonProcessingException;
import ourProducts.model.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
public class ProductFactory {
public static IProduct createProduct(String category, String
productJson) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
IProduct product;
switch (category.toLowerCase()) {
case "phone":
return product = objectMapper.readValue(productJson,
Phone.class);
case "home":
return product = objectMapper.readValue(productJson,
Home.class);
case "computer":
return product = objectMapper.readValue(productJson,
Computer.class);
case "book":
return product = objectMapper.readValue(productJson,
Book.class);
case "food":
return product = objectMapper.readValue(productJson,
Food.class);
case "pant":
return product = objectMapper.readValue(productJson,
Pant.class);
case "shirt":
return product = objectMapper.readValue(productJson,
Shirt.class);
case "sweatshirt":
return product = objectMapper.readValue(productJson,
Sweatshirt.class);
case "dress":
return product = objectMapper.readValue(productJson,
Dress.class);
default:
throw new IllegalArgumentException("Invalid product
category: " + category);
Código de la clase Phone
package ourProducts.model;
import java.util.List;
public class Phone extends Technology {
public Phone() {
super();
}
public Phone(String name, String description, double price,
int quantity, String dataSheet,
List<String> photos, int energyDrain, String
model) {
super(name, description, price, quantity, dataSheet,
photos, energyDrain, model);
}
}
En situaciones donde la categoría del producto no existe, se generará una
Excepción con el mensaje correspondiente. Lo mismo sucede si el cuerpo de la
solicitud no puede procesarse correctamente.
Cuando el producto se ha creado con éxito, se devuelve al componente del
controlador y es la capa de servicio la encargada de registrar dicho producto en la
base de datos.
4. Uso y Documentación de IA Generativa
Tal y como se ha comentado en clase podréis ayudaros de Herramientas de IA Generativa para
producir parte del código. En la memoria incluiréis los prompts usados (no todos, porque pueden
haber “probado” muchos, solo el prompt o prompts “finales” o más representativos) y sus
resultados, así como la solución elegida, que puede ser una variación o adaptación de la que
proponga la IA. Debéis explicar por qué se ha elegido (y adaptado) esta solución. Es importante
que quede destacado o remarcado en la memoria qué partes ha resuelto la IA y qué partes
habéis desarrollado vosotros.
Creación de clases
Hemos utilizado ChatGPT para generación de código básico como puede ser la
creación de una clase. Esto incluye la declaración de los atributos y sus respectivos
getters y setters.
Prompt
En java, tengo una clase Product que tiene los siguientes atributos: String name,
double price, int quantity, String description, String dataSheet y List<String> images.
Haz el código de la clase con su constructor y sus getters y setters.
Resultado
public class Product {
private String name;
private double price;
private int quantity;
private String description;
private String dataSheet;
private List<String> images; // Lista de imágenes
public Product(String name, double price, int quantity, String
description, String dataSheet, List<String> images) {
this.name = name;
this.price = price;
this.quantity = quantity;
this.description = description;
this.dataSheet = dataSheet;
this.images = images;
}
// Getters and setters for name, price, quantity, description,
technicalSheet, and images
// Getter and setter for name
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
// Getter and setter for price
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
// Getter and setter for quantity
public int getQuantity() {
return quantity;
}
public void setQuantity(int quantity) {
this.quantity = quantity;
}
// Getter and setter for description
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
// Getter and setter for technicalSheet
public String getDataSheet() {
return technicalSheet;
}
public void setDataSheet(String dataSheet) {
this.dataSheet = dataSheet;
}
// Getter and setter for images
public List<String> getImages() {
return images;
}
public void setImages(List<String> images) {
this.images = images;
}
}
Hemos realizado ajustes adicionales en algunos atributos y hemos introducido
elementos de JPA (Java Persistence API) para facilitar la gestión de la interacción
con la base de datos. Además, hemos incorporado las anotaciones @Getter y
@Setter del Plugin “Lombok”, lo que nos ha permitido generar automáticamente
tanto los getters como los setters de cada atributo, evitando así la necesidad de
mostrar cada uno de estos métodos. Este enfoque nos ha permitido mantener un
código más conciso y legible. A continuación, se muestra la versión final de la clase
Producto:
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@MappedSuperclass
public class Product implements IProduct{
@Id
private String name;
private String description;
private double price;
private int quantity;
private String dataSheet;
private List<String> photos;
public Product(String name, String description, double price,
int quantity, String dataSheet, List<String> photos) {
this.name = name;
this.description = description;
this.price = price;
this.quantity = quantity;
this.dataSheet = dataSheet;
this.photos = photos;
}
}
Creación de la capa de servicio
Para ahorrar tiempo, hemos utilizadoIA para obtener la implementación básica de la
capa de servicio, por ejemplo, para los productos.
Prompt
En una arquitectura multicapa donde tengo: ORM, Repositorio, Servicio, Modelo, etc.
Quiero que en Java SpringBoot me diseñes la capa de servicio de los productos con
las funcionalidades básicas de una aplicación CRUD.
Resultado
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
public Product getProduct(Long id) {
return productRepository.findById(id).orElse(null);
}
public Product createProduct(Product product) {
return productRepository.save(product);
}
public Product updateProduct(Long id, Product product) {
// Implementar lógica para actualizar el producto
}
public void deleteProduct(Long id) {
// Implementar lógica para eliminar el producto
}
public List<Product> getAllProducts() {
return productRepository.findAll();
}
}
De la respuesta de ChatGPT hemos tenido que implementar algunos métodos como
updateProduct y deleteProduct.
@Service
public class ProductService {
private final ProductRepository productRepository;
@Autowired
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public Product getProduct(Long id) {
return productRepository.findById(id).orElse(null);
}
public void createProduct(Product product) {
productRepository.save(product);
}
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
public List<Product> getAllProducts() {
return (List<Product>) productRepository.findAll();
}
public void updateProduct(Long id, Product product) {
Product p = productRepository.findById(id).orElse(null);
if (p != null) {
p.setName(product.getName());
p.setPrice(product.getPrice());
p.setQuantity(product.getQuantity());
p.setDescription(product.getDescription());
p.setDataSheet(product.getDataSheet());
p.setPhotos(product.getPhotos());
productRepository.save(p);
}
}
}
Creación de la fábrica de productos
Inicialmente hemos pedido a ChatGPT que crease la fábrica de productos.
Prompt
Aplicame el patrón de fábrica en java para el siguiente caso:
Tengo una clase Product con atributos: name, description, dataSheet. De Product
hereda Technology con atributos: energyDrain, Model; Book con atributo: ISBN;
Food con atributo: calories; Clothes con atributos: size. De Technology heredan las
clases Phone, Home y Computer. De Clothes heredan las clases Pant, Shirt,
Sweatshirt y Dress.
Resultado
Claro, implementemos el patrón de fábrica en Java para este caso específico.
implementemos la fábrica (ProductFactory) que se encargará de crear instancias de
los productos:
java
Copy code
public class ProductFactory {
public Product createProduct(String type) {
switch (type.toLowerCase()) {
case "phone":
return new Phone();
case "home":
return new Home();
case "computer":
return new Computer();
case "book":
return new Book();
case "food":
return new Food();
case "pant":
return new Pant();
case "shirt":
return new Shirt();
case "sweatshirt":
return new Sweatshirt();
case "dress":
return new Dress();
default:
throw new IllegalArgumentException("Invalid product
type: " + type);
}
}
}
Con esta implementación, puedes crear instancias de productos utilizando la fábrica
y pasarle el tipo de producto que deseas crear. Por ejemplo:
java
Copy code
ProductFactory factory = new ProductFactory();
// Crear un teléfono
Product phone = factory.createProduct("phone");
// Crear una camisa
Product shirt = factory.createProduct("shirt");
// Y así sucesivamente...
Este es un ejemplo básico del patrón de fábrica en Java aplicado a tu caso
específico. La fábrica manejará la creación de instancias de productos, lo que hace
que tu código sea más modular y fácil de mantener.
Función para crear imágenes a partir de una URI
Como no teníamos conocimiento de cómo agregar una imagen al catálogo desde
una URI, usamos ChatGPT para obtener la función necesaria. La razón de esto es
que la manera típica de obtener imágenes de la galería de un dispositivo
proporciona una referencia a la foto (URI) que no se alinea con el tipo del atributo de
entrada (ImageId), que es la forma más habitual de crear imágenes en Jetpack
Compose.
Prompt
Necesito que me generes una función en jetpack compose, que consiga mediante
una variable photoUri de tipo Uri que cree una imagen así como existe la función de
Image() en jetpack compose pero en vez de necesitar un painter que pueda crearla
con una Uri, además esta ha de ser clickable, añadele el modifier de clickable
también.
Resultado
AsyncImage(
model = photoUri,
contentDescription = null,
modifier = Modifier
.clickable {}
.fillMaxWidth(),
contentScale = ContentScale.Crop
)
}
Corrección de condicionales
En el método del front-end para manejar el filtrado de productos teníamos un error
en los condicionales, por lo que hemos pedido que lo corrigiese.
Prompt
Hay algun error en este código:
for (i in filteredProducts){
if(filterC && filterP){
if(i.name.contains(search) && search.trim() != "") {
val m:String = viewModel.returnCategoriesChecked()
if(i.category in m && i.price.toFloatOrNull()!! >=
minPrice && i.price.toFloatOrNull()!! <= maxPrice){
ProductoItem(
viewModel = viewModel,
navControler = navControler,
nombre = i.name,
uri = i.uri,
precio = i.price,
descripcion = i.description,
cat = i.category
)}}
}else if(filterC && !filterP && i.name.contains(search) &&
search.trim() != ""){
val m:String = viewModel.returnCategoriesChecked()
if(i.category in m){
ProductoItem(
viewModel = viewModel,
navControler = navControler,
nombre = i.name,
uri = i.uri,
precio = i.price,
descripcion = i.description,
cat = i.category
)}
}else if(filterC && !filterP && i.name.contains(search) &&
search.trim() != ""){
if(i.price.toFloatOrNull()!! >= minPrice &&
i.price.toFloatOrNull()!! <= maxPrice) {
ProductoItem(
viewModel = viewModel,
navControler = navControler,
nombre = i.name,
uri = i.uri,
precio = i.price,
descripcion = i.description,
cat = i.category
)}
}
else if(!filterC && !filterP && i.name.contains(search) &&
search.trim() != ""){
ProductoItem(
viewModel = viewModel,
navControler = navControler,
nombre = i.name,
uri = i.uri,
precio = i.price,
descripcion = i.description,
cat = i.category
)
}
else{
ProductoItem(
viewModel = viewModel,
navControler = navControler,
nombre = i.name,
uri = i.uri,
precio = i.price,
descripcion = i.description,
cat = i.category
)
}
Resultado
El código parece tener redundancias y potenciales problemas lógicos. Por ejemplo,
tienes un bloque else if duplicado para el caso donde filterC es verdadero y filterP es
falso. Además, hay una serie de condiciones que están siendo verificadas
repetidamente en cada bloque if, lo que podría simplificarse.
Aquí hay una versión simplificada y corregida del código:
kotlin
Copy code
for (i in filteredProducts) {
if (i.name.contains(search) && search.trim() != "") {
val m: String = viewModel.returnCategoriesChecked()
val price = i.price.toFloatOrNull()
if (price != null && price >= minPrice && price <= maxPrice)
{
if (filterC) {
if (filterP && i.category in m) {
ProductoItem(
viewModel = viewModel,
navControler = navControler,
nombre = i.name,
uri = i.uri,
precio = i.price,
descripcion = i.description,
cat = i.category
)
} else if (!filterP && i.category in m) {
ProductoItem(
viewModel = viewModel,
navControler = navControler,
nombre = i.name,
uri = i.uri,
precio = i.price,
descripcion = i.description,
cat = i.category
)
}
} else {
ProductoItem(
viewModel = viewModel,
navControler = navControler,
nombre = i.name,
uri = i.uri,
precio = i.price,
descripcion = i.description,
cat = i.category
)
}
}
}
}
En esta versión:
Se verifica la condición de name.contains(search) && search.trim() != "" solo una vez
antes de realizar más verificaciones.
Se verifica si price es válido (toFloatOrNull() puede devolver null) antes de realizar
cualquier operación en él.
Se elimina el bloque else if duplicado y se maneja dentro del bloque principal.
Se simplifican las verificaciones dentro de los bloques if.
Cuestión sobre el manejo de diferentes respuestas API
Uno de los desafíos principales que hemos enfrentado está relacionado con la
estructura de las respuestas de la API. Dado que hay diversos productos con
atributos distintos, la composición del cuerpo de la respuesta varía. En
consecuencia, el modelo de datos al que debemos transformar la respuesta también
cambia en función del producto consultado.
Prompt
Como manejarías el hecho de convertir las respuestas API a un modelo de dato en
una situación donde la estructura cambie, por ejemplo, en el caso que hayan
productos con diferentes atributos.
Respuesta
Si las categorías de productos tienen diferentes atributos, tendrás que manejar esta
variabilidad en tu diseño de datos y en la forma en que procesas la respuesta JSON
en tu aplicación frontend. Aquí tienes algunas opciones:
Clases de datos específicas por categoría: Puedes crear diferentes clases de datos
para cada categoría de productos si tienen atributos diferentes. Por ejemplo, podrías
tener una clase ClothingProduct con los atributos específicos de ropa y una clase
TechnologyProduct con los atributos específicos de tecnología. Luego, en tu
aplicación frontend, tendrás que determinar la categoría de cada producto en la
respuesta JSON y utilizar la clase de datos correspondiente para deserializarlo.
Clase de datos genérica con atributos opcionales: Puedes definir una clase de datos
genérica que incluya todos los atributos posibles para todos los tipos de productos,
pero que sean opcionales. Por ejemplo:
kotlin
Copy code
data class Product(
val name: String,
val price: Double,
val category: String,
val size: String? = null, // Atributo específico de
ropa
val brand: String? = null, // Atributo específico de
tecnología
// Otros atributos opcionales...
)
En este enfoque, los atributos específicos de cada categoría se declaran como
opcionales y pueden ser nulos si no están presentes en el JSON.
Uso de un mapa de atributos adicionales: Puedes utilizar un mapa para almacenar
atributos adicionales que son específicos de ciertas categorías de productos. Por
ejemplo:
kotlin
Copy code
data class Product(
val name: String,
val price: Double,
val category: String,
val attributes: Map<String, Any>? = null // Mapa de
atributos adicionales
)
En este enfoque, los atributos específicos de cada categoría se almacenan en un
mapa de atributos adicionales. Esto te permite manejar cualquier variabilidad en los
atributos de forma flexible.
Cada enfoque tiene sus ventajas y desventajas, y la elección dependerá de tus
necesidades específicas y de la complejidad de tu aplicación. En cualquier caso, es
importante asegurarse de manejar correctamente la variabilidad en los atributos de
las diferentes categorías de productos para que tu aplicación frontend pueda
procesar la respuesta JSON de manera adecuada.