-2

I've been thinking alot about this particular problem I'm having an how I'm supposed to solve it the cleanest way.

Imagine an application looking like this:

type AreaCalculator interface {
  Area() int
}

type Rectangle struct {
    color  string
    width  int
    height int
}

type (r *Rectangle) Area() int {
   return r.width * r.height
}

type Circle struct {
    color    string
    diameter int
}

type (c *Circle) Area() int {
   return r.diameter / 2 * r.diameter / 2 * π
}

type Canvas struct {
    children []AreaCalculator
}

func (c *Canvas) String() {
    for child := range c.children {
        fmt.Println("Area of child with color ", child.color, " ", child.Area())
    }
}

This example obviously would not compile because while the String() method of Canvas can call c.Area(), it can't access c.color since there's no way to make sure that a struct implementing AreaCalculator has that property.

One solutions I could think of was to do it like this:

type AreaCalculator interface {
  Area() int
  Color() string
}

type Rectangle struct {
    color  string
    width  int
    height int
}

type (r *Rectangle) Color() string {
   return r.color
}

type (r *Rectangle) Area() int {
   return r.width * r.height
}

type Circle struct {
    color    string
    diameter int
}

type (c *Circle) Area() int {
   return r.diameter / 2 * r.diameter / 2 * π
}
type (c *Circle) Color() string {
   return c.color
}

type Canvas struct {
    children []AreaCalculator
}

func (c *Canvas) String() {
    for child := range c.children {
        fmt.Println("Area of child with color ", child.Color(), " ", child.Area())
    }
}

The other way would be to try something like this:

type Shape struct {
    Area func() int 
    color string
    diameter int
    width int
    height int
}

func NewCircle() Shape {
    // Shape initialisation to represent a Circle. Setting Area func here
}


func NewRectangle() Shape {
    // Shape initialisation to represent a Rectangle. Setting Area func here
}

type Canvas struct {
    children []Shape
}

func (c *Canvas) String() {
    for child := range c.children {
        fmt.Println("Area of child with color", child.color, " ", child.Area())
    }
}

None of these options seem clean to me. I'm sure there's a way cleaner solution I can't think of.

marhaupe
  • 267
  • 1
  • 3
  • 10

2 Answers2

3

An important starting point is that you should not mimic inheritance in Go. Go does not have inheritance. It has interfaces and it has embedding. They didn't forget to include inheritance; it's intentionally not part of the language. Go encourages composition instead.

Your Canvas needs more than a AreaCalculator. It needs something that provides a color. You need to express that. For example, you might do this:

type DrawableShape interface {
  AreaCalculator
  Color() string
}

And then you would implement Color() for Rectangle and Circle.

func (r Rectangle) Color() string {
  return r.color
}

func (c Circle) Color() string {
  return c.color
}

And children would be []DrawableShape:

children []DrawableShape

That would leave something like this (building off of Mohammad Nasirifar's code).

package main

import (
    "fmt"
    "math"
    "strings"
)

type AreaCalculator interface {
    Area() int
}

type DrawableShape interface {
  AreaCalculator
  Color() string
}

type Rectangle struct {
    color  string
    width  int
    height int
}

func (r Rectangle) Area() int {
    return r.width * r.height
}

func (r Rectangle) Color() string {
  return r.color
}

type Circle struct {
    color    string
    diameter int
}

func (c Circle) Area() int {
    area := math.Round(float64(c.diameter*c.diameter) * math.Pi / float64(4))
    return int(area)
}

func (c Circle) Color() string {
  return c.color
}

type Canvas struct {
    children []DrawableShape
}

func (c Canvas) String() string {
    lines := make([]string, 0)
    for _, child := range c.children {
        lines = append(lines, fmt.Sprintf("Area of child with color %s %d", child.Color(), child.Area()))
    }
    return strings.Join(lines, "\n")
}

func main() {
    circle := &Circle{color: "red", diameter: 2}
    rect := &Rectangle{color: "blue", width: 3, height: 4}

    canvas := &Canvas{
        children: []DrawableShape{circle, rect},
    }

    fmt.Println(canvas.String())
}
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • 1
    Oh this seems a bit similar to the one option I desribed so I wasn't thinking that wrong. I just might have get used to this way of thinking. Seeing your answer definetely helps much. Thank you for taking your time, I really appreciate that! – marhaupe Nov 28 '18 at 05:41
1

The key observation here is if you need a particular functionality, make it explicit. Also don't do other objects' job on behalf of them.

Also note that String() must return a string, not write to stdout.

package main

import (
    "fmt"
    "math"
    "strings"
)

type AreaCalculator interface {
    fmt.Stringer
    Area() int
}

type Rectangle struct {
    color  string
    width  int
    height int
}

func (r *Rectangle) Area() int {
    return r.width * r.height
}

func (r *Rectangle) String() string {
    return fmt.Sprintf("I'm a rectangle %d", r.width)
}

type Circle struct {
    color    string
    diameter int
}

func (c *Circle) Area() int {
    area := math.Round(float64(c.diameter*c.diameter) * math.Pi / float64(4))
    return int(area)
}

func (c *Circle) String() string {
    return fmt.Sprintf("I'm a circle: %d", c.diameter)
}

type Canvas struct {
    children []AreaCalculator
}

func (c *Canvas) String() string {
    lines := make([]string, 0)
    for _, child := range c.children {
        lines = append(lines, child.String())
    }
    return strings.Join(lines, "\n")
}

func main() {
    circle := &Circle{color: "red", diameter: 2}
    rect := &Rectangle{color: "blue", width: 3, height: 4}

    canvas := &Canvas{
        children: []AreaCalculator{circle, rect},
    }

    fmt.Println(canvas.String())
}
  • It's important that Canvas needs to know about it's children's color in this case. I should have made that a bit more clearer, sorry. You couldn't have known. If that wouldn't be that case your solution would fit perfectly. Thanks for taking your time! In that case, would you approach it the same way @Rob Napier did? – marhaupe Nov 28 '18 at 05:50