В этой статье мы изучим программное решение экзаменационной задачи конца второго семестра в AltSchool Africa. Эта задача подразумевает построение системы для управления складскими запасами магазина, продающего машины и другие товары. В частности, магазин должен иметь возможность отслеживать количество и общую стоимость проданных и оставшихся на складе товаров.

Изначальное решение этой задачи было реализовано с помощью объектно-ориентированного подхода на Go и включало классы для следующих «объектов»: Car, Product, Store. Класс Car представлял конкретную машину в магазине, а класс Product – некий товар, включая машины. В свою очередь, класс Store представлял сам магазин, включая список товаров на складе и список проданных товаров.

Однако после оценки этого решения нас попросили отрефакторить его код, чтобы устранить некоторые проблемы и улучшить общую структуру. Далее мы разберём исходное решение и его отрефакторенную версию, попутно обсудив внесённые изменения и их причины.

▍ Задача


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

  1. Число машин на складе.
  2. Общую стоимость машин на складе.
  3. Количество проданных машин.
  4. Сумму выручки от проданных машин.
  5. Список реализованных заказов.

С помощью ООП-принципов на 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!

Комментарии (10)


  1. SH33011
    13.01.2023 16:50
    +1

    Изначальное решение этой задачи было реализовано с помощью объектно-ориентированного подхода на Go

    Разве в Go есть классы?


    1. DollyPapper
      13.01.2023 23:30
      +1

      ООП про объекты, а не про классы. Т.е. про штуки которые могут делать некоторые дела по запросу, и сохранять целостным свое внутреннее состояние. Можно сказать, что в Go есть объекты.


  1. vadimr
    13.01.2023 16:50
    +4

    Конечно, это Африка, но я бы за такое преподавание увольнял. Какой смысл в наличии класса Car в этой задаче? Магазину настолько не всё равно, чем торговать, что надо автомобиль зашить в код приложения? А если Джон захочет продать резину, придётся менять код?

    В данном случае внедряется распространённый антипаттерн ООП, согласно которому иерархия классов повторяет иерархию предметов, различаемых нами в окружающем мире. В то время как классы проектируются, исходя из удобства кодирования.


    1. avshkol
      13.01.2023 17:40
      +4

      Потому что автомобили - основной товар, а шампунь и коврики для них - вспомогательный.

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


      1. vadimr
        13.01.2023 17:45

        type Car struct {

        Product
        }

        (однако, почему-то,

        if p.(Car).Name == name

        ).

        Да и вряд ли в реальной жизни вы найдёте такой типизирующий магазин.


    1. MentalBlood
      13.01.2023 20:21
      +1

      Классы — часть ПО. Прикладное ПО проектируется в первую очередь исходя из удобства использования, потому что оно прикладное, а не рекреационное


      1. vadimr
        13.01.2023 21:31

        Удобство использования ПО никоим образом не зависит от внутреннего устройства его кода.


        1. MentalBlood
          13.01.2023 21:56

          Удобство использования библиотеки сильно зависит от иерархии классов, доступных пользователю


    1. funca
      13.01.2023 20:38
      +4

      антипаттерн ООП, согласно которому иерархия классов повторяет иерархию предметов, различаемых нами в окружающем мире. В то время как классы проектируются, исходя из удобства кодирования.

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

      Тема интересная, я пожалуй подпишусь на комменты. :)


  1. yellow79
    14.01.2023 12:04
    +1

    Что-то тут не сходится. Магазин должен содержать список товаров, что вроде как соответствует начальному заданию. Но добавляете в магазин вы почему-то машину, которая товаром не является, а лишь содержит его. Не машина должна содержать товар, а товарная единица должна в себе иметь машину, что в нынешних реалиях go легко реализуется с помощью дженериков