En la versión defectuosa la clase Mobile exponía atributos públicos (pub) como brand, model, price, stock y banderas (has_extra_warranty, has_screen_protector). Esto permitía que cualquier parte del código leyera o modificara directamente el estado del objeto sin control ni validación, generando:
- Violación del principio de encapsulamiento.
- Riesgo de inconsistencias (por ejemplo,
pricenegativo ostocknegativo). - Dificultad para auditar cambios y añadir reglas de negocio (p. ej. validaciones).
- Dificultad para migrar a una implementación que necesite invariantes más complejos.
// -------------------------
// CellShop - Código Defectuoso (ANTES)
// Lenguaje: Rust
// Propósito: ejemplificar 10 malas prácticas pedidas en la actividad
// -------------------------
// 1) Atributos públicos en Mobile (no encapsulado)
pub struct Mobile {
pub brand: String,
pub model: String,
pub price: f64,
pub stock: i32,
// característica añadida directamente (no decorator)
pub has_extra_warranty: bool,
pub has_screen_protector: bool,
}
impl Mobile {
// 5) Mobile se instancia directamente y sin fábrica
pub fn new(brand: &str, model: &str, price: f64, stock: i32) -> Self {
Mobile {
brand: brand.to_string(),
model: model.to_string(),
price,
stock,
has_extra_warranty: false,
has_screen_protector: false,
}
}
// 2) Método largo y confuso: procesa venta, revisa promociones, factura, actualiza inventario
pub fn process_sale(&mut self, qty: i32, customer_loyalty: bool, coupon: Option<&str>) -> String {
// Validaciones básicas
if qty <= 0 {
return "Cantidad inválida".to_string();
}
if self.stock < qty {
return "Stock insuficiente".to_string();
}
// 4 & 10) Lógica rígida de promociones (codificada por marca y encadenada aquí)
// - Si la marca es \"BrandX\" aplica 10%
// - Si hay loyalty aplica 5% adicional
// - Si coupon == \"BLACKFRIDAY\" reemplaza por 25%
let mut discount = 0.0;
if self.brand == "BrandX" {
discount += 0.10;
}
if customer_loyalty {
discount += 0.05;
}
if let Some(code) = coupon {
if code == "BLACKFRIDAY" {
discount = 0.25; // sobreescribe otras reglas
} else if code == "WELCOME10" {
discount += 0.10;
}
}
// 3 & 9) InventoryAndBilling todo junto (mezcla inventario y facturación)
// 9) Facturación acoplada a implementación concreta (imprime directamente)
let subtotal = self.price * (qty as f64);
let total = subtotal * (1.0 - discount);
// 7) Cambios de inventario no notifican a nadie (no observer)
self.stock -= qty;
// 6) StoreManager (puede haber múltiples instancias)
// (ejemplo de uso más abajo)
// 8) Añadir característica: modifico el móvil directamente (no decorator)
if coupon == Some("ADD_WARRANTY") {
self.has_extra_warranty = true; // cambia estado del objeto
}
// Imprimir factura (acoplamiento a salida)
println!("--- FACTURA ---");
println!("Producto: {} {} x{}", self.brand, self.model, qty);
println!("Subtotal: {:.2}", subtotal);
println!("Descuento: {:.2}%", discount * 100.0);
println!("Total a pagar: {:.2}", total);
println!("--------------");
// 10) Lógica de promociones embebida y secuencial (difícil de componer)
// Retorno simple
format!("Venta procesada. Total: {:.2}", total)
}
}
// 3) Clase mezclada InventoryAndBilling (SRP violado)
// maneja inventario y facturación a la vez.
pub struct InventoryAndBilling {
pub items: Vec<Mobile>, // 1) campos públicos
}
impl InventoryAndBilling {
pub fn new() -> Self {
InventoryAndBilling { items: Vec::new() }
}
pub fn add_mobile(&mut self, mobile: Mobile) {
self.items.push(mobile);
}
// Busca por marca y modelo - uso directo de strings (acoplamiento a implementación)
pub fn find_mobile_mut(&mut self, brand: &str, model: &str) -> Option<&mut Mobile> {
for m in &mut self.items {
if m.brand == brand && m.model == model {
return Some(m);
}
}
None
}
// 2) Método que orquesta venta delegando a Mobile::process_sale (pero también factura)
pub fn sell(&mut self, brand: &str, model: &str, qty: i32, loyalty: bool, coupon: Option<&str>) -> String {
match self.find_mobile_mut(brand, model) {
Some(mobile) => {
// Actualiza inventario y genera \"factura\" imprimiendo
let result = mobile.process_sale(qty, loyalty, coupon);
// aquí podríamos además grabar en base de datos, enviar emails, etc.
result
}
None => "Producto no encontrado".to_string(),
}
}
}
// 6) StoreManager con múltiples instancias posibles (no es Singleton)
pub struct StoreManager {
pub name: String,
// referencia directa a la implementación concreta InventoryAndBilling (acoplamiento)
pub inventory_billing: InventoryAndBilling,
}
impl StoreManager {
pub fn new(name: &str) -> Self {
StoreManager {
name: name.to_string(),
inventory_billing: InventoryAndBilling::new(),
}
}
pub fn report_stock(&self) {
println!("Reporte de stock por el manager {}", self.name);
for item in &self.inventory_billing.items {
println!("{} {} - stock: {}", item.brand, item.model, item.stock);
}
}
// Método que no notifica a otros módulos (no Observer)
pub fn restock(&mut self, brand: &str, model: &str, qty: i32) {
for m in &mut self.inventory_billing.items {
if m.brand == brand && m.model == model {
m.stock += qty;
}
}
}
}
fn main() {
// CREACIÓN DIRECTA DE OBJETOS (no factory) - Problema 5
let mut samsung_a = Mobile::new("BrandX", "A100", 299.99, 10);
let mut iphone = Mobile::new("BrandY", "IPhoneMini", 699.99, 5);
// 3) InventoryAndBilling y StoreManager acoplados
let mut inv_bill = InventoryAndBilling::new();
inv_bill.add_mobile(samsung_a);
inv_bill.add_mobile(iphone);
// 6) Se crean múltiples managers (no singleton) — ejemplo de mala práctica
let mut manager_juan = StoreManager::new("Juan");
manager_juan.inventory_billing = inv_bill; // movimiento directo (propiedad pública)
// Se crea otro manager sin compartir estado correctamente (posible inconsistencia)
let mut manager_maria = StoreManager::new("Maria");
// manager_maria tiene su propio InventoryAndBilling vacío -> inconsistencias reales
// 2 & 10) Venta: todo embebido, promociones rígidas, facturación dentro del proceso
let sale_result = manager_juan.inventory_billing.sell("BrandX", "A100", 2, true, Some("WELCOME10"));
println!("Resultado venta: {}", sale_result);
// 7) Cambio de inventario (restock) no notifica ni dispara eventos
manager_juan.restock("BrandX", "A100", 5);
manager_juan.report_stock();
// 8) Añadir característica fija a Mobile (se usa campo booleano en la entidad)
// en lugar de usar decorator o composición.
if let Some(m) = manager_juan.inventory_billing.find_mobile_mut("BrandX", "A100") {
m.has_screen_protector = true; // modificación directa
}
// 4) Promoción por marca rígida: difícil de cambiar sin tocar process_sale
let sale_two = manager_juan.inventory_billing.sell("BrandY", "IPhoneMini", 1, false, Some("BLACKFRIDAY"));
println!("Resultado venta 2: {}", sale_two);
// 9) Código altamente acoplado y con lógica dispersa:
// - muchas responsabilidades en InventoryAndBilling
// - StoreManager manipula estructura interna directamente
// - no existe una fachada simple para operaciones comunes
}
| Nº | Problema detectado | Dónde se ve en el código | Recomendación / Patrón GoF sugerido |
|---|---|---|---|
| 1 | Atributos públicos en Mobile | Mobile con campos pub |
Encapsular con getters/setters o private + métodos; buenas prácticas OOP |
| 2 | process_sale largo y confuso |
Mobile::process_sale |
Aplicar SRP y extraer responsabilidades a métodos/objetos (Billing, Promotion) |
| 3 | InventoryAndBilling mezcla inventario y facturación |
InventoryAndBilling |
Separar en Inventory y Billing (SRP) |
| 4 | Promotion rígida por marca | Lógica de descuentos en process_sale |
Strategy Pattern para promociones configurables |
| 5 | Mobile se instancia directamente | Mobile::new(...) en main |
Usar Factory Method o una Factory para creación de móviles |
| 6 | Múltiples instancias de StoreManager | manager_juan, manager_maria |
Singleton (o inyección de dependencia/shared state) si se necesita instancia única |
| 7 | Cambios de inventario no notifican a otros módulos | restock y process_sale |
Observer Pattern para notificar inventario, UI, logs, etc. |
| 8 | Añadir características fijas al Mobile | campos has_extra_warranty, has_screen_protector |
Decorator Pattern para añadir funcionalidades sin modificar la clase |
| 9 | Clases fuertemente acopladas a implementaciones concretas | StoreManager.inventory_billing manipulado directamente |
Facade Pattern para simplificar API y desacoplar consumidores |
| 10 | Lógica de promociones embebida en la venta | Promociones y chaining en process_sale |
Chain of Responsibility para componer promociones y reglas secuenciales |
// src/main.rs
// CellShop - Refactor Punto 1: Encapsular atributos en Mobile
// Autor: Alvarado Cardona Antonio 22210279
// Comentarios en español explicando la decisión y el propósito.
use std::fmt;
// La estructura Mobile ahora tiene campos privados (por defecto en Rust).
// Acceso y mutación controlados por métodos públicos -> encapsulamiento.
pub struct Mobile {
brand: String,
model: String,
price: f64,
stock: i32,
// características que antes eran públicas; ahora privadas gestionadas por métodos
extra_warranty: bool,
screen_protector: bool,
}
impl Mobile {
/// Constructor público que valida valores básicos.
pub fn new(brand: impl Into<String>, model: impl Into<String>, price: f64, stock: i32) -> Self {
let price = if price < 0.0 { 0.0 } else { price };
let stock = if stock < 0 { 0 } else { stock };
Mobile {
brand: brand.into(),
model: model.into(),
price,
stock,
extra_warranty: false,
screen_protector: false,
}
}
// ----------------------------
// Getters (solo lectura)
// ----------------------------
pub fn brand(&self) -> &str {
&self.brand
}
pub fn model(&self) -> &str {
&self.model
}
pub fn price(&self) -> f64 {
self.price
}
pub fn stock(&self) -> i32 {
self.stock
}
pub fn has_extra_warranty(&self) -> bool {
self.extra_warranty
}
pub fn has_screen_protector(&self) -> bool {
self.screen_protector
}
// ----------------------------
// Setters / mutators controlados
// ----------------------------
/// Actualiza el precio si es válido (no negativo).
pub fn set_price(&mut self, new_price: f64) -> Result<(), String> {
if new_price < 0.0 {
Err("El precio no puede ser negativo".into())
} else {
self.price = new_price;
Ok(())
}
}
/// Ajusta el stock (puede sumar o restar). No permite stock negativo.
pub fn adjust_stock(&mut self, delta: i32) -> Result<(), String> {
let new_stock = self.stock + delta;
if new_stock < 0 {
Err("No se puede reducir el stock por debajo de 0".into())
} else {
self.stock = new_stock;
Ok(())
}
}
/// Marca que el móvil tiene garantía extra (ej.: al vender un add-on).
pub fn enable_extra_warranty(&mut self) {
self.extra_warranty = true;
}
/// Remueve la garantía extra.
pub fn disable_extra_warranty(&mut self) {
self.extra_warranty = false;
}
/// Marca que el móvil tiene protector de pantalla.
pub fn set_screen_protector(&mut self, enabled: bool) {
self.screen_protector = enabled;
}
// ----------------------------
// Operaciones relacionadas (ejemplos)
// ----------------------------
/// Intenta vender una cantidad; devuelve el total calculado o error.
/// Nota: aquí solo se gestiona stock/total; la facturación real debería estar en otra clase (SRP).
pub fn sell(&mut self, qty: i32) -> Result<f64, String> {
if qty <= 0 {
return Err("Cantidad a vender debe ser mayor que 0".into());
}
if self.stock < qty {
return Err("Stock insuficiente".into());
}
let total = (self.price) * (qty as f64);
// disminuir stock de forma controlada
self.adjust_stock(-qty)?;
Ok(total)
}
}
// Implementamos Display para facilitar logging/printings controlados
impl fmt::Display for Mobile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} {} - Precio: {:.2} - Stock: {}{}{}",
self.brand,
self.model,
self.price,
self.stock,
if self.extra_warranty { " - Garantía Extra" } else { "" },
if self.screen_protector { " - Protector" } else { "" }
)
}
}
fn main() {
// Ejemplo de uso del Mobile refactorizado
let mut m = Mobile::new("BrandX", "A100", 299.99, 10);
println!("Móvil creado: {}", m);
// Lectura vía getters
println!("Brand: {}, Model: {}", m.brand(), m.model());
println!("Precio actual: {:.2}", m.price());
println!("Stock actual: {}", m.stock());
// Intento de cambiar precio válido
match m.set_price(279.99) {
Ok(_) => println!("Precio actualizado a {:.2}", m.price()),
Err(e) => println!("Error al actualizar precio: {}", e),
}
// Venta de 2 unidades
match m.sell(2) {
Ok(total) => println!("Venta procesada. Total: {:.2}", total),
Err(e) => println!("Error en venta: {}", e),
}
println!("Estado final: {}", m);
}
// ----------------------------
// Tests unitarios para verificar el encapsulamiento/validaciones
// ----------------------------
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create_mobile_and_getters() {
let m = Mobile::new("BrandY", "ModelZ", 100.0, 5);
assert_eq!(m.brand(), "BrandY");
assert_eq!(m.model(), "ModelZ");
assert_eq!(m.price(), 100.0);
assert_eq!(m.stock(), 5);
assert!(!m.has_extra_warranty());
assert!(!m.has_screen_protector());
}
#[test]
fn set_price_negative_fails() {
let mut m = Mobile::new("B", "M", 50.0, 1);
assert!(m.set_price(-10.0).is_err());
assert_eq!(m.price(), 50.0); // no cambió
}
#[test]
fn adjust_stock_and_sell() {
let mut m = Mobile::new("B", "M", 20.0, 3);
assert!(m.adjust_stock(2).is_ok());
assert_eq!(m.stock(), 5);
// vender 4 -> OK
let total = m.sell(4).unwrap();
assert_eq!(total, 80.0);
assert_eq!(m.stock(), 1);
// vender más que stock -> error
assert!(m.sell(2).is_err());
}
#[test]
fn warranty_and_protector_flags() {
let mut m = Mobile::new("B", "M", 10.0, 1);
assert!(!m.has_extra_warranty());
m.enable_extra_warranty();
assert!(m.has_extra_warranty());
assert!(!m.has_screen_protector());
m.set_screen_protector(true);
assert!(m.has_screen_protector());
}
}
- Los campos de
Mobileson privados (visibilidad por defecto en Rust). - Se han añadido getters para exponer información de solo lectura:
brand(),model(),price(),stock(),has_extra_warranty(),has_screen_protector().
- Se han añadido mutadores controlados (setters/operaciones) con validaciones:
set_price(new_price)— rechaza precios negativos.adjust_stock(delta)— evita stock negativo.enable_extra_warranty()/disable_extra_warranty()set_screen_protector(enabled)
- Se añadió un método
sell(qty)que gestiona la lógica mínima de venta (validaciones de cantidad y stock) y actualiza el stock a través deadjust_stock. - Se implementó
Displaypara obtener una representación segura/controlada del objeto. - Se añadieron tests unitarios que validan las propiedades y las restricciones.
Nota: el método
selles una implementación mínima para el ejemplo. En la refactorización completa deberías separar facturación y ventas en clases distintas para respetar SRP.
- Encapsulamiento: el estado interno ya no puede ser alterado arbitrariamente; todos los cambios pasan por métodos que pueden validar e imponer invariantes.
- Seguridad: se evita que
priceostockadopten valores inválidos. - Facilita pruebas: los tests pueden verificar comportamiento observado a través de la API pública en lugar de tocar campos internos.
- Facilita futuras refactorizaciones: si luego se necesita persistir cambios, emitir eventos o añadir reglas, basta con ampliar los métodos en vez de buscar todos los accesos directos a campos públicos.
- Prepara terreno para patrones posteriores: con
Mobileencapsulado es más fácil introducir Decorator, Factory o Strategy porque la API pública es estable y controlada.