Загрузка данных


Практическая работа №6#
Объектно-ориентированное программирование#
Тема: Основы ООП. Классы, инкапсуляция, наследование и полиморфизм
Язык: Java
Формат: консольное приложение
Сложность: средняя
Цель: разработать объектно-ориентированную систему управления заказами интернет-магазина.

1. Постановка задачи#
Необходимо разработать консольное приложение для управления заказами небольшого интернет-магазина.

Система должна позволять добавлять товары, формировать корзину покупателя, оформлять заказы и обрабатывать оплату различными способами.

Приложение должно быть спроектировано с использованием принципов объектно-ориентированного программирования. Основной акцент необходимо сделать на правильном разделении ответственности между классами, инкапсуляции данных и использовании полиморфизма.

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

2. Основные требования#
2.1. Класс Product#
Реализуйте класс Product, описывающий товар интернет-магазина.

Класс должен содержать следующие поля:

id - уникальный идентификатор товара;
name - название товара;
priceInCents - стоимость товара в копейках;
stock — количество товара на складе.
Требования:

Все поля должны быть закрыты модификатором private.
Для чтения данных должны быть предусмотрены необходимые геттеры.
Цена не может быть отрицательной.
Остаток товара на складе не может быть отрицательным.
Название товара не может быть пустым.
Изменение количества товара должно выполняться через отдельные методы, а не через прямой доступ к полю.
Идентификатор не должен меняться после инициализации.
Реализуйте методы:

increaseStock(int quantity) - увеличить остаток товара;
decreaseStock(int quantity) - уменьшить остаток, если товара достаточно;
isAvailable(int quantity) - проверить наличие нужного количества товара;
getFormattedPrice() - вернуть стоимость в удобном для пользователя формате.
2.2. Класс Customer#
Реализуйте класс Customer, описывающий покупателя.

Поля:

id - уникальный идентификатор;
name - имя покупателя;
email - адрес электронной почты.
Требования:

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

2.3. Класс CartItem#
Реализуйте класс CartItem, представляющий товар в корзине.

Поля:

product - ссылка на объект Product;
quantity - количество единиц товара.
Реализуйте методы:

getSubtotal() - вычислить стоимость позиции;
increaseQuantity(int quantity) - увеличить количество;
decreaseQuantity(int quantity) - уменьшить количество.
Количество товара в позиции должно быть положительным. Стоимость позиции рассчитывается на основе количества и цены товара.

2.4. Класс ShoppingCart#
Реализуйте класс ShoppingCart, представляющий корзину покупателя.

Корзина должна содержать коллекцию объектов CartItem.

Реализуйте следующие методы:

addProduct(Product product, int quantity) - добавить товар в корзину;
removeProduct(int productId) - удалить товар из корзины;
changeQuantity(int productId, int quantity) — изменить количество товара;
getTotal() - вычислить общую стоимость корзины;
getItems() - получить список товаров;
clear() - очистить корзину;
printCart() - вывести содержимое корзины в консоль.
При добавлении товара необходимо проверять его наличие на складе. Если такой товар уже есть в корзине, следует увеличить количество в существующей позиции, а не создавать дубликат.

Корзина не должна позволять добавлять нулевое или отрицательное количество товара. Если товара на складе недостаточно, операция должна быть отклонена с понятным сообщением.

2.5. Класс OrderItem#
Реализуйте класс OrderItem, представляющий отдельную позицию оформленного заказа.

Поля:

productId - идентификатор товара;
productName - название товара на момент оформления;
priceInCents - цена товара на момент оформления;
quantity - заказанное количество.
При создании позиции заказа необходимо сохранить название и стоимость товара на момент оформления. Последующее изменение цены товара в каталоге не должно влиять на уже оформленный заказ.

Реализуйте метод getSubtotal() для вычисления стоимости позиции.

2.6. Класс Order#
Реализуйте класс Order, представляющий оформленный заказ.

Поля:

id - идентификатор заказа;
customer - покупатель;
items - список позиций заказа;
totalInCents - итоговая стоимость;
status - состояние заказа.
Предусмотрите следующие состояния:

