Featured image of post Object Oriented in Go

Object Oriented in Go

Undersstanding Object Orientation in Golang

back to Go tutorial index

Intro

Understanding object-oriented implementation in Go is very important to build scalable app. Source for this article can be downloaded here

Traditional Object Orientation

Object orientation is about isolating objects one from another so that each aspect of our system can interact with another, but not have to be overly concerned about the implementation details of the objects that they’re interacting with.

When we start to break down an object-oriented system, there are several features that the object exhibit in order to allow us to establish this separation of concern.

The first characteristic of an object-oriented system is something called encapsulation and that’s that idea that we’re going to interact with objects through their methods, but we’re not going to understand the details of how those objects are going to accomplish the functionality that we’re asking them to. So for example, when we’re interacting with a TCP connection handler object, we don’t know how it’s managing that connection. So it might have a direct connection to the port, or it might have another object that it’s using to manage that port, or it might be managing the state some other way. The point with encapsulation is, we don’t know, and we don’t have to care. That’s the responsibility of the TCP connection handler. We just know that it can provide us some functionality and we’re going to take advantage of that functionality.

The next characteristic is message passing. So one of the things that we want to be able to do when we’re interacting with these objects is we don’t want to have anymore information about the object that we need. So when we want to invoke some functionality, sometimes we want to know exactly the functionality that we want to invoke, and that’s not going to be message passing, that’s directly calling a method on an object that we’re working with. But sometimes, we want to allow that object flexibility for exactly how that message is going to be interpreted. So in those situation we’re going to do what’s called message passing where we’re going to provide a message, but we’re not going to have any opinion on how that object is going to interpret that message, or if it does anything with it at all.

The next characteristic that we’re used to seeing in an object-oriented system is a concept called inheritance, where we can have a base type, often called a class in many languages, and we can derive descendent types from that. Now in Go we don’t have capability, there is no inheritance model in Go, but we will talk about another mechanism that is present in the Go language.

And the final characteristic that we’re going to be focusing on in an object-oriented system is this concept of polymorphism. Now polymorphism is the ability to have multiple types stand in for some common type. So in our payment processing system, we might have a payment option type, and then we might have implementing that type, a credit card payment method, we might have a checking account payment method, we might have a cash payment method, and all of those can operate as a payment option in our system. And when we work with types that way, they’re going to be working polymorphically, which means we don’t really care about the underlying type, we just know that it’s going to expose some capabilities that we’re interested in.

To sum up:

  1. By encapsulation, we isolate our objects from one another so that they don’t have access to one another’s data
  2. By passing messages, we’re not going to understand as we ask an object to perform some behaviour, we’re not going to be sure of that exact behaviour that it’s going to invoke. We just know that we’re asking it to do something, we’re relying on it to understand how to do that work.
  3. With inheritance, we’re going to be able to work with some kind of a super type, and that supertype is going to be able to delegate functionality down to its children.
  4. And then with polymorphism we’re going to have a common abstract type of some sort, and we’re going to be able to interact with that abstraction without actually understanding the underlying implementation.

OOP in Go

Just to name some of the challenges that we’re going to be facing,

  • Go is not a class and object language,
  • Go doesn’t have the concept of private data,
  • Go doesn’t support inheritance
  • Go doesn’t have abstract base types

OOP Concept in Go

  • Methods: So while Go doesn’t have a traditional class structure, we can take data structures and attach functions to them, turning them into methods,
  • Package oriented design: So while Go doesn’t have the concept of private data, meaning data that’s private to a class or to a data structure, we do have the concept of package-level data. So if we change our design prespective from a class-oriented design, which is what a lot of object-oriented programming languages drive us to, and focus instead on a package-orientated design, then I think we’re going to see that a lot of our problems go away.
  • Type embedding: That’s going to allow us to gain a lot of the advantages of an inheritance-based system without a lot of baggage.
  • Interface: is relied on heavily in the Go language to provide a lot of the object-orientated characteristics that we’re going to be talking about.

Practicing

We’re going to practice object-oriented design implementation in payment study case.

Encapsulation

