简体   繁体   中英

Combine multiple types into one without interfacing

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.

@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 )

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM