1

In a Go project, I've got to define two different kinds of "shapes" to types that implement an interface called MyObject. The shapes themselves are types defined in an external library, and do not implement any shared interface.

MyObject looks like

type MyObject interface {
    GetShape() *Shape //some unified return value
}

Shapes look like

type Circle struct {
    Radius int
    X int
    Y int
}

type Square struct {
   X int
   Y int
   W int
   H int
}

func NewCircle(x int, y int, radius int) Circle
func NewSquare(x int, y int, w int, h int) Square

I've got a ball and a box that implement MyObject:

type Ball struct {
    shape *Circle
}

type Box struct {
    shape *Square
}

func (b *Ball) GetShape() *Shape {
    return b.shape
}

func (s *Square) GetShape() *Shape {
    return s.shape
}

This seems straightforward enough with interfacing - but we can't use one in this situation since there are no methods implemented by Circle and Square that are identical, plus they are outside of the package we're working in.

For methods using the circle and the square, I need to use methods like

testCircleSquare(circle *Circle, square *Square) bool {}
testSquareSquare(square1 *Square, square2 *Square) bool {}

How can I distinguish or make these two objects more generic? The only idea I had so far was to containerize them into a type like

type Shape struct {
    circle *Circle
    square *Square
}

and check for nil circle or square values to determine which to use, but this seems hacky and difficult to maintain if I add more shapes.

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
Rukinom
  • 23
  • 4
  • If they have no methods in common, why do you want to treat them as the same type? Perhaps you need an [adapter](https://en.wikipedia.org/wiki/Adapter_pattern) instead. – Schwern Jul 10 '17 at 18:20

2 Answers2

2

@Adrian already explained what's wrong with using interface{} here.

Instead, use the Adapter Pattern. Create your own Shape interface and make adapters for the pre-made shapes.

The Shape interface (it should probably be called Shape2D because 3D shapes behave differently) might look like this. This gives you the advantages of the type system, and having a unified shape interface.

type Shape interface {
    Area() float32
    Perimeter() float32
    X() int
    Y() int
}

Then create adapters around the existing objects. No wrapper is necessary, you can define an alias for the type. (external here represents that Circle and Square come from some other package).

type ShapeCircle external.Circle

func (self ShapeCircle) Area() float32 {
    return math.Pi * float32(self.Radius) * float32(self.Radius)
}

...and so on...

type ShapeSquare external.Square

func (self ShapeSquare) Area() float32 {
    return float32(self.W) * float32(self.H)
}

...and so on...

Now you can copy Circle and Square objects to their Shape adapters and use them as Shape.

c := external.Circle{ Radius: 10, X: 0, Y: 0 }

shape := ShapeCircle(c)

fmt.Println(shape.Area())

You can also go the other way.

external.Function( external.Circle(shape) )

Again, this creates a copy.


Alternatively, if you don't like the copying, you can embed Circle inside ShapeCircle and Square inside ShapeSquare.

type ShapeCircle struct {
    external.Circle
}
type ShapeSquare struct {
    external.Square
}

Then you can use ShapeCircle as before, but you have to give it a Circle. Might want to make New function to take care of that.

c := ShapeCircle{
    Circle: external.Circle{ Radius: 10, X: 0, Y: 0 }
}

It can be used as a Shape.

fmt.Println(c.Area())

And c.Circle can be used as a Circle. No copying necessary.

external.Function( c.Circle )
Schwern
  • 153,029
  • 25
  • 195
  • 336
  • I've got to use external lib functions (same library as the shapes themselves) that specifically take `Circle` and `Square` arguments. Could this adapted type be cast back to what it's adapting? Even if it did, it's still unsafe. Any value in that? – Rukinom Jul 11 '17 at 20:57
  • @Rukinom Since `Circle` and `ShapeCircle` have the same struct definition, you can *copy* both ways. `ExternalFunction( Circle(shape) )` will work. AFAIK there's nothing unsafe about it, why do you say that? – Schwern Jul 11 '17 at 21:03
  • Awesome, that works great. I retract statement about safety, since it casts directly. – Rukinom Jul 11 '17 at 21:20
  • @Rukinom Note it's not casting, it's copying. There might be a way to cast, but I don't know it. – Schwern Jul 11 '17 at 21:24
  • @Rukinom I added a version that does not copy. – Schwern Jul 11 '17 at 21:32
1

If you can't build a specific interface for them, your only real option is the empty interface interface{}, which can hold any value. You'll then have to use type assertions or reflection to do anything useful with the values. This is an unusual case from a design perspective as you're holding an arbitrary value that you cannot make any assumptions about.

Adrian
  • 42,911
  • 6
  • 107
  • 99