Accessing a service on an object without knowing how that service is implemented.

Two tools in Go for encapsulation, package-oriented design and interface. To encapsulate application data in Go means separating the data that the object can make available from how that object is actually storing it or retrieving it.

Package-oriented Design

We want to start by defining a data structure for a credit card in gopherpay/payment/payment.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package payment

type CreditCard struct {
	ownerName       string
	cardNumber      string
	expirationMonth int
	expirationYear  int
	securityCode    int
	availableCredit float32
}
...

The available credit being stored internal to the credit card structure is really a design decision that we need to make, because we might decide we’re going to access that available every time through web server or we might use the web service to populate this first time we access the data.

Now the first challange that we have with this type is we can’t actually build one from outside of the package, because all of the fields are internal and private to the package.

So a typical one to get around that is by adding a constructor function as you see here

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
...
func CreateCreditAccount(ownerName, cardNumber string, expirationMonth, expirationYear, securityCode int) *CreditCard {
	return &CreditCard{
		ownerName:       ownerName,
		cardNumber:      cardNumber,
		expirationMonth: expirationMonth,
		expirationYear:  expirationYear,
		securityCode:    securityCode,
	}
}
...

Now that we have the ability to create the credit card, we need to allow the consumers of this credit card to be able to interact with it, because now they can create credit card, but they still can’t access any of the data that is stored or any the behaviors that we want to expose. So we add few methods below

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
...
func (c CreditCard) OwnerName() string {
  return c.ownerName
}

func (c *CreditCard) SetOwnerName(value string) error {
  if len(value) == 0 {
    retrun errors.New("invalid owner name provided")
  }
  c.ownerName = value
  return nil
}

func (c CreditCard) CardNumber() string {
  return c.cardNumber
}

var cardNumberPattern = regexp.MustCompile("\\d{4}-\\d{4}-\\d{4}-\\d{4}")

func (c *CreditCard) SetCardNumber(value string) error {
  if !cardNumberPattern.Match([]byte(value)) {
    return erros.New("Invalid credit card number format")
  }
  c.cardNumber = value
  return nil
}

func (c CreditCard) ExpirationDate() (int, int) {
  return c.expirationMonth, c.expirationYear
}

func (c *CreditCard) SetExpirationDate(month, year int) error {
  now := time.Now()
  if year < now.Year() || (year == now.Year() && time.Month(month) < now.Month()) {
    return errors.New("Epiration date must lie in the future")
  }
  c.expirationMonth, c.expirationYear = month, year
  return nil
}

func (c CreditCard) SecurityCode() int {
  return c.securityCode
}

func (c *CreditCard) SetSecurityCode(value int) error {
  if value < 100 || value > 999 {
    return errors.New("Security code is not valid")
  }
  c.securityCode = value
  return nil
}

func (c CreditCard) AvailableCredit() float32 {
  return 5000 // this come from web service, client doesn't know or care
}

Naming conventions for accessor methods above is to name accessor method for getters the same name as the the field that you’re getting the data from. So for example, the accessor for ownerName is going to be OwnerName method, we’re just going to capitalize the field name instead of using lowercase. If we need to make an accessor method available that’s going to set the data, then you just prefix that method with set. So the ability to set the ownerName is going to be given the method name SetOwnerName.

Don’t Overuse: There are certainly times when you should design a data structure that accessor methods for some or all of its properties, and you might not make some of those properties available at all, you’re going to encapsulate that data within the package. However, if you just designing a data structure that’s going to be passing JSON data back and forth between a web client and your application, then you probably don’t need to create accessor methods, just use a simple data structure with publicly accessible fields.

Now we are going to drop simple implementation in client gopherpay/client/main.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
...
func main() {

  credit := payment.CreateCreditAccount(
    "Don Krieg",
    "1111-2222-3333-4444",
    5,
    2044,
    323)

  fmt.Printf("Owner name: %v\n", credit.OwnerName())
  fmt.Printf("Card Number: %v\n", credit.CardNumber())
  fmt.Println("Trying to change card number")
  if err := credit.SetCardNumber("invalid"); err != nil {
    fmt.Printf("That didn't work: %v\n", err)
  }
  fmt.Printf("Available credit: %v\n", credit.AvailableCredit())

}

