Skip to content

Latest commit

 

History

History
319 lines (218 loc) · 18.4 KB

File metadata and controls

319 lines (218 loc) · 18.4 KB

Практическая работа Модуль 2: «Интернет-магазин: корзина покупок»

Часть 1. Техническое задание

Перед тем как писать код, хороший разработчик внимательно читает ТЗ. Прочитай его целиком, прежде чем переходить к следующей части.


1.1 Описание задачи

Нам поступил заказ на разработку консольного прототипа корзины покупок для интернет-магазина.

Заказчик хочет систему, в которой можно:

  • добавлять товары в корзину (название, цена, количество);
  • если товар с таким названием уже есть в корзине — количество должно суммироваться, а не создавать дубликат позиции;
  • удалять товар из корзины по названию;
  • смотреть список всех товаров в корзине;
  • смотреть итоговое количество товаров и итоговую стоимость корзины;
  • сохранять текущую корзину в файл и загружать её обратно.

Ключевое требование заказчика: никакие некорректные данные не должны попадать в корзину. Ни в коем случае в системе не должно быть возможности создать товар с отрицательной ценой, пустым названием или дробным количеством — вне зависимости от того, откуда пришли данные: из консоли или из ранее сохранённого файла.


1.2 Бизнес-правила

  1. Название товара — непустая строка.
  2. Цена товара — число (целое или дробное), строго больше нуля.
  3. Количество товара — целое число, не меньшее нуля (ноль допустим — это означает «нет в наличии», а вот отрицательного количества быть не может).
  4. При повторном добавлении товара с уже существующим названием в корзине количество суммируется с уже имеющимся, новая отдельная позиция не создаётся.
  5. Удалить можно только тот товар, который действительно есть в корзине — попытка удалить отсутствующий товар должна завершаться понятной ошибкой, а не молча ничего не делать.
  6. Список товаров, возвращаемый наружу из корзины, не должен позволять менять содержимое корзины в обход её собственных методов (add_product/remove_product).
  7. Итоговая стоимость корзины — это не хранимое, а вычисляемое значение: она всегда должна быть согласована с фактическим содержимым корзины на текущий момент, и её невозможно случайно «забыть» пересчитать.

1.3 Требования к технологиям

Требование Детали
Язык Python 3.10+
Хранилище данных JSON-файл (data/cart.json)
Интерфейс консоль (input/print)
Сторонние библиотеки не использовать (только стандартная библиотека)

Часть 2. Задание для студента

Здесь описана точная архитектура, которую нужно реализовать. Отступать от неё без согласования с преподавателем нельзя — архитектура специально спроектирована так, чтобы закрепить пройденные темы Модуля 2 (инкапсуляция, property, дескрипторы).


2.1 Структура проекта

online_shop/
│
├── data/                          # папка создаётся автоматически при первом запуске
│   └── cart.json                  # сохранённая корзина
│
├── models/
│   ├── __init__.py
│   ├── descriptors.py             # NonEmptyString, PositiveNumber, NonNegativeInteger
│   └── product.py                 # класс Product
│
├── cart/
│   ├── __init__.py
│   └── cart.py                    # класс Cart
│
├── storage/
│   ├── __init__.py
│   └── json_storage.py            # класс JsonStorage
│
└── main.py                        # точка входа, консольное меню

2.2 Описание классов

📄 models/descriptors.py — дескрипторы валидации

В этом файле нужно реализовать три отдельных класса-дескриптора, каждый из которых инкапсулирует одну проверку и может быть переиспользован для разных атрибутов и разных классов, не будучи привязанным к конкретному полю конкретного класса.

Каждый дескриптор должен реализовывать __set_name__, __get__ и __set__ (это data descriptor — чтение и запись должны быть под контролем).

NonEmptyString

Проверяет, что присваиваемое значение — непустая строка. При нарушении — ValueError.

PositiveNumber

