Design Patterns in Go: Mediator, uncoupling objects made easy

Introduction

The mediator pattern is a pattern used when you want to simplify communication i.e. message dispatching in complex applications. It involves building a mediator where each objects can delegate its communication, and which routes the messages to the right receiver(s).

So, what does it look like? Well, like this:

An explanation of this diagram is needed:

  1. First there is the Mediator interface, which defines the message which can be routed through this Mediator.
  2. The ConcreteMediator implements the Mediator interface, and coordinates the communication between the ConcreteWorker objects. It knows about the ConcreteWorker objects and how they should communicate
  3. The Worker is an interface (or in some languages an abstract class) which is an interface for the communication between the ConcreteWorkers
  4. The ConcreteWorker(x) classes implement the Worker interface, and communicate with other ConcreteWorker classes through the Mediator.

Implemementation in Go

In an empty directory open your commandline or terminal and type:

go mod init github.com/mediator_pattern

Open your favorite IDE and add a main.go file.

The Mediator interface

First we will the define the Mediator interface:

type Mediator interface {
	notify(sender Worker, event string)
}

This interface only has one method which is used to notify other objects connected to this Mediator of some event happening. This method gets two parameters:

  1. The sender, that is the object doing the notifying. In some cases this important to know, we use it here to see where a message is coming from.
  2. The event data, in this case it is just a string. In practice this could be some complex object.

The Worker interface

The Worker interface looks like this:

type Worker interface {
	setMediator(m Mediator)
	send(event string)
	receive(event string)
}

An explanation:

  1. The setMediator() method sets the connected Mediator object.
  2. The send() method sends an event to the objects connected to the Mediator object.
  3. The receive() method is used to handle incoming events.

The ConcreteMediator struct and its implementation

Now we need to make a ConcreteMediator. This looks as follows:

type ConcreteMediator struct {
	workers []Worker
}

func (m *ConcreteMediator) notify(sender Worker, event string) {
	for _, w := range m.workers {
		if w != sender {
			w.receive(event)
		}
	}
}

A line-by-line explanation:

  1. The Mediator holds an array of connected workers, so that it can send out events when they occur.
  2. The notify() method iterates over workers array, makes sure we do not send the event back to the sender, and calls the receive method on the rest of the workers.

The ConcreteWorker struct and its implementation

The ConcreteWorker looks like this:

type ConcreteWorker struct {
	mediator Mediator
	name     string
}

func (c *ConcreteWorker) setMediator(m Mediator) {
	c.mediator = m
}

func (c *ConcreteWorker) send(event string) {
	fmt.Printf("Sending event %q\n", event)
	c.mediator.notify(c, event)
}

func (c *ConcreteWorker) receive(event string) {
	fmt.Printf("Received event on component %q :  %q\n", c.name, event)
}

A brief explanation:

  1. The ConcreteWorker struct needs to know about its Mediator, hence the mediator field. Also, I added a name for easier identification.
  2. The setMediator() method just sets the Mediator, this means that the Mediator can be changed at runtime.
  3. The send() method prints out an info message, then dispatches the event to the mediator.
  4. The receive() method receives the event from the mediator and prints out a message.

The main method

Now we can test whether this works:

func main() {
	mediator := &ConcreteMediator{workers: make([]Worker, 0)}

	w1 := &ConcreteWorker{name: "First component"}
	w1.setMediator(mediator)
	mediator.workers = append(mediator.workers, w1)

	w2 := &ConcreteWorker{name: "Second component"}
	w2.setMediator(mediator)
	mediator.workers = append(mediator.workers, w2)

	w1.send("Hello to my second component")
	w2.send("Hello first component")
}

Line by line:

  1. We create a ConcreteMediator object, with an empty array of workers.
  2. Next we create the first ConcreteWorker, give it a name, set its mediator and append it to the workers array of the mediator.
  3. Then we see if the communication setup actually works.

The beauty of this setup is that the two workers are totally unaware of each other.

Conclusion

Again, this was very easy to build in Go. The use of interfaces makes for an extensible design. In a next blog post about the use of go-routines in this setup, as they seem suitable for this pattern. For now, it is all synchronous.

Leave a Reply

Your email address will not be published. Required fields are marked *