Using the Proxy Design Pattern in Golang

Kacper Bąk
4 min readDec 9, 2022

--

Photo by Joseph Starbuck on Unsplash

Proxy

  • manage the reference, so that the access of the proxy, is a real object. A Proxy can refer to an Object if the interfaces of the Object and the True Object are the same.
  • Provides an interface identical to the one provided by the Subject, so the Proxy can be overridden by the True Subject.
  • Manages access to the Real Item and can be responsible for creating and deleting them.

Other responsibilities depend on the type of Proxy:

  • remote proxies — are responsible for encoding requests and their arguments, and are responsible for sending Real Objects elsewhere in memory;
  • virtual proxies — can cache additional information about Real Objects, so they can push access to that;
  • protection proxies — check whether the caller has access to the permissions required to execute the request.

Adapter

  • Adapts one interface to another, so that classes can work together that normally couldn’t because of incompatible interfaces.
  • It allows the interface of an existing class to be used as another interface.
  • It is used to make unrelated classes work together.

Subject

  • defines the interface for the Real Subject and the Proxy, so the Proxy can be used wherever the Real Subject is expected.

Real Subject / True Subject

  • defines the real object that the proxy represents

Consequences

The proxy design pattern introduces a level that reduces direct access to the object. Additional redirection has many uses, depending on what type of proxy is used:

  1. a remote proxy can hide facts that an object contains in different memory addresses.
  2. a virtual proxy can optimize processes, such as object creation.
  3. protective proxy and wise reference allow better management of processes when access to an object is allocated.

There is also another optimization that the proxy design pattern can hide from the client. It is copy-on-write, and it is related to creation. Copying a large and complex object can be an expensive operation. If the copy is never modified, then you don’t have to worry about the cost. By using a proxy to move the copying process, we make sure that we pay the price of copying an object, only when it is modified.

To create a copy-on-write, the Object must be a countable reference. Copying a proxy will do nothing more than increment that countable reference. Only when the client requests an operation that modifies the object, then the proxy will copy it. In that case, the proxy must decrement the object count reference. When the reference is changed to zero, then the object will be deleted.

Copy-on-write can VERY much reduce the cost of copying heavy objects.

Example code

The following code implements the virtual proxy design pattern.

The Graphic class defines an interface for a graphical object:

type Graphic interface {
Draw(gs *GraphicState) // Draw the graphic
Bounds() Rectangle
Load(filename string)
}

The Image class implements an interface to show image files. Image overrides HandleMouse to allow the user to resize the image interactively.

type Image struct {
Graphic // The Graphic object that this Image object is a proxy for
}

The ImageProxy has the same interface as the Image.

type ImageProxyInterface interface {
Load(filename string)
HandleMouse(event *MouseEvent)
}

The constructor writes a local copy of the filename that stores the image.

func NewImageProxy(filename string) *ImageProxy {
return &ImageProxy{
filename: filename,
}
}

The GetExtent implementation returns a cached extent if possible; otherwise, the image is loaded from a file.

func (image *ImageProxy) GetExtent() interface{} {
if image._extent == nil {
image.Load(image.filename)
}
return image._extent
}

The Save operation saves the cached image extent and image file along with the name to the pipeline.

type Save struct {
// The name of the image file.
Filename string

// The image extent.
Extent interface{}

// The image file.
Image interface{}

// The name of the image.
Name string
}

The Load reads this information and initializes it to the corresponding members.

func (save *Save) Load(filename string, extent interface{}, image interface{}, name string) {
save.Filename = filename
save.Extent = extent
save.Image = image
save.Name = name
}

Finally, we have the TextDocument class, which contains Graphic objects:

type TextDocument struct {
Graphic []Graphic
}

We can pass ImageProxy to a text document in the following way:

func (textDocument *TextDocument) AddImageProxy(imageProxy ImageProxyInterface) {
textDocument.Graphic = append(textDocument.Graphic, imageProxy)
}

The full source code I created for understanding this design pattern can be found in this link: https://github.com/53jk1/proxy

Related Design Patterns

  • Adapter: An adapter provides different interfaces to an object that adapts. By contrast, a proxy provides the same interface that the object has. However, the proxy is used to protect access and can be rejected, to perform operations that the object wants to perform, so the interface can effectively be a subset of objects.
  • Decorator: Decorators may have similar implementations to proxies, but decorators may have a different purpose. A decorator adds one or more responsibilities to an object, while a proxy controls access to the object. A defensive proxy can be implemented exactly like a decorator. On another note, a remote proxy will not contain a direct reference to the real object, but an indirect reference such as, for example, “host ID and local address on the host.”

--

--