Проверяет, что присваиваемое значение — число (int или float), строго больше нуля. При нарушении — ValueError.

NonNegativeInteger

Проверяет, что присваиваемое значение — целое число, не меньшее нуля. При нарушении — ValueError.

Почему именно дескрипторы, а не property? Как разбиралось на Уроке 9, если бы каждая из этих трёх проверок писалась через @property/@setter отдельно для каждого поля класса Product, пришлось бы трижды повторить практически одинаковую структуру, отличающуюся только условием проверки. Дескриптор пишется один раз и переиспользуется для любого количества полей — в том числе, если магазин в будущем обрастёт новыми классами (например, классом заказа или классом скидки), эти же три дескриптора можно будет применить и там, ничего не переписывая заново.


📄 models/product.py — класс Product

Представляет товар в каталоге магазина.

Атрибуты:

Атрибут Тип Описание
name str название товара — реализовано через дескриптор NonEmptyString
price int | float цена товара — реализовано через дескриптор PositiveNumber
quantity int количество товара — реализовано через дескриптор NonNegativeInteger

Методы:

  • __init__(name, price, quantity) — присваивает три атрибута через обычное присваивание (self.name = name и так далее). Само присваивание автоматически пройдёт через дескрипторы и выполнит валидацию — отдельно писать проверки в __init__ не нужно.

  • to_dict() — возвращает словарь {"name": ..., "price": ..., "quantity": ...} для последующего сохранения в JSON.

  • from_dict(data)метод класса (альтернативный конструктор, как на Уроке 6). Принимает словарь такого же вида, что возвращает to_dict(), и создаёт из него новый объект Product через cls(...).

Важно: не пытайся обращаться к product._name, product._price или product._quantity напрямую откуда-либо, кроме как через официальные атрибуты name/price/quantity. Технически такое обращение возможно (мы разбирали на Уроке 7, что одно подчёркивание — это соглашение, а не запрет), но оно полностью обходит валидацию дескриптора, ради которой всё и было спроектировано.


📄 cart/cart.py — класс Cart

Представляет корзину покупок.

Атрибуты:

Атрибут Тип Описание
_items list[Product] protected-список товаров в корзине

Методы:

  • __init__() — создаёт пустой список _items.

  • add_product(product) — добавляет товар:

    • если товар с таким же name уже есть в корзине — увеличивает его quantity на quantity добавляемого товара (то есть суммирует количество, а не создаёт вторую позицию);
    • если такого товара ещё нет — добавляет переданный объект Product в список.
  • remove_product(name) — удаляет товар из корзины по названию. Если товара с таким названием нет — выбрасывает ValueError.

  • get_products() — возвращает копию списка _items (через list(self._items)), а не сам список, — чтобы внешний код не мог изменить содержимое корзины в обход add_product/remove_product.

  • _find_by_name(name) — protected вспомогательный метод, возвращающий объект Product с заданным именем или None, если такого нет. Используется внутри add_product и remove_product, чтобы не дублировать поиск.

Свойства (@property):

  • total_priceтолько для чтения. Возвращает сумму price * quantity по всем товарам корзины. Значение не хранится, а вычисляется заново при каждом обращении — точно так же, как вычислялась площадь в примере с классом Circle на Уроке 8.

  • total_itemsтолько для чтения. Возвращает сумму quantity по всем товарам корзины.


📄 storage/json_storage.py — класс JsonStorage

Отвечает за сохранение и загрузку корзины в JSON-файл.

Атрибуты:

Атрибут Тип Описание
_filepath str путь к JSON-файлу

