DEV Community

Cover image for A Go Drawback Lesson: Exploring OOP in GoLang in greater depth.
Iheanacho Ebere
Iheanacho Ebere

Posted on

A Go Drawback Lesson: Exploring OOP in GoLang in greater depth.

My quest to study Golang began in an alt-school Africa. They provided an exam to gauge how well we understood the essential OOP concept of Golang through a number of lectures and instructions. We were asked to offer a solution to the inventory systems using the OOP paradigm; the significance of an inventory cannot be overstated. This is as a result of its many advantages and social significance.

In this article, I'd be discussing OOP in Goland and how I was able to provide some solutions to the inventory system.

Prerequisites
In order to follow through this article:

  • You need to have a basic understanding of programming.
  • You need to have a basic understanding of Go programming concepts such as variables, data types, structures, maps, interfaces and packages.

The Assessment question:

John has just opened up his car selling shop, to sell different cars. He gets the cars he needs to sell from different people and they all bring it to him.
He needs to manage the list of cars he has, attach a price to them, and put them on display to be sold, basically John needs an inventory to manage cars & to manage sales. For instance,

He needs to see the number of cars that are left to be sold
He needs to see the sum of the prices of the cars left
He needs to see the number of cars he has sold
He needs to see the sum total of the prices of cars he has sold
He needs to see a list of orders for the sales he made
Using the knowledge of OOP in Go, Build simple classes for the following “objects”:

Car
Product
Store
The Car class can have any car attributes you can think of.

The Product class should have attributes of a product i.e (the product, quantity of the product in stock, price of the product). A car is a product of the store, but there can be other products so the attribute of the car can be promoted to the Product. The Product class should have methods to display a product, and a method to display the status of a product if it is still in stock or not.

The Store class should have attributes like

Number of products in the store that are still up for sale
Adding an Item to the store
Listing all product items in the store
Sell an item
Show a list of sold items and the total price

This assessment made me realize how unprepared I was for the most fundamental and crucial core notion of GoLang as well as programming in general,known as OOP(Object-Oriented Programming).

What is OOP?
I think of OOP as a programming architecture that is heavily based on the ideas of classes and objects. This grants us flexibilty of creating reusable pieces of code blueprints (usually called classes), which are used to create individual instances of objects. We can easily recognize some classes using this insight and the data from the assessment, such as:

Car: Represents a specific car in the store which is also product, this means that it has all the attributes and methods of the Product struct.

Product: Represents a product in the store including cars called Embedding, this means the cars class also its inherits attributes from Product Class.

Initial Implementation

type Car struct {
    name   string
    engine string
    model  string
}

type Product struct {
    Car
    Name     string
    quantity int
    price    float32
}
Enter fullscreen mode Exit fullscreen mode

Store: Represents a store that sells product, including cars. Has a list of products in stock and a list of sold products.

package store

import (
    "Auto-shop/products"
    "fmt"
)

type Store struct {
    name         string
    productStore []products.Product
    soldProducts []products.Product
}

Enter fullscreen mode Exit fullscreen mode

It is undeniable that Go is not an object-oriented programming language. Can we, however, use go language features to adhere to the principles of object-oriented methodologies? Yes.

However, the key idea behind object-orientation is to separate the code into several manageable parts or objects. Each object has its own identity that is determined by the data (attributes) and the methods (behavior) that operate on the data. The attributes are typically hidden and can only be accessed through the methods defined within the object. This is called encapsulation.

This led us to construct a receiver function(methods) for our classes.

Product class methods:


func (p *Product) Display() string {
    return p.Name
}

func (p *Product) Quantity() int {
    return p.quantity
}

func (p *Product) Status() bool {
    if p.quantity <= 0 {
        fmt.Println("Product Out of Stock")
        return false
    }
    fmt.Printf("Quantity of Prduct in Stock: %v", p.quantity)
    return true
}

func (p *Product) Sell(quantity int) error {
    if p.quantity < quantity {
        return errors.New("not enough products to sell")
    }

    p.quantity = p.quantity - quantity

    return nil
}

func (p *Product) Price() int {
    return p.price
}
Enter fullscreen mode Exit fullscreen mode

Store class methods:

func (s *Store) AddProduct(p products.Product) {
    s.productStore = append(s.productStore, p)
}

