What is the Proxy Design Pattern in Go?

Protection proxy

Let’s assume you have a car struct and a Driven interface that implements a Drive() function. There’s nothing too fancy in this example, but it will make it all click in the end:

type Driven interface {
    Drive()
}

type Car struct{}

func (c *Car) Drive() {
    fmt.Println("Car is being driven")
}

The Car struct is implementing the Driven interface with the Drive() function.

So, now, you’d be able to just instantiate a car and make it drive regardless of the driver’s condition. For example, if our Car’s driver were underage, there’d be nothing stopping him from driving the car, even though that is a big ‘no no’ in many places.

Let’s assume there is also a Driver struct with just an Age attribute:

type Driver struct {
    Age int
}

We need create a CarProxy and a constructor for this:

type CarProxy struct {
    car    Car
    driver *Driver
}

func NewCarProxy(driver *Driver) *CarProxy {
    return &CarProxy{Car{}, driver}
}

The CarProxy will have a Car and a Driver as its attributes and will receive the Driver as a parameter. Since cars are cars, and we don’t care that much about them in this example, we are going to create a generic car for the car proxy.

To do this, we need to implement the Driven interface for the CarProxy and restrict the user from being underage with this method:

func (c *CarProxy) Drive() {
    if c.driver.Age >= 16 {
        c.car.Drive()
    } else {
        fmt.Println("Driver too young!")
    }
}

Let’s test the results in the main function:

car := NewCarProxy(&Driver{12})
    car.Drive()
go run main.go
Driver too young!

But if we change the Driver’s age to 22, for example:

go run main.go
Car is being driven

As you can see, we didn’t have to add things to the car struct in order to get the restrictions we needed for it. Instead, we just applied the restrictions to an upper layer (or proxy) to make the modifications.

Virtual proxy

For this example let’s assume you want to create an Image Drawer for different types of formats.

Let’s create an Image interface that will have a Draw() signature:

type Image interface {
    Draw()
}

We can then create a struct for a Bitmap image that will be constructed from a filename:

type Bitmap struct {
    filename string
}

func NewBitmap(filename string) *Bitmap {
    fmt.Println("Loading image from", filename)
    return &Bitmap{filename: filename}
}

func (b *Bitmap) Draw() {
    fmt.Println("Drawing image", b.filename)
}

func DrawImage(image Image) {
    fmt.Println("About to draw the image")
    image.Draw()
    fmt.Println("Done drawing the image")
}

I also added a DrawImage method that will show the progress of the image while being drawn. Again, nothing fancy.

And this would work just fine:

bmp := NewBitmap("demo.png")
    DrawImage(bmp)
go run main.go

Loading image from demo.png
About to draw the image
Drawing image demo.png
Done drawing the image

But there is an issue if, for example, we used the constructor without storing it in a variable:

_ = NewBitmap("demo.png")
Loading image from demo.png

In that case, our constructor would be lying to us! This is also a big ‘no no’.

To fix this issue, ;et’s use a proxy for. To be more specific, a Virtual Proxy:

type LazyBitmap struct {
    filename string
    bitmap   *Bitmap
}

func NewLazyBitmap(filename string) *LazyBitmap {
    return &LazyBitmap{filename: filename}
}

Our LazyBitmap struct will store the filename and a bitmap, but will only receive the filename while instantiated, leaving the bitmap as a nil attribute.

This is because we don’t want the bitmap to be created before being rendered! So, let’s render it when we need it to be rendered:

func (l *LazyBitmap) Draw() {
    if l.bitmap == nil {
        l.bitmap = NewBitmap(l.filename)
    }
    l.bitmap.Draw()
}

This way, we won’t be loading infinite instances of the bitmap, and it will mostly work as a singleton attribute.

bmp := NewLazyBitmap("demo.png")
    DrawImage(bmp)

We now instantiate the bitmap as a LazyBitmap and we can still use the DrawImage() method because it will call the bitmap’s Draw() method on the inside. However, this time, the result will be a little bit different:

go run main.go
                                                                           
About to draw the image
Loading image from demo.png
Drawing image demo.png
Done drawing the image

As you can see, the image is now loaded after the Draw method is called and not before, which is what we wanted.

Check out the four other design patterns in GO:

Free Resources

Attributions:
  1. undefined by undefined
Copyright ©2024 Educative, Inc. All rights reserved