Методы:

  • __init__(filepath) — сохраняет путь. Если папка, в которой должен лежать файл, не существует — создаёт её. Если сам файл не существует — создаёт его с содержимым {"items": []}.

  • _read() — protected-метод. Читает и возвращает содержимое JSON-файла в виде словаря.

  • _write(data) — protected-метод. Записывает переданный словарь в JSON-файл.

  • save_cart(cart) — сохраняет содержимое переданной корзины в файл: получает список товаров через cart.get_products(), преобразует каждый товар в словарь через product.to_dict(), записывает результат в формате {"items": [...]}.

  • load_cart(cart) — читает файл, восстанавливает каждый товар через Product.from_dict(...) и добавляет его в переданную корзину через cart.add_product(...) (то есть если корзина уже что-то содержала — количества корректно суммируются тем же механизмом, что и при обычном добавлении).


2.3 Схема связей между классами

main.py
  └── создаёт Cart и JsonStorage
        ├── Cart хранит список объектов Product
        │     └── Product.name / .price / .quantity — реализованы через дескрипторы
        │           (NonEmptyString, PositiveNumber, NonNegativeInteger из models/descriptors.py)
        └── JsonStorage читает/пишет Product через to_dict() / from_dict()

2.4 Файл main.py — консольное меню

Точка входа в приложение. Здесь находится только консольный интерфейс — никакой бизнес-логики (валидация — внутри дескрипторов, работа с корзиной — внутри Cart, работа с файлом — внутри JsonStorage).

=== Интернет-магазин: корзина ===
1. Добавить товар
2. Удалить товар
3. Показать список товаров
4. Показать общую стоимость корзины
5. Сохранить корзину в файл
6. Загрузить корзину из файла
0. Выйти
# main.py — структура (не финальный код, только скелет)

from models.product import Product
from cart.cart import Cart
from storage.json_storage import JsonStorage

def main():
    cart = Cart()
    storage = JsonStorage("data/cart.json")

    while True:
        # показать меню
        # обработать выбор пользователя
        # вызвать методы cart.* / storage.*, обернув создание Product в try/except ValueError
        pass

if __name__ == "__main__":
    main()

Часть 3. Пример использования (примерная реализация main.py)

Конкретная реализация всегда зависит от разработчика.

from models.product import Product
from cart.cart import Cart
from storage.json_storage import JsonStorage


def show_menu():
    print("\n=== Интернет-магазин: корзина ===")
    print("1. Добавить товар")
    print("2. Удалить товар")
    print("3. Показать список товаров")
    print("4. Показать общую стоимость корзины")
    print("5. Сохранить корзину в файл")
    print("6. Загрузить корзину из файла")
    print("0. Выйти")
    return input("Ваш выбор: ").strip()


def main():
    cart = Cart()
    storage = JsonStorage("data/cart.json")

    while True:
        choice = show_menu()

        if choice == "1":
            name = input("Название товара: ").strip()
            try:
                price = float(input("Цена: "))
                quantity = int(input("Количество: "))
                product = Product(name, price, quantity)
                cart.add_product(product)
                print(f"Товар '{name}' добавлен в корзину")
            except ValueError as e:
                print(f"Ошибка: {e}")

        elif choice == "2":
            name = input("Название товара для удаления: ").strip()
            try:
                cart.remove_product(name)
                print(f"Товар '{name}' удалён из корзины")
            except ValueError as e:
                print(f"Ошибка: {e}")

        elif choice == "3":
            products = cart.get_products()
            if not products:
                print("Корзина пуста")
            for product in products:
                print(f"{product.name}: {product.price:.2f} руб. x {product.quantity} шт.")

        elif choice == "4":
            print(f"Итого товаров: {cart.total_items}")
            print(f"Итоговая стоимость: {cart.total_price:.2f} руб.")

        elif choice == "5":
            storage.save_cart(cart)
            print("Корзина сохранена в файл")

        elif choice == "6":
            storage.load_cart(cart)
            print("Корзина загружена из файла")

        elif choice == "0":
            print("Работа программы завершена")
            break

        else:
            print("Неверный выбор.")


if __name__ == "__main__":
    main()

Ориентировочное время выполнения: 2–3 академических часа.