loading...

Object Calisthenics in Golang

eminetto profile image Elton Minetto Updated on ・7 min read

Jeff Bay introduced the term Object Calisthenics in his book Thought Works Anthology. It is a set of good practices and programming rules that can improve the quality of our code.

I saw these techniques for the first time when Rafael Dohms and Guilherme Blanco did a great job adapting them from the Java language to PHP. If you write code in PHP and do not know Object Calisthenics, I recommend two of the presentations made by them:

Your code sucks, let's fix it

Object Calisthenics Applied to PHP

But what are these rules, anyway? They are, in the original version:

  • One level of indentation per method.
  • Don't use the ELSE keyword.
  • Wrap all primitives and Strings in classes.
  • First class collections.
  • One dot per line.
  • Don't abbreviate.
  • Keep all classes less than 50 lines.
  • No classes with more than two instance variables.
  • No getters or setters.

As I mentioned earlier, Jeff created these rules based on the Java language and they need adaptations for other environments. Just as Rafael and Guilherme did for PHP, we need to look at each item and analyze it to see if it makes sense in Go.

The first point to consider is the name itself. Calisthenics comes from the Greek and means a series of exercises to reach an end, such as improving your physical fitness. In this context, the exercises improve the conditioning of our code. The problem is the Object term, because this concept does not exist in Go. Therefore, I propose a first change: from Object Calisthenics to Code Calisthenics. I leave the comment area below to discuss if this is a good suggestion.

Let’s analyze the other items.

One level of indentation per method

Applying this rule allows our code to be more readable.

Let’s apply the rule to the code:

package chess

import "bytes"

type board struct {
    data [][]string
}

func NewBoard(data [][]string) *board {
    return &board{data: data}
}

func (b *board) Board() string {
    var buffer = &bytes.Buffer{}

    // level 0
    for i := 0; i < 10; i++ {
        // level 1
        for j := 0; j < 10; j++ {
            // level 2
            buffer.WriteString(b.data[i][j])
        }
        buffer.WriteString("\n")
    }

    return buffer.String()
}

One result, far more readable, would be:

package chess

import "bytes"

type board struct {
    data [][]string
}

func NewBoard(data [][]string) *board {
    return &board{data: data}
}

func (b *board) Board() string {
    var buffer = &bytes.Buffer{}

    b.collectRows(buffer)

    return buffer.String()
}

func (b *board) collectRows(buffer *bytes.Buffer) {
    for i := 0; i < 10; i++ {
        b.collectRow(buffer, i)
    }
}

func (b *board) collectRow(buffer *bytes.Buffer, row int) {
    for j := 0; j < 10; j++ {
        buffer.WriteString(b.data[row][j])
    }

    buffer.WriteString("\n")
}

Don't use the ELSE keyword

The goal of this item is to avoid using the else keyword, generating a cleaner and faster code because it has fewer execution flows.

Let's look at the code:

package login

import (
    "github.com/user/project/user/repository"
)

type loginService struct {
    userRepository *repository.UserRepository
}

func NewLoginService() *loginService {
    return &loginService{
        userRepository: repository.NewUserRepository(),
    }
}

func (l *loginService) Login(userName, password string) {
    if l.userRepository.IsValid(userName, password) {
        redirect("homepage")
    } else {
        addFlash("error", "Bad credentials")

        redirect("login")
    }
}

func redirect(page string) {
    // redirect to given page
}

func addFlash(msgType, msg string) {
    // create flash message
}

We can apply the Early Return concept and remove the else from the function Login:


func (l *loginService) Login(userName, password string) {
    if l.userRepository.IsValid(userName, password) {
        redirect("homepage")
        return
    }
    addFlash("error", "Bad credentials")
    redirect("login")
}

Wrap all primitives and Strings in classes

This rule suggests that primitive types that have behavior must be encapsulated, in our case, in structs or types and not in classes. That way, we encapsulate the behavior logic and make the code easy to maintain. Let’s see the example:

package ecommerce

type order struct {
    pid int64
    cid int64
}

func CreateOrder(pid int64, cid int64) order {
    return order{
        pid: pid, cid: cid,
    }
}

func (o order) Submit() (int64, error) {
    // do some logic

    return int64(3252345234), nil
}

Applying the rule, using structs:

package ecommerce

import (
    "strconv"
)

type order struct {
    pid productID
    cid customerID
}

type productID struct {
    id int64
}

// some methods on productID struct

type customerID struct {
    id int64
}

// some methods on customerID struct

type orderID struct {
    id int64
}

func (oid orderID) String() string {
    return strconv.FormatInt(oid.id, 10)
}