func (s *Store) ListItemsCount() int {
    fmt.Printf("list items count: %+v \n\n", s.productStore)
    var count int
    for _, v := range s.productStore {
        count += v.Quantity()
    }
    return count
}

func (s *Store) ListItems() {
    if len(s.productStore) > 0 {
        for _, v := range s.productStore {
            fmt.Printf("list items: %+v \n\n", v.Display())
        }
    } else {
        fmt.Println("No products in the store atm.")
    }
}

func (s *Store) ItemsTotalPrice() int {
    fmt.Printf("items total price:%+v \n\n", s.productStore)
    var price int
    for _, v := range s.productStore {
        price += v.Quantity() * v.Price()
    }
    return price
}

func (s *Store) SellItems(productName string, quantity int) {
    var product products.Product

    for _, v := range s.productStore {
        if v.Name == productName {
            product = v
            if quantity <= v.Quantity() {
                product.Sell(quantity)
                s.soldProducts = append(s.soldProducts, product)
                fmt.Printf("You just sold %d amount of %s. /n The remaining %s in the store is %d", quantity, productName, productName, v.Quantity())
            } else {
                fmt.Printf("Insufficient Product in stock")
            }

        } else {
            fmt.Printf("%s is not in store", productName)
        }
    }
}

func (s *Store) ShowSoldItemsCount() {
    var sumNumberSold int
    for _, v := range s.soldProducts {
        sumNumberSold += v.Quantity()
    }
    fmt.Printf("total number of items sold: %v /n", sumNumberSold)
}

func (s *Store) ShowSoldItems() {
    fmt.Printf("sold items: %+v \n\n", s.soldProducts)
}

func (s *Store) ShowSoldItemsPrice() {
    var sumAmountSold int
    for _, v := range s.soldProducts {
        sumAmountSold += v.Price() * v.Quantity()
    }
    fmt.Printf("total sold items price: %v \n\n", sumAmountSold)
}
Enter fullscreen mode Exit fullscreen mode

Final Implementation

Thus, after delving deeper into the vast world of GoLang and attempting to comprehend more advanced concepts. I discovered that I had already broken the encapsulation rule in previous projects, which prompted me to completely refactor my code.

Unlike Java or C++, where we have private or public to restrict the accessibility of attributes, Go has no such features. Instead, what it provides is a means for package-level accessibility i.e attributes starting with uppercase can be accessed directly outside the package when the object is created. Note how all my previous attributes started with Uppercase which is wrong and a rookie mistake!.

This helped me understand one of the true applications of interfaces. Interfaces also introduces a new concept in Go known as polymorphism. It provides the ability to write code through the implementation of types that can take on different behavior at runtime. In Go, polymorphism is achieved through interfaces and interfaces only.

var _ Product = &product{}

type Product interface {
    Quantity() int
    Price() float32
    Name() string

    Sell(quantity int) error
    Display()
    Status() bool
}

type car struct {
    name  string
    color string
}

type product struct {
    car
    productName string
    quantity    int
    price       float32
}
Enter fullscreen mode Exit fullscreen mode

The code above provides us with some sense of encapsulation, granting limited access to the attributes through interface. I also implemented this same concept on the store class while using product interface from the product package.

package store

import (
    "backendexam-project/product"
    "errors"
    "fmt"
)


type Store interface {
    AddItem(item product.Product)
}

type store struct {
    name      string
    Products  []product.Product //list of products
    SoldItems []product.Product
}
Enter fullscreen mode Exit fullscreen mode

Overall, the implementation of the Car, Product, and Store classes in Go using object-oriented programming principles allows us to manage the inventory and sales of a car and other product store efficiently. The initial implementation had some encapsulation and abstraction concerns with all of the data/attributes that were exposed. These issues were resolved by implementing interfaces efficiently.

GitHub Url: https://github.com/manlikeNacho/Auto-shop.git

In conclusion, the process of modifying the code to enhance its design after completing the second-semester exam question at AltSchool Africa has allowed us to better grasp OOP in Go and put best practices for creating scalable and maintainable systems into practice. Through this technical essay, we want to share what we've learned while also serving as a helpful resource for anyone else working through a similar issue.

Resources
The resources below might be helpful

Top comments (0)