NEW - новый заказ;
PAID - заказ оплачен;
CANCELLED - заказ отменён.
Реализуйте методы:

getTotal() - получить общую стоимость;
getItems() - получить позиции заказа;
getStatus() - получить статус;
markAsPaid() - отметить заказ оплаченным;
cancel() - отменить заказ;
printOrder() - вывести полную информацию о заказе.
После оформления заказа его позиции и итоговая стоимость не должны произвольно изменяться. Оплаченный заказ нельзя отменить обычным методом cancel().

2.7. Абстрактный класс PaymentMethod#
Для обработки оплаты необходимо реализовать абстрактный класс PaymentMethod.

Он должен содержать:

абстрактный метод pay(Order order), выполняющий оплату;
обычный метод printPaymentInfo(Order order), выводящий информацию об оплате.
Метод pay() должен возвращать результат операции, например boolean, показывающий, прошла ли оплата успешно.

Создайте три класса-наследника:

CardPayment

Оплата банковской картой.
Выводит сообщение о попытке оплаты картой.
При успешной оплате возвращает true.
CashPayment

Оплата наличными.
Выводит сообщение о принятии наличных.
При успешной оплате возвращает true.
OnlinePayment

Оплата через интернет.
Выводит сообщение об обработке онлайн-платежа.
При успешной оплате возвращает true.
В рамках работы реальное взаимодействие с банками и платёжными системами не требуется. Достаточно имитировать процесс оплаты через сообщения в консоли.

Каждый способ оплаты должен иметь собственную реализацию метода pay().

2.8. Класс OrderService#
Реализуйте класс OrderService, отвечающий за оформление и обработку заказов.

Он должен содержать методы:

createOrder(Customer customer, ShoppingCart cart) - создать заказ на основе содержимого корзины;
processPayment(Order order, PaymentMethod paymentMethod) - обработать оплату;
cancelOrder(Order order) - отменить заказ с учётом его текущего состояния.
При оформлении заказа необходимо:

Проверить, что корзина не пуста.
Повторно проверить наличие нужного количества каждого товара на складе.
Создать позиции заказа, сохранив название и цену каждого товара.
Рассчитать итоговую стоимость.
Списать заказанное количество товаров со склада.
Создать заказ со статусом NEW.
Очистить корзину после успешного оформления.
Если на любом этапе обнаружится недостаток товара, заказ не должен быть оформлен, а остатки товаров не должны измениться.

При обработке оплаты:

Проверить, что заказ находится в статусе NEW.
Вызвать метод pay() переданного способа оплаты.
Если оплата успешна, изменить статус заказа на PAID.
Если оплата не удалась, оставить заказ в статусе NEW.
При отмене неоплаченного заказа необходимо изменить его статус на CANCELLED и вернуть товары на склад. Повторная отмена не должна приводить к повторному возврату товаров.

3. Демонстрация полиморфизма#
В методе main() необходимо продемонстрировать работу полиморфизма.

Создайте несколько объектов различных способов оплаты:

java
PaymentMethod card = new CardPayment();
PaymentMethod cash = new CashPayment();
PaymentMethod online = new OnlinePayment();
Создайте несколько заказов и обработайте их разными способами оплаты через один и тот же метод processPayment().

Также создайте коллекцию:

java
List<PaymentMethod> paymentMethods = new ArrayList<>();

paymentMethods.add(new CardPayment());
paymentMethods.add(new CashPayment());
paymentMethods.add(new OnlinePayment());
Организуйте цикл, который вызывает метод pay() для каждого способа оплаты на подходящем новом заказе.

В коде не должно быть необходимости проверять тип объекта с помощью instanceof или выполнять явное приведение типов для выбора способа оплаты.

4. Демонстрация работы программы#
В методе main() необходимо продемонстрировать полный сценарий работы интернет-магазина.

