DEV Community ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป

DEV Community ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป is a community of 966,904 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Cover image for Proxy Pattern in Go
Tomas Sirio
Tomas Sirio

Posted on

Proxy Pattern in Go

Hey there!

In this week's post, I'm bringing you the proxy design pattern.

Proxies may be used for a lot of things, such as communication, logging, caching, etc. So in this post, I'm going to show you two implementations of it.

Protection Proxy

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

type Driven interface {
    Drive()
}

type Car struct{}

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

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. So for example, if our Car's driver was underage, there'd be nothing stopping him from driving the car. And that's a big 'no no' where I come from.

Let's assume there is a Driver struct as well with just an Age attribute:

type Driver struct {
    Age int
}
Enter fullscreen mode Exit fullscreen mode

So Let's create a CarProxy and a constructor for this:

type CarProxy struct {
    car    Car
    driver *Driver
}

func NewCarProxy(driver *Driver) *CarProxy {
    return &CarProxy{Car{}, driver}
}
Enter fullscreen mode Exit fullscreen mode

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

So now we need to implement the Driven interface for the CarProxy and we are going to restrict the user from being underage to use this method:

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

Let's test the results in the main function:

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

But if we change the Driver's age to 22 for example

go run main.go
Car is being driven
Enter fullscreen mode Exit fullscreen mode

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 those restrictions to an upper layer (or proxy) to make these modifications.

Virtual Proxy

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

Then let's create an Image interface that will have a Draw() signature:

type Image interface {
    Draw()
}
Enter fullscreen mode Exit fullscreen mode

Then we can create a struct for a Bitmap image which 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")
}
Enter fullscreen mode Exit fullscreen mode

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

And this would work just fine:

    bmp := NewBitmap("demo.png")
    DrawImage(bmp)
Enter fullscreen mode Exit fullscreen mode
go run main.go

Loading image from demo.png
About to draw the image
Drawing image demo.png
Done drawing the image
Enter fullscreen mode Exit fullscreen mode

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

    _ = NewBitmap("demo.png")
Enter fullscreen mode Exit fullscreen mode
Loading image from demo.png
Enter fullscreen mode Exit fullscreen mode

Our constructor would be lying to us! And that's also a big 'no no' from where I come from.

So let's use a proxy for this issue. To be more specific, a Virtual Proxy.

type LazyBitmap struct {
    filename string
    bitmap   *Bitmap
}

func NewLazyBitmap(filename string) *LazyBitmap {
    return &LazyBitmap{filename: filename}
}
Enter fullscreen mode Exit fullscreen mode

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

This is because we don't want the bitmap to be created before being rendered! So Let's render it when we actually need it to be:

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

This way we won't be loading infinite instances of the bitmap. It will mostly work as a singleton attribute.

    bmp := NewLazyBitmap("demo.png")
    DrawImage(bmp)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

That's all for this week, I hope you are learning as much as I do with these posts.

Top comments (0)

Take a look at this:

Settings

Go to your customization settings to nudge your home feed to show content more relevant to your developer experience level. ๐Ÿ›