// some other methods on orderID struct

func CreateOrder(pid int64, cid int64) order {
    return order{
        pid: productID{pid}, cid: customerID{cid},
    }
}

func (o order) Submit() (orderID, error) {
    // do some logic

    return orderId{int64(3252345234)}, nil
}

Another possible and more idiomatic refactoring, using types could be::

package ecommerce

import (
    "strconv"
)

type order struct {
    pid productID
    cid customerID
}

type productID int64


// some methods on productID type

type customerID int64

// some methods on customerID type

type orderID int64

func (oid orderID) String() string {
    return strconv.FormatInt(int64(oid), 10)
}

// some other methods on orderID type

func CreateOrder(pid int64, cid int64) order {
    return order{
        pid: productID(pid), cid: customerID(cid),
    }
}

func (o order) Submit() (orderID, error) {
    // do some logic

    return orderID(int64(3252345234)), nil
}

Besides being readable, the new code allows easy evolution of business rules and greater security in relation to what we are manipulating.

First class collections

If you have a set of elements and want to manipulate them, create a dedicated structure for that collection. Thus, we can implement all the related behaviors to the collection in the structure, such as filters, validation rules, and so on.

So, the code:

package contact

import "fmt"

type person struct {
    name    string
    friends []string
}

type friend struct {
    name string
}

func NewPerson(name string) *person {
    return &person{
        name:    name,
        friends: []string{},
    }
}

func (p *person) AddFriend(name string) {
    p.friends = append(p.friends, name)
}

func (p *person) RemoveFriend(name string) {
    new := []string{}
    for _, friend := range p.friends {
        if friend != name {
            new = append(new, friend)
        }
    }
    p.friends = new
}

func (p *person) GetFriends() []string {
    return p.friends
}

func (p *person) String() string {
    return fmt.Sprintf("%s %v", p.name, p.friends)
}

Can be refactored to:

package contact

import (
    "strings"
    "fmt"
)

type friends struct {
    data []string
}

type person struct {
    name    string
    friends *friends
}

func NewFriends() *friends {
    return &friends{
        data: []string{},
    }
}

func (f *friends) Add(name string) {
    f.data = append(f.data, name)
}

func (f *friends) Remove(name string) {
    new := []string{}
    for _, friend := range f.data {
        if friend != name {
            new = append(new, friend)
        }
    }
    f.data = new
}

func (f *friends) String() string {
    return strings.Join(f.data, " ")
}

func NewPerson(name string) *person {
    return &person{
        name:    name,
        friends: NewFriends(),
    }
}

func (p *person) AddFriend(name string) {
    p.friends.Add(name)
}

func (p *person) RemoveFriend(name string) {
    p.friends.Remove(name)
}

func (p *person) GetFriends() *friends {
    return p.friends
}

func (p *person) String() string {
    return fmt.Sprintf("%s [%v]", p.name, p.friends)
}

One dot per line

This rule states you should not chain functions, but use only those that are part of the same context.

Example:

package chess

type piece struct {
    representation string
}

type location struct {
    current *piece
}

type board struct {
    locations []*location
}

func NewLocation(piece *piece) *location {
    return &location{current: piece}
}

func NewPiece(representation string) *piece {
    return &piece{representation: representation}
}

func NewBoard() *board {
    locations := []*location{
        NewLocation(NewPiece("London")),
        NewLocation(NewPiece("New York")),
        NewLocation(NewPiece("Dubai")),
    }
    return &board{
        locations: locations,
    }
}

func (b *board) squares() []*location {
    return b.locations
}

func (b *board) BoardRepresentation() string {
    var buffer = &bytes.Buffer{}
    for _, l := range b.squares() {
        buffer.WriteString(l.current.representation[0:1])
    }
    return buffer.String()
}

We will refactor to:

package chess

import "bytes"

type piece struct {
    representation string
}

type location struct {
    current *piece
}

type board struct {
    locations []*location
}

func NewPiece(representation string) *piece {
    return &piece{representation: representation}
}

func (p *piece) character() string {
    return p.representation[0:1]
}

func (p *piece) addTo(buffer *bytes.Buffer) {
    buffer.WriteString(p.character())
}

func NewLocation(piece *piece) *location {
    return &location{current: piece}
}

func (l *location) addTo(buffer *bytes.Buffer) {
    l.current.addTo(buffer)
}

func NewBoard() *board {
    locations := []*location{
        NewLocation(NewPiece("London")),
        NewLocation(NewPiece("New York")),
        NewLocation(NewPiece("Dubai")),
    }
    return &board{
        locations: locations,
    }
}