Создать не менее пяти товаров с различными ценами и остатками на складе.
Создать не менее двух покупателей.
Создать корзины для покупателей.
Добавить в корзины несколько товаров, в том числе один и тот же товар в разных количествах.
Вывести содержимое корзин и их стоимость.
Изменить количество одного из товаров в корзине и удалить другую позицию.
Оформить заказы для обоих покупателей.
Оплатить один заказ банковской картой, а второй - онлайн.
Вывести информацию об оформленных заказах и их статусах.
Продемонстрировать отмену неоплаченного заказа и возврат товаров на склад.
Вывести обновлённые остатки товаров.
5. Дополнительные требования#
При реализации программы необходимо соблюдать следующие условия:

Не использовать public-поля для хранения состояния объектов.
Не размещать всю бизнес-логику в методе main().
Не дублировать одинаковую логику в нескольких классах без необходимости.
Использовать конструкторы для первоначальной инициализации объектов.
Применять private и предоставлять доступ к полям только через методы.
Использовать наследование там, где оно отражает отношение «является».
Использовать переопределение методов с аннотацией @Override.
Применять коллекции Java, например ArrayList.
Разделять ответственность между сущностями и сервисным классом.
Не использовать instanceof для определения способа оплаты.
Использовать final при необходимости.
ВАЖНО#
При расчёте денежных сумм необходимо избегать float и double. Используйте целое число копеек (long или int).

А зачем?#
Деньги рекомендуется хранить в целых числах, например в копейках, чтобы избежать ошибок округления, возникающих при использовании типов с плавающей точкой (float и double).

Проблема заключается в том, что float и double хранят числа в двоичном формате IEEE 754. Некоторые десятичные дроби невозможно точно представить в двоичной системе.

Пример проблемы#
Допустим, мы используем double для хранения стоимости товара:

java
double price = 0.1;
double total = price + price + price;

System.out.println(total);
Можно ожидать, что результат будет 0.3, однако на практике Java выведет примерно:


0.30000000000000004
Это происходит потому, что 0.1 не имеет точного конечного представления в двоичной системе счисления. В памяти хранится ближайшее представимое значение.

Если таких операций много, погрешности могут накапливаться. При расчёте стоимости заказов, скидок, налогов и балансов это может приводить к расхождениям. Да, в практической это некритично, но я хочу, чтобы вы узнали об этом нюансе. Так что да.

Пример решения#
Хранить стоимость как количество копеек:

java
long price = 100050 // 1000 рублей и 50 копеек
IO.println((double) price / 100) // для вывода можно уже преобразовать
И небольшой лайфхак: большие числа для упрощения чтения можно разделять нижним подчёркиванием:

java
long price = 1000_50; 
Нижнее подчёркивание тут не влияет ни на что. Чисто визуальная штука, чтобы число можно было читать удобнее. В данном случае, явно отделяем рубли и копейки. После компиляции число всё равно будет представлено как 100050.

6. Проверка корректности#
Программа должна корректно обрабатывать следующие ситуации:

Попытка добавить в корзину товар, которого нет на складе.
Попытка добавить отрицательное количество товара.
Попытка оформить пустую корзину.
Попытка оформить заказ, когда товара на складе уже недостаточно.
Попытка оплатить уже оплаченный заказ.
Попытка отменить уже отменённый заказ.
Попытка отменить оплаченный заказ.
Попытка изменить цену или количество товара некорректным значением.
Повторное добавление одного товара в корзину.
Изменение цены товара после оформления заказа.
Для каждой ситуации необходимо предусмотреть корректную обработку: отклонение операции, информативное сообщение или исключение, если оно обосновано.

7. Дополнительное задание (по желанию)#
Добавьте систему скидок.

Создайте абстрактный класс Discount, содержащий метод calculateDiscount(long totalInCents).

Реализуйте наследников:

PercentageDiscount - скидка в процентах.
FixedDiscount - скидка на фиксированную сумму.
NoDiscount - отсутствие скидки.
Добавьте возможность применять скидку к заказу до его оформления. Итоговая стоимость не должна становиться отрицательной.

Продемонстрируйте полиморфизм при использовании разных видов скидок, не изменяя основную логику OrderService для каждого нового вида скидки.