Implementing OOP Concepts in Golang

Implementing OOP Concepts in Golang

A Comprehensive Guide to Object-Oriented Programming (OOP) in Golang

ยท

5 min read

Golang is not purely an object-oriented programming language, but it has features that make OOP possible.
In this article, we will look at OOP in Golang by building an auto-shop project. The project is described in this document: https://docs.google.com/document/d/1HVQ97RLeGzIxdAt_rEv-Az99DqqzF6w_C5OR3zKDuFo/edit.

First things first, how do we represent a real-life entity in Golang? To represent an entity in Golang, we can use a Golang type called the "struct" type. This type allows us to have multiple fields, meaning we can describe entities. For instance, the following is how we can define a car.

type Car struct {
    ID    string
    Brand string
    Model string
    Color string
}

The above creates the skeleton of the entity, but a real-life entity can also perform actions. To do this, we will leverage Golang receiver functions. For example, to add movement functionality to our car entity created previously, we can create a receiver function named "Move."

func (c Car) Move() {
    fmt.Printf("Car %s is driving\n", c.ID)
}

But this is not all. We have to look at one more topic before diving into our project, which is Golang interfaces. To demonstrate this, let us create an "airplane" entity.

type Airplane struct {
    ID string
    Name string
    SeatingCapacity int
}

Now that we have this, we can do the same thing we did above for the "Car" type to add movement functionality. Both airplanes and cars are vehicles, and they can both move; this is where interfaces come in. We can create a vehicle type without having to do the same steps of creating a struct type and receiver functions as done previously. Instead, we will create an interface type.

type Vehicle interface {
    Move()
}

What this means is that any struct type that has a "Move" receiver function is a "Vehicle" type, which definitely makes it easy to group or classify entities.

Now let's start with our project. From the requirements in the previously-described document, we know a shop must include products, so let us start by implementing the product entity.

type Product struct {
    ID         string
    Item       ProductItem
    Price      float64
    QtyInStock int
}

func (p *Product) DisplayStatus() {
    m := "not in stock"
    if p.IsAvailable() {
        m = "in stock"
    }

    fmt.Printf("Product is %s. Amount left is %d\n", m, p.QtyInStock)
}

func (p *Product) IsAvailable() bool {
    return p.QtyInStock > 0
}

Based on our requirement, a product can be any type of automobile, which is why our product entity references a "ProductItem" type. This "ProductItem" type will be an interface type, as it can be any type of automobile.

type ProductItem interface {
    Details()
    GetID() string
    GetName() string
}

func (p *Product) Display() {
    fmt.Printf("Product ID: %s\nPrice: %f\nQtyInStock: %d\n", p.ID, p.Price, p.QtyInStock) // render price and qty
    p.Item.Details()                                                                       // render the item details
}

With this type in place, any entity that implements the "ProductItem" interface can be sold at the shop. Let's create our first product item, which will be a car ๐Ÿ™‚.

type Car struct {
    ID    string
    Brand string
    Model string
    Color string
}

func (c *Car) GetID() string {
    return c.ID
}

func (c *Car) GetName() string {
    return fmt.Sprintf("%s-%s (%s)", c.Brand, c.Model, c.Color)
}

func (c *Car) Details() {
    fmt.Printf("Item ID: %s\nCar Brand: %s\nCar Model: %s\nCar Color: %s\n", c.ID, c.Brand, c.Model, c.Color)
}

With this in place, we now have our first product item.

Before we proceed to create our store, we need to create an order entity. This entity is what we will use to record orders made to the shop.

type Order struct {
    Product   *Product
    SoldPrice float64
}

We added the "SoldPrice" field to the order entity because, in the real world, prices change due to the foreign exchange market. This field will allow us effectively calculate cash flows.

Finally, let's create a store entity for our shop; this is where all the shop operations occur.

type Store struct {
    ID     string
    Stock  map[string]Product
    Orders []Order
}

The reason we are using a map to store the products in stock as opposed to a slice is for improved performance when searching for a product. Now, let's start adding functionality to our store.

  1. Adding items to the store.
func (s *Store) AddItem(item ProductItem, price float64) {
    // check if there are existing product with that item
    itemId := item.GetID()
    if p, e := s.Stock[itemId]; e {
        p.QtyInStock++  // increment quantity
        p.Price = price // update price if needed
        s.Stock[itemId] = p
    } else {
        s.Stock[itemId] = Product{
            ID:         GenerateID(),
            Item:       item,
            QtyInStock: 1,
            Price:      price,
        }
    }
}
  1. List items in the store.
func (s *Store) GetItems() []ProductItem {
    var items []ProductItem
    for _, v := range s.Stock {
        items = append(items, v.Item)
    }

    return items
}

func (s *Store) DisplayItems() {
    for i, item := range s.GetItems() {
        fmt.Printf("%d: %s => %s\n", i+1, item.GetID(), item.GetName())
    }
}
  1. Sell items in the store.
func (s *Store) SellItem(item ProductItem) (bool, error) {
    itemId := item.GetID()

    if p, e := s.Stock[itemId]; e {
        if !p.IsAvailable() {
            return false, errors.New("product not in stock")
        }

        if p.QtyInStock == 1 {
            // remove item from stock completely
            delete(s.Stock, itemId)
        } else {
            p.QtyInStock--
            s.Stock[itemId] = p
        }

        // add item to orders
        s.Orders = append(s.Orders, Order{&p, p.Price})
        return true, nil
    }

    return false, errors.New("product not in stock")
}
  1. Display list of sold items and total amount made.
func (s *Store) GetOutflow() ([]Order, float64) {
    totalPrice := float64(0)
    for _, o := range s.Orders {
        totalPrice += o.SoldPrice
    }

    return s.Orders, totalPrice
}

// DisplayOutflow - Display's list of sold items and total price
func (s *Store) DisplayOutflow() {
    fmt.Println("List of sold items and total price")

    orders, totalPrice := s.GetOutflow()
    for i, o := range orders {
        fmt.Printf("%d: %s => %f\n", i+1, o.Product.Item.GetName(), o.SoldPrice)
    }

    fmt.Printf("Total Price is: %f\n", totalPrice)
}

And our shop now has all the functionality it needs based on our requirements.
I hope this little project has really helped in understanding the OOP concepts of the Go programming language.

You can find the full source code on GitHub.

Follow me on Twitter.
Learn more about me or hire me.
Check out my side project ๐Ÿ‘‰ https://nodelayer.xyz

ย