func (b *board) squares() []*location {
    return b.locations
}

func (b *board) BoardRepresentation() string {
    var buffer = &bytes.Buffer{}
    for _, l := range b.squares() {
        l.addTo(buffer)
    }
    return buffer.String()
}

This rule reinforces the use of "Law Of Demeter":

Only talk to your immediate friends

Don't abbreviate

This rule does not apply directly to Go. The community has its own rules for creating variable names, including reasons for using smaller ones. I recommend reading this chapter of: ‌Practical Go: Real world advice for writing maintainable Go programs

Keep all classes less than 50 lines

Although there is no concept of classes in Go, this rule states that entities should be small. We can adapt the idea to create small structs and interfaces that we can use, via composition, to form larger components. For instance, the interface:

type Repository interface {
    Find(id entity.ID) (*entity.User, error)
    FindByEmail(email string) (*entity.User, error)
    FindByChangePasswordHash(hash string) (*entity.User, error)
    FindByValidationHash(hash string) (*entity.User, error)
    FindByChallengeSubmissionHash(hash string) (*entity.User, error)
    FindByNickname(nickname string) (*entity.User, error)
    FindAll() ([]*entity.User, error)
    Update(user *entity.User) error
    Store(user *entity.User) (entity.ID, error)
    Remove(id entity.ID) error
}

Can be adapted to::

type Reader interface {
    Find(id entity.ID) (*entity.User, error)
    FindByEmail(email string) (*entity.User, error)
    FindByChangePasswordHash(hash string) (*entity.User, error)
    FindByValidationHash(hash string) (*entity.User, error)
    FindByChallengeSubmissionHash(hash string) (*entity.User, error)
    FindByNickname(nickname string) (*entity.User, error)
    FindAll() ([]*entity.User, error)
}

type Writer interface {
    Update(user *entity.User) error
    Store(user *entity.User) (entity.ID, error)
    Remove(id entity.ID) error
}

type Repository interface {
    Reader
    Writer
}

Thus, other interfaces and scenarios can reuse the Reader and Writer.

No classes with more than two instance variables

This rule does not seem to make sense in Go, but if you have any suggestions, please share.

No Getters/Setters

Like the previous rule, this also does not seem to be adaptable to Go, because it is not a pattern used by the community, as seen in this topic of Effective Go. But suggestions are welcome.

Applying these rules, among other good practices, it is possible to have a cleaner, simple and easy to maintain code. I would love to hear your opinions on the rules and these suggestions of adaptation to Go.

References

https://javflores.github.io/object-calisthenics/

https://williamdurand.fr/2013/06/03/object-calisthenics/

https://medium.com/web-engineering-vox/improving-code-quality-with-object-calisthenics-aa4ad67a61f1

I have adapted some examples used in this post from the repository: https://github.com/rafos/object-calisthenics-in-go

Acknowledgment

Thank you Wagner Riffel and Francisco Oliveira for suggestions on how to improve the examples.

Posted on Jun 4 '19 by:

eminetto profile

Elton Minetto

@eminetto

Teacher, speaker, developer. CTO at @codenationdev

Discussion

markdown guide
 

Thank you for writing this. I had not encountered Object Calisthenics before and there are some interesting ideas.

I did not find your indention example easy to read. In fact, I only quickly understood it because I had the benefit of previous example. One thing that I feel got missed is Go has a very elegant way of traversing multi-dimensional arrays.

func (b *board) Board() string {
    var buffer = &bytes.Buffer{}

    for x, row := range b.data {
        for y, v:= range row {
            cell := parseCell(x, y, v)
            buffer.WriteString(cell)
        }
        buffer.WriteString("\n")
    }

    return buffer.String()
}

Perhaps a better rule for nesting is that you should nest for only one purpose. Here the nesting is about traversing a board. But if you did any logic in the middle of my loop, that often makes it more difficult to read.

However, if I were manipulating 3-D space, having three nested loops would all share the same purpose. I just don't believe you can achieve the same easy understanding if you split it up into lots of functions.

 

Thank you for the article, interesting read.

I was just wondering why in First class collections have you chosen to use:

type friends struct {
    data []string
}

instead of using type alias like:

type friends []string

It seems to me that it would work fine with the examples.
Are there any advantages of the approach you used?

 

I think there’s no difference at all. Both constructions are valid in this case.

 

I think is much more readable the 2 nested "for cycle", if you keep going like that the next step it will be the functional programming you can choose scala or java stream, go has to be easy and simple, stop with the rules, just code buddy!