# programacion-orientada-a-objetos-en-python
Programación Orientada a Objetos en Python
Volver a la portada del cursoPlanificación del proyecto final
Sección 6 · Proyecto Final
Planificación del proyecto final
El proyecto final reúne todo el curso en un solo sistema: PyShop, la tienda en línea que has construido por partes. Antes de escribir la primera clase conviene planear: qué debe hacer el sistema, qué clase se encarga de cada cosa y en qué orden se construye. Un buen plan evita el error más caro de la POO: descubrir a la mitad que una responsabilidad quedó en la clase equivocada.
Requisitos de PyShop
- La tienda registra productos en un catálogo y los encuentra por SKU, un número que se asigna solo desde 100.
- Hay productos físicos, con peso y stock, y productos digitales, que nunca se agotan.
- El precio siempre es mayor que cero; el stock solo cambia al reponer o al vender.
- Cada tipo de producto calcula su envío: 49 + 15 × peso × cantidad para los físicos y 0 para los digitales.
- Un cliente arma un carrito, puede aplicar un descuento (porcentaje o monto fijo con compra mínima) y confirma el pedido.
- Confirmar es atómico: o se surte el pedido completo o no se toca el stock.
- Los errores son específicos y se pueden atrapar todos con una sola excepción raíz.
Responsabilidades por clase
La regla práctica: cada dato vive en la clase que mejor lo conoce, y cada clase hace una sola cosa bien.
Producto(abstracta): nombre, precio validado con@property, stock de solo lectura, SKU,reponer,retirarysubtotal. Declaracosto_enviocomo método abstracto.ProductoFisicoyProductoDigital: solo lo que las distingue: el peso, el cálculo del envío y si el stock se agota.Descuento(abstracta): un contrato conaplicar(subtotal), que devuelve el monto a descontar.DescuentoPorcentajeyDescuentoFijolo implementan.Linea: un producto y una cantidad, inmutable (@dataclass(frozen=True)).Carrito: tiene líneas y, opcionalmente, un descuento. Calcula subtotal, envío y total, y confirma el pedido. No pregunta de qué tipo es cada producto: delega en sus métodos.Tienda: el catálogo (diccionario SKU → producto),registrarybuscar.- Excepciones:
ErrorTiendacomo raíz;PrecioInvalidoError,StockInsuficienteErroryProductoNoEncontradoErrordebajo.
Diagrama de clases
Una flecha <|-- indica herencia («es un»); un rombo <>--, composición («tiene»).
Producto (ABC) <|-- ProductoFisico (peso_kg)
<|-- ProductoDigital (sin stock real)
Descuento (ABC) <|-- DescuentoPorcentaje
<|-- DescuentoFijo
Tienda <>-- 0..* Producto (catálogo por SKU)
Carrito <>-- 0..* Linea (lista de líneas)
Carrito --> 0..1 Descuento
Linea --> 1 Producto
ErrorTienda (Exception)
<|-- PrecioInvalidoError (+ ValueError)
<|-- StockInsuficienteError (faltante)
<|-- ProductoNoEncontradoError (+ KeyError)
Orden de construcción
Se construye de abajo hacia arriba: primero lo que no depende de nada y al final lo que usa todo lo demás. Prueba cada pieza antes de pasar a la siguiente.
- Excepciones. No dependen de nada y las usan todas las demás clases.
Productoy sus subclases. Comprueba el setter del precio, el stock de solo lectura y el envío de cada tipo.- Descuentos. Son independientes del carrito; se prueban con un subtotal suelto.
LineayCarrito. Primero los cálculos y al finalconfirmar.Tienda. El catálogo y la búsqueda con su error propio.- Un pedido de punta a punta que use todas las clases juntas.
Probar con assert
No hace falta ningún framework para empezar: assert condición no hace nada si la condición es verdadera y lanza AssertionError si es falsa. Escribe las pruebas con cifras que puedas calcular a mano. Con 2 tazas de 149, 1 teclado de 899 y 1 ebook de 299, el subtotal es 1496 y el envío 61 + 67 + 0 = 128:
car = Carrito("Ana")
car.agregar(taza, 2)
car.agregar(teclado, 1)
car.agregar(ebook, 1)
assert len(car) == 4
assert car.subtotal() == 1496.0
assert car.total() == 1624.0
try:
taza.precio = -10
except PrecioInvalidoError:
pass
else:
raise AssertionError("aceptó un precio negativo")
assert taza.precio == 149.0
Las pruebas de errores tienen dos partes: que el error ocurra y que el objeto quede intacto. Esta segunda parte es la que revela si confirmar es realmente atómico.
Dinero: float, Decimal o centavos
Los float son binarios y no representan exactamente 0.1: en Python, 0.1 + 0.2 da 0.30000000000000004. En una tienda, esos errores se acumulan en totales que no cuadran por un centavo. Hay dos soluciones estándar:
from decimal import Decimal
print(Decimal("0.1") + Decimal("0.2")) # 0.3
centavos = 2 * 14900 + 89900 + 29900
print(centavos) # 149600
decimal.Decimal hace aritmética decimal exacta; créalo siempre desde texto, porque Decimal(0.1) hereda el error del float. La alternativa es guardar centavos como enteros y dividir entre 100 solo al mostrar. La solución de referencia usa float con round(..., 2) para mantener el código corto, y el cambio a Decimal queda como extra del proyecto.
Trampas comunes
- Validar el stock línea por línea: si el mismo producto aparece en dos líneas, cada una pasa la validación por separado. Suma primero las cantidades por producto.
- Descontar stock mientras se valida: si falla el tercer producto, los dos primeros ya se descontaron.
- Llenar el carrito de
if isinstance(...): si necesitas saber el tipo, falta un método polimórfico. - Asignar
self._preciodirectamente en__init__: se salta la validación del setter.
Con este plan, cada clase tiene un propósito claro y cada paso se puede probar por separado. El siguiente paso es construirlo.