[Перевод] Эволюция кода: путь к лучшему дизайну
В этой статье мы изучим программное решение экзаменационной задачи конца второго семестра в AltSchool Africa. Эта задача подразумевает построение системы для управления складскими запасами магазина, продающего машины и другие товары. В частности, магазин должен иметь возможность отслеживать количество и общую стоимость проданных и оставшихся на складе товаров.
Изначальное решение этой задачи было реализовано с помощью объектно-ориентированного подхода на Go и включало классы для следующих «объектов»: Car
, Product
, Store
. Класс Car
представлял конкретную машину в магазине, а класс Product
— некий товар, включая машины. В свою очередь, класс Store
представлял сам магазин, включая список товаров на складе и список проданных товаров.
Однако после оценки этого решения нас попросили отрефакторить его код, чтобы устранить некоторые проблемы и улучшить общую структуру. Далее мы разберём исходное решение и его отрефакторенную версию, попутно обсудив внесённые изменения и их причины.
▍ Задача
Джон только что открыл магазин по продаже машин. Он присвоил каждому автомобилю ценник и выставил их на продажу. Теперь ему нужно вести складской учёт, чтобы контролировать проданные и оставшиеся товары. Например, ему необходимо видеть:
- Число машин на складе.
- Общую стоимость машин на складе.
- Количество проданных машин.
- Сумму выручки от проданных машин.
- Список реализованных заказов.
С помощью ООП-принципов на Go нужно создать простые классы для следующих «объектов»:
- Car
- Product
- Store
Класс Car
может содержать любые атрибуты автомобиля.
Класс Product
должен содержать атрибуты товара, то есть его наименование, количество на складе и стоимость. Машина является товаром, но в магазине есть и другие товары, поэтому атрибуты машины можно возвести к Product
. Класс Product
должен содержать методы для отображения товара и его статуса — на складе либо продан.
В классе Store
должны присутствовать следующие атрибуты и методы:
- Число товаров на продаже.
- Добавление элемента товара в магазин.
- Вывод списка всех элементов товаров в магазине.
- Продажа товара.
- Вывод списка проданных товаров и их общей суммы.
Это не приложение для интерфейса командной строки или интернета. Идея в том, чтобы продемонстрировать процесс решения задачи, используя знания, полученные на занятиях. Преподавателю необходимо увидеть, как ты мыслишь в роли программиста, и предложенную реализацию он будет оценивать, анализируя одну строку за другой.
▍ Мой изначальный ход мысли и реализация
На языке Go необходимо реализовать классы для управления товарными запасами и продажами магазина. Его владельцу нужно управлять списком автомобилей, которым он присвоил ценники, отслеживая продажи и остатки. Для этого мы создаём следующие классы:
Car
: представляет конкретную машину в магазине. Содержит полеProduct
, которое означает, что в нём есть все атрибуты и методы структурыProduct
.Product
: представляет товар в магазине, включая машины. Содержит атрибуты товара, такие как его наименование (Name
), остаток на складе (Quantity
) и цена (Price
). Реализует интерфейсProductInterface
, определяющий методы, которые должен реализовыватьProduct
.Store
: представляет магазин по продаже товаров, включая машины. Содержит список товаров на складе и список проданных товаров. Реализует интерфейсStoreInterface
, определяющий методы, которые должен реализовыватьStore
.
Вот моя первая реализация:
package main
import "fmt"
// Класс Product должен иметь атрибуты товара (то есть наименование, количество на складе и цену)
type Product struct {
Name string
Quantity int
Price float64
}
// Car. Машина является лишь одним из товаров, то есть в магазине могут быть и другие, поэтому её атрибут также относится к Product.
type Car struct {
Product
}
// ProductInterface Класс Product должен содержать методы для отображения товара и его статуса – продан или на складе.
type ProductInterface interface {
DisplayProduct()
DisplayStatus()
}
// Класс Store должен содержать:
// функцию DisplayProduct для отображения товара.
func (p Product) DisplayProduct() {
fmt.Printf("Product: %s", p.Name)
}
// функцию DisplayStatus для отображения статуса товара.
func (p Product) DisplayStatus() {
if p.Quantity > 0 {
fmt.Println("In stock")
} else {
fmt.Println("Out of stock")
}
}
// Класс Store должен содержать следующие атрибуты и методы: количество товаров в магазине, добавление товара, вывод списка всех элементов товаров, продажа элемента, вывод списка проданных товаров и их общей суммы.
type Store struct {
Product []ProductInterface
soldProduct []ProductInterface
}
// StoreInterface. Класс Store должен содержать методы для добавления товара, вывода всех товаров, продажи товара и вывода списка проданных товаров.
type StoreInterface interface {
AddProduct(ProductInterface)
ListProducts()
SellProduct(string)
ListSoldProducts()
}
// AddProduct. Класс Store должен содержать методы для добавления товара, вывода всех товаров, продажи товара и вывода списка проданных товаров.
func (s *Store) AddProduct(p ProductInterface) {
s.Product = append(s.Product, p)
}
// ListProducts. Класс Store должен содержать методы для добавления товара, вывода всех товаров, продажи товара и вывода списка проданных товаров.
func (s *Store) ListProducts() {
for _, p := range s.Product {
p.DisplayProduct()
}
}
// SellProduct. Класс Store должен содержать методы для добавления товара, вывода всех товаров, продажи товара и вывода списка проданных товаров.
func (s *Store) SellProduct(name string) {
// Перебор товаров магазина.
for i, p := range s.Product {
// Если товар найден, он удаляется из магазина и добавляется в срез проданных товаров.
if p.(Car).Name == name {
s.soldProduct = append(s.soldProduct, p)
s.Product = append(s.Product[:i], s.Product[i+1:]...)
}
}
}
// ListSoldProducts. Класс Store должен содержать методы для добавления товара, вывода всех товаров, продажи товара и вывода списка проданных товаров.
func (s *Store) ListSoldProducts() {
for _, p := range s.soldProduct {
p.DisplayProduct()
}
}
func main() {
//Создание магазина.
store := Store{}
//Создание товара.
car := Car{Product{Name: "Toyota", Quantity: 10, Price: 100000}}
//Добавление товара в магазин.
store.AddProduct(car)
//Продажа товара.
store.SellProduct("Toyota")
//Вывод списка всех товаров магазина.
store.ListProducts()
//Вывод всех проданных товаров.
store.ListSoldProducts()
}
▍ Спустя два месяца
В изначальной реализации были некоторые проблемы, требующие решения. Одна из них заключалась в том, что методы структуры Product
содержали получателей указателей и получателей значений, что документацией Go не рекомендуется. Кроме того, структура Car
содержала поле Product
, но оно не было определено как тип.
Чтобы исправить эти проблемы, мы изменили реализацию так:
- определили структуру
Car
как содержащую полеProduct
. Это значит, что у неё есть все атрибуты и методы структурыProduct
. - определили структуру
Product
как реализующую интерфейсProductInterface
, определяющий методы, которые должен реализовыватьProduct
. - определили структуру
Store
как реализующую интерфейсStoreInterface
, определяющий методы, которые должен реализовыватьStore
. - изменили методы структуры
Product
, чтобы они содержали только получателей указателей.
После этих изменений реализация стала корректной и завершённой. Класс Store
можно использовать для управления товарным учётом и продажами, а классы Product
и Car
— для представления и управления товарами и машинами.
▍ Текущий ход мысли и реализация
Далее приводится итоговая реализация, но сначала я хочу перестроить вопрос, оттолкнувшись от решения. Процесс создания классов для объектов Car
, Product
и Store
можно реализовать следующими этапами:
1. Определить требования для каждого класса:
В класс Car
нужно внести атрибуты, описывающие конкретную машину, а именно её Make
, Model
и Year
.
В класс Product
нужно включить атрибуты, описывающие товар, а именно его Name
, Quantity
и Price
. Этот класс также должен содержать методы для отображения товара и его статуса (продан или на складе).
В класс Store
нужно добавить атрибуты, отслеживающие количество товаров на складе, список товаров на складе и список проданных товаров. Этот класс также должен содержать методы для добавления товара, вывода списка товаров, продажи товара и вывода списка проданных товаров.
2. Реализовать классы на Go:
Здесь мы начнём с реализации класса Product
, который будет использоваться классами Car
и Store
. Он будет иметь атрибуты Name
, Quantity
и Price
, а также методы DisplayProduct()
и DisplayStatus()
.
Далее реализуем класс Car
, имеющий тип Product
. Этот класс будет содержать атрибуты, наследуемые от класса Product
, а также дополнительные, относящиеся конкретно к автомобилю, то есть Make
, Model
и Year
.
Наконец, реализуем класс Store
, который будет содержать атрибуты SoldProducts
и Products
для отслеживания проданных товаров и товаров на складе соответственно. В нём также будут присутствовать методы AddProduct()
, ListProducts()
, SellProduct()
и ListSoldProducts()
.
3. Протестировать реализацию на предмет соответствия требованиям.
Для проверки реализации мы создадим экземпляры каждого класса и будем использовать методы для выполнения различных задач, таких как добавление товаров, их продажа и вывод списков. Помимо этого, мы добавим обработку ошибок для сценариев, в которых товар оказывается не найден или отсутствует на складе.
package main
import "fmt"
// Класс Car представляет конкретную машину.
type Car struct {
Make string
Model string
Year int
Product
}
// Класс Product представляет товар в магазине, включая машины.
// Он содержит атрибуты товара, такие как его name, quantity и price.
type Product struct {
Name string
Quantity int
Price float64
}
// Интерфейс ProductInterface определяет методы, которые должен реализовывать Product.
type ProductInterface interface {
DisplayProduct()
DisplayStatus()
UpdateQuantity(int) error
}
// DisplayProduct – это метод класса Product, отображающий информацию о товаре.
func (p Product) DisplayProduct() {
fmt.Printf("Product: %s\n", p.Name)
fmt.Printf("Quantity: %d\n", p.Quantity)
fmt.Printf("Price: $%.2f\n", p.Price)
}
// DisplayStatus – это метод класса Product, отображающий статус товара (продан или на складе).
func (p Product) DisplayStatus() {
if p.Quantity > 0 {
fmt.Println("Status: In stock")
} else {
fmt.Println("Status: Out of stock")
}
}
// UpdateQuantity – это метод класса Product, обновляющий количество товара на складе.
func (p Product) UpdateQuantity(quantity int) error {
if p.Quantity+quantity < 0 {
return fmt.Errorf("cannot set quantity to a negative value")
}
p.Quantity += quantity
return nil
}
// Класс Store представляет магазин, продающий товары, в том числе машины.
// Он содержит список товаров на складе и список проданных товаров.
type Store struct {
Products []ProductInterface
SoldProducts []ProductInterface
}
// Интерфейс StoreInterface определяет методы, которые должен реализовывать Store.
type StoreInterface interface {
AddProduct(ProductInterface)
ListProducts()
SellProduct(string) error
ListSoldProducts()
SearchProduct(string) ProductInterface
}
// AddProduct – это метод класса Store, добавляющий товар в список товаров на складе.
func (s *Store) AddProduct(p ProductInterface) {
s.Products = append(s.Products, p)
}
// ListProducts – это метод класса Store, выводящий список всех товаров на складе.
func (s *Store) ListProducts() {
for _, p := range s.Products {
p.DisplayProduct()
p.DisplayStatus()
fmt.Println()
}
}
// SellProduct – это метод класса Store, продающий товар из списка товаров на складе и добавляющий его в список проданных товаров.
func (s *Store) SellProduct(name string) error {
// Поиск товара в списке товаров на складе.
product := s.SearchProduct(name)
if product == nil {
return fmt.Errorf("product not found")
}
//Утверждение, что товар имеет тип Product.
p, ok := product.(*Product)
if !ok {
return fmt.Errorf("product is not a Product type")
}
// Проверяет, достаточно ли на складе товара для его продажи.
if p.Quantity < 1 {
return fmt.Errorf("product is out of stock")
}
// Удаляет товар из списка товаров на складе и добавляет его в список проданных товаров.
for i, p := range s.Products {
// Утверждает, что переменная p имеет тип Product.
p, ok := p.(*Product)
if !ok {
return fmt.Errorf("product has wrong type")
}
if p.Name == name {
s.SoldProducts = append(s.SoldProducts, p)
s.Products = append(s.Products[:i], s.Products[i+1:]...)
break
}
}
// Обновляет количество товаров на складе.
err := product.UpdateQuantity(-1)
if err != nil {
return err
}
return nil
}
// ListSoldProducts – это метод класса Store, выводящий список всех проданных товаров.
func (s *Store) ListSoldProducts() {
for _, p := range s.SoldProducts {
p.DisplayProduct()
fmt.Println()
}
}
// SearchProduct – это метод класса Store, ищущий товар с заданным наименованием в списке товаров на складе.
// При обнаружении этого товара он его возвращает. В противном случае возвращается нуль.
func (s *Store) SearchProduct(name string) ProductInterface {
for _, p := range s.Products {
// Утверждает, что переменная p имеет тип Product.
p, ok := p.(*Product)
if !ok {
return nil
}
if p.Name == name {
return p
}
}
return nil
}
func main() {
// Создаёт новый магазин.
store := &Store{}
// Добавляет в магазин машины.
store.AddProduct(&Car{Make: "Toyota", Model: "Camry", Year: 2020, Product: Product{Name: "Toyota Camry", Quantity: 3, Price: 30000}})
store.AddProduct(&Car{Make: "Honda", Model: "Accord", Year: 2021, Product: Product{Name: "Honda Accord", Quantity: 5, Price: 35000}})
store.AddProduct(&Car{Make: "Ford", Model: "Mustang", Year: 2019, Product: Product{Name: "Ford Mustang", Quantity: 2, Price: 40000}})
// Выводит список товаров в магазине.
fmt.Println("Products in stock:")
store.ListProducts()
fmt.Println()
// Продаёт машину из магазина.
err := store.SellProduct("Toyota Camry")
if err != nil {
fmt.Println(err)
}
// Снова выводит список товаров.
fmt.Println("\nProducts in stock:")
store.ListProducts()
fmt.Println()
// Выводит проданные товары.
fmt.Println("\nSold products:")
store.ListSoldProducts()
}
▍ Обобщение
Перед нами стояла задача создать классы для управления магазином по продаже автомобилей с помощью принципов ООП на Go.
Магазин должен управлять списком доступных для продажи автомобилей, присваивать им цены и показывать покупателям. Он также должен отслеживать количество проданных машин, общую выручку и список выполненных заказов.
Для решения этой задачи мы определили три класса: Car
, Product
и Store
. Класс Car
представляет конкретный автомобиль и может содержать любые выбранные нами атрибуты. Класс Product
представляет товар в магазине, включая машины. Он содержит атрибуты товара, такие как Name
, Quantity
и Price
. Кроме того, этот класс содержит методы для отображения товара и его статуса (продан либо на складе).
Класс Store
представляет сам магазин и содержит такие атрибуты, как количество проданных и оставшихся товаров, а также методы для добавления товара, вывода списка всех товаров, продажи товара и вывода списка проданных товаров.
В изначальной реализации классы Product
и Car
были определены верно, а вот с классом Store
были проблемы. Во-первых, срез Product
в структуре Store
был определён как срез значений Product
при том, что должен был быть определён как срез значений ProductInterface
, поскольку класс Product
не реализует интерфейс ProductInterface
. Это вызывало ошибку компиляции при попытке использовать метод SellProduct
, так как значения Product
в срезе Product
не содержали необходимых методов.
Ещё одна проблема базовой реализации заключалась в методе SellProduct
класса Store
. В нём значение продаваемого Product
удалялось из среза Product
, но в срез soldProduct
не добавлялось. В результате метод ListSoldProducts
всегда возвращал пустой срез.
Для исправления этих недочётов мы изменили класс Store
, чтобы определить срез Product
как срез значений ProductInterface
, а также добавили в метод SellProduct
строчку кода для внесения проданного Product
в срез soldProduct
. Кроме того, мы добавили в этот метод обработку ошибок для случаев, когда товар не был найден или отсутствовал на складе.
Напоследок скажу, что процесс решения этого экзамена с последующим рефакторингом кода для улучшения его структуры позволил нам углубить своё понимание ООП в Go и применить лучшие практики для создания обслуживаемых и масштабируемых систем. В этой статье мы поделились своим опытом и, надеюсь, предоставили полезную информацию для других людей, решающих подобные задачи.
Играй в нашу новую игру прямо в Telegram!