Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Golang interface cast to embedded struct

Tags:

interface

go

I want to implement a collision library using the interface Collidable

type Collidable interface{
    BoundingBox() (float64,float64,float64,float64)
    FastCollisionCheck(c2 Collidable) bool
    DoesCollide(c2 Collidable) bool
    Collide(c2 Collidable)
}

It has predefined shapes like.

type Circle struct{
X,Y,Radius float64
}

The idea is that I can do

type Rock struct{
    collision.Circle
    ....
}

which then implements the interface Collidable, so I can pass it to a Spatial Hash Map (that expects a collidable). Only thing needed to do would be to override the Collide() function to my needs.

However the functions in type circle can not handle the type rock, even tough it has a circle embedded.

func (c1 *Circle) DoesCollide(i Collidable) bool{
    switch c2 := value.(type) {
    case Circle:
    //doesn't fire, as it is of type Rock (unknown in this package)
    //Needed is something like
    //if i_embeds_Circle then c2 := i_to_Circle 
    }
}

Is this possible? Is there a better way?

like image 355
user2089648 Avatar asked Mar 13 '15 18:03

user2089648


1 Answers

You are trying to use an object oriented design pattern with inheritance. This is not how to do this in Go. Beside, interface names end in 'able' in Java or equivalent object oriented languages. In Go the convention is to end interface names with 'er'.

To answer your question about the Rock, I would suggest that all thing that can collide into another thing implements the method CollisonShape() returning a collison.Shaper (e.g. Circle) that you will use to test collison. Here collison is the name of your package.

// This interface is defined in the collison package.
// Any object that may collide must implement that method.
type Collider interface {
    CollisonShape() Shaper
}

// This function defined in the collison package 
// test if two Collider collide into each other.
func Collide(c1, c2 Collider) bool {
    shape1, shape2 := c1.CollisonShape(), c2.CollisonShape()
    ...
}

// This is how you would define an object that can collide.
type Rock struct {
    shape *collison.Circle
    ...
}
// Implements the Collider interface.
// The return type must be the same as in the interface.
func (r *Rock) CollisonShape() collison.Shaper {
    return r.shape
}

As you see, we use a method to access the collison shape of the rock. This allow us to write

if collison.Collide(rock, spaceCraft) {...}

This answer your question on how to do get the collison Shape of Rock.

If you want to avoid the call to the CollisonShape() method in the Collide() function, you will have to pass directly the collison.Shaper.

The method Collide would be defined in the collison package as

func Collide(shape1, shape2 Shaper) bool {...}

You then would have to write

if collison.Collide(rock.shape, spacecraft.shape) {...}

This design would be slightly more efficient, but the price to pay is less readable code which is frown upon by experienced Go programmers.

If you want Circle to be an embedded struct in rock you would then have to define it the following way. Embedding the shape saves allocation time for the Circle and some work for the GC.

type Rock struct {
    shape collison.Circle
    ....
}

if collison.Collide(&rock.shape, &spacecraft.shape) {...}

If you want to use an anonymous embedded struct, you then would have to write

type Rock struct {
    Circle
    ....
}

if collison.Collide(&rock.Circle, &spacecraft.Rectangle) {...}

As you see, the code gets less and less readable, and less convenient to use. The shape is not abstracted anymore. Using anonymous embedded struct should be limited to the very few cases where it really make sense.

By using the initially suggested CollisonShape() method, you could easily change your Rock structure into this one without breaking any code.

type Rock struct {
    shape collison.Circle
    ...
}


func (r *Rock) CollisonShape() collison.Shaper {
    return &r.shape
}

This now make the shape and embedded struct. The use of a method to get the shape decouples the internal implementation of Rock from the access to the shape. You can change the internal implementation of Rock without needing to change code in other places.

This is one of the reason why Go doesn't support inheritence. It creates a very strong dependency and coupling between the base class and derived classes. Experience has shown that people often regret such coupling as the code evolves. Object composition is preferred and recommended and well supported by Go.

If efficiency is your goal, every Collider should have one position which changes and a bounding box with a width and height that doesn't change. You can save a few operations using these values for the bounding box overlap test. But this is another story.

like image 147
chmike Avatar answered Oct 14 '22 04:10

chmike