Interface

Now in this case, the data structure is going to be package scoped. So we will create creditAccount struct with lowercase c that means that the data structure creditAccount is not going to be available outside of the payment package. To make it available, we are going to define an interface, PaymentOption, and it’s going to define the accessor methods that are going to be available to access the credit account’s data.

Add PaymentOption interface before data structure

1
2
3
type PaymentOption interface {
  ProcessPayment(float32) bool
}

To implement this interface, we’re going to add method below after CreateCreditAccount func.

1
2
3
4
func (c *CreditCard) ProcessPayment(amount float32) bool {
  fmt.Prinln("processing credit card payment..")
  return true
}

The nice thing about interface is we’re encapsulating not just the data, but the actual type that’s being called. So create another file cash.go in payment package to hold another payment method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package payment

type Cash struct{}

func CreateCashAccount() *Cash {
  return &Cash{}
}

func (c Cash) ProcessPayment(amount float32) bool {
  fmt.Prinln("processing cash transaction....")
  return true
}

In the main function, we peform client action

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
...
func main() {
  var option payment.PaymentOption

  // peform credit payment
  option := payment.CreateCreditAccount(
    "Don Krieg",
    "1111-2222-3333-4444",
    5,
    2044,
    323)
  option.ProcessPayment(500)

  // peform cash transaction
  option = payment.CreateCashAccount()
  option.ProcessPayment(500)
}

Message Passing

Sending a message to an object, but letting that object determine what to do with it.

The first strategy is using interface to establish that abstraction layer. The second is going to be the use of channels to allow as to abstract the sender of a message from the receiver of that message.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
...

type CreditAccount struct{}

// package scoped
func (c *CreditAccount) processPayment(amount float32) {
  fmt.Println("Processing credit card payment....")
}

// construction method
func CreateCreditAccount(chargeCh chan float32) *CreditAccount {
  creditAccount := &CreditAccount{}
  go func(chargeCh chan float32) {
    for amount := range chargeCh {
      creditAccount.processPayment(amount)
    }
  }(chargeCh)

  return creditAccount
}

func main() {
  // creating channel
  chargeCh := make(chan float32)

  // pass into construction function
  CreateCreditAccount(chargeCh)

  // pass message into that channel
  // we don't know what service is being invoked
  chargeCh <- 500

  // just to keep main func from shutting down too early
  var a string
  fmt.Scanln(&a)
}

Inheritance / Composition

Inheritance : Behavior reuse strategy where a type is based upon another type, allowing it to inherit functionality from the base type.

Composition : Behavior reuse strategy where a type contains objects that have desired functionality. The type delegates calls to those object to use their behaviours.

Here is the example creating HybridAccount that use embeded functionality of CheckingAccount.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
...
type CreditAccount struct{}

func (c *CreditAccount) AvailableFunds() float32 {
  fmt.Println("Getting credit funds")
  return 250
}

type CheckingAccount struct{}

func (c *CheckingAccount) AvailableFunds() float32 {
  fmt.Println("Getting checking funds")
  return 125
}

type HybridAccount struct {
  CreditAccount
  CheckingAccount
}

func (h *HybridAccount) AvailableFunds() float32 {
  return h.CreditAccount.AvailableFunds() + h.CheckingAccount.AvailableFunds()
}

func main() {
  ha := &HybridAccount{}
  fmt.Println(ha.AvailableFunds)
}

Polymorphism

The ability to transparently substitute a family of types that implement a common set of behavior.

Again, for this feature we’re going to use interface. Interfaces are implicitly implemented in Go.

  • Interfaces can be used to provide encapsulation by abstracting away the service that’s being provided when we invoke that method through the interface.
  • Interfaces provide us a level of message passing, because we don’t know the method that’s actually being called when we call through the interface.

If we’re working with interfaces and we’re interacting with third-party libraries, make sure that your consuming packages define the interfaces.

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy