Channels ▼
RSS

Open Source

Go Introduction: How Go Handles Objects


Incidentally, we have made every methods' receiver accept a pointer rather than a value. If we used values, these would be copied and any changes applied lost. By passing pointers, we have made the IntSet mutable. Go automatically dereferences structs up to one level of indirection, and automatically passes the address of addressable values when a pointer is required, so Go programs have far fewer *s and &s than C or C++ programs.

But what if we want our IntSet's actions to be undoable? One obvious approach is to subclass. We can't do that as such in Go, but we can get pretty close (although this is not the recommended way — we'll get to that soon)!

type UndoableIntSet struct { // Poor style
    IntSet    // Embedding (delegation)
    functions []func()
}

func NewUndoableIntSet() UndoableIntSet {
    return UndoableIntSet{NewIntSet(), nil}
}
func (set *UndoableIntSet) Add(x int) { // Override
    if !set.Contains(x) {
	set.data[x] = true
	set.functions = append(set.functions, func() { set.Delete(x) })
    } else {
	set.functions = append(set.functions, nil)
    }
}

func (set *UndoableIntSet) Delete(x int) { // Override
    if set.Contains(x) {
	delete(set.data, x)
	set.functions = append(set.functions, func() { set.Add(x) })
    } else {
	set.functions = append(set.functions, nil)
    }
}
func (set *UndoableIntSet) Undo() error {
    if len(set.functions) == 0 {
	return errors.New("No functions to undo")
    }
    index := len(set.functions) - 1
    if function := set.functions[index]; function != nil {
	function()
	set.functions[index] = nil // Free closure for garbage collection
    }
    set.functions = set.functions[:index]
    return nil
}

The functions field's type is a slice (effectively a variable length array) of references to functions that take no arguments and return nothing.

Because UndoableIntSet has no Contains() method, its embedded IntSet's Contains() method is used instead (and passed the embedded IntSet as its reciever, not the complete UndoableIntSet). The Add() and Delete() methods have been overridden because these modify the set and must use the undo apparatus. And, of course, an Undo() method has been added.

The Add() and Delete() methods add their inverses to the functions slice. These are method expressions (bound methods) because anonymous (unnamed or literal) functions in Go are closures. Sometimes the inverse is to do nothing. For example, if we have an empty ints UndoableIntSet, and call ints.Add(5), the set will become {5}. If we do the same call again, the set will still be {5}. If we then call ints.Undo(), the set should not be changed (since the second Add() call did nothing). But if we call Undo() a second time, then the set should become empty.

The Undo() method retrieves the last bound method (or nil). If there is a method to call, it is called and then it is set to nil to free it up for garbage collection. We also slice the method (or nil) from the functions slice because it is no longer needed. (Go supports a subset of Python's slicing syntax.)

Unfortunately, the mechanics of the undo functionality are intertwined with the set's normal (mutating) methods. Furthermore, if we wanted to add the undo functionality to another type, we would basically have to copy and paste it. The solution is to use the Strategy pattern — this means creating a type that provides the algorithm we want, and aggregating a field with that type in any types that we want to make use of the algorithm. Let's put this in concrete terms, starting with an Undo type.

type Undo []func()
func (undo *Undo) Add(function func()) {
    *undo = append(*undo, function)
}

func (undo *Undo) Undo() error {
    functions := *undo
    if len(functions) == 0 {
	return errors.New("No functions to undo")
    }
    index := len(functions) - 1
    if function := functions[index]; function != nil {
	function()
	functions[index] = nil // Free closure for garbage collection
    }
    *undo = functions[:index]
    return nil
}

Rather than using a struct, we have chosen to define the Undo type directly as a slice of functions that take no arguments and return nothing. This means that when we create mutating methods, not only must we pass the receiver as a pointer, but we must dereference the pointer to access the underlying value. (When a struct is used, Go will automatically dereference one level of indirection, which is why the syntaxes for the IntSet and UndoableIntSet are a bit nicer.) The Undo.Add() method appends a function or a bound method (or nil) to an Undo value. The Undo.Undo() method has exactly the same logic as the UndoableIntSet.Undo() method. To make the syntax inside the method slightly nicer, we have assigned the pointed to value to a variable that we have then used throughout.

We can now add undo functionality to any type, providing that all of its mutating methods have inverses. One way to add the functionality would be to embed an Undo value like this:

type IntSet struct { // Unwise
    data map[int]bool
    Undo
}

Unfortunately, this would expose the Undo.Add() and Undo.Undo() methods to IntSet clients. Here is a complete undoable IntSet with an aggregated rather than an embedded Undo value (and excluding the Contains() method, which is identical to the one shown earlier).

type IntSet struct {
    data map[int]bool
    undo Undo
}
func NewIntSet() IntSet {
    return IntSet{data: make(map[int]bool)}
}
func (set *IntSet) Add(x int) {
    if !set.Contains(x) {
	set.data[x] = true
	set.undo.Add(func() { set.Delete(x) })
    } else {
	set.undo.Add(nil)
    }
}
func (set *IntSet) Delete(x int) {
    if set.Contains(x) {
	delete(set.data, x)
	set.undo.Add(func() { set.Add(x) })
    } else {
	set.undo.Add(nil)
    }
}

func (set *IntSet) Undo() error {
    return set.undo.Undo()
}

This is much cleaner than before because we are using the Undo type's API (Add() and Undo()) rather than directly manipulating a slice of functions. And, of course, it is easy to cleanly add undo functionality to other classes. For example:

type Account struct {
    balance int
    undo    Undo
}

func (account *Account) Credit(x int) error {
    if x < 0 {
	return errors.New("Negative credit disallowed")
    }
    account.balance += x
    account.undo.Add(func() { account.Debit(x) })
    return nil
}

func (account *Account) Debit(x int) error {
    if x < 0 {
	return errors.New("Negative credit disallowed")
    }
    if account.balance-x < 0 {
	return errors.New("Overdraft disallowed")
    }
    account.balance -= x
    account.undo.Add(func() { account.Credit(x) })
    return nil
}
func (account *Account) Balance() int {
    return account.balance
}

func (account *Account) Undo() error {
    return account.undo.Undo()
}

Here, the Credit() and Debit() methods have the expected inverses. And, of course, neither the undoable IntSet nor the undoable Account know or care about their Undo fields' functionality beyond the APIs they use (Undo.Add() and Undo.Undo()).

Using aggregated fields that provide the algorithms we need in accordance with the Strategy pattern is much more flexible than using inheritance. And as we have seen, it is much easier to compose functionality this way and at the same time avoid code duplication.

For programmers coming from a conventional object-oriented language such as C++ or Java, adapting to Go's approach can take some getting used to, at least at first. However, Go's complete separation of a type's data from its methods, and use of aggregation and interfaces rather than inheritance, is very powerful and convenient in practice.

Go is free, easy to install and try, has a stable API, and has a large standard library. It can be used to create standalone executables on all the major platforms. And — rather like Python — Go is fun to use.

Complete source code for all examples.

[Stay tuned next week for the fourth installment of this series. —Ed.]


Mark Summerfield is an independent trainer, consultant, and writer specializing in Go, Python, C++, and Qt. His most recent book is Programming in Go.


Related Articles

Getting Going with Go

Go Tutorial: Object Orientation and Go's Special Data Types


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video