Trusted answers to developer questions

What is the Mediator Design Pattern in Go?

Free System Design Interview Course

Many candidates are rejected or down-leveled due to poor performance in their System Design Interview. Stand out in System Design Interviews and get hired in 2024 with this popular free course.

When to use this pattern?

  • When you detect that you have a couple of classes that are tightly coupled.

  • When you notice that you are creating a lot of subclasses in order to reuse a basic behavior in different contexts.

  • When you can’t reuse a component in a different program because it’s too dependent on other components.

How to implement it?

  1. Identify a class (or classes) that are tightly coupled and would benefit from being more independent and/or free.

  2. Define a mediator interface with a method that your components will use to communicate with each other through the mediator.

  3. Implement the Mediator class. This class will benefit from getting a reference (pointer) to each of the components that will use this class.

  4. The components should implement a reference to the Mediator.

  5. Modify the components method so that they communicate to the mediator instead of methods on other components.

Our example

Let’s create an Airport Runway with its control tower as our mediator:

Following our steps from the previous subtitle, we need to detect our coupled classes:

type AirTransport struct {
    PilotsName string
    Type       string
    Runway     *Runway
    runwayLog  []string
}

func NewHelicopter(pilotsName string) *AirTransport {
    return &AirTransport{PilotsName: pilotsName, Type: "Helicopter"}
}

func NewPlane(pilotsName string) *AirTransport {
    return &AirTransport{PilotsName: pilotsName, Type: "Plane"}
}

func NewUFO(pilotsName string) *AirTransport {
    return &AirTransport{PilotsName: pilotsName, Type: "UFO"}
}

We have a lot of subclasses that will behave similarly and need to communicate among themselves in order to coordinate how will they land on the runway.

The issue here is that if we create a lot of methods for communication among the classes, we will need to keep adding validations as soon as we add new AirTransports or the objects will become too convoluted and dependent.

So, let’s create an interface for our communication method:

type ATCMediator interface {
    communicate(airTransport *AirTransport, message string)
}

Now, we need our Runway, which will implement the mediator interface:

type Runway struct {
    airTransports []*AirTransport
}

func (r *Runway) Communicate(src *AirTransport, message string) {
    for _, at := range r.airTransports {
        if at != src {
            at.Receive(at, message)
        }
    }
}

func (r *Runway) Message(src, dst *AirTransport, message string) {
    for _, at := range r.airTransports {
        if at == dst {
            at.Receive(src, message)
        }
    }
}

func (r *Runway) Join(at *AirTransport) {
    joinMsg := at.PilotsName + " joined the Runway"
    r.Communicate(at, joinMsg)

    at.Runway = r
    r.airTransports = append(r.airTransports, at)
}

This Runway implements the communication method from the declared interface, adds two new methods to message the AirTransports privately, and adds a method to join the Runway.

So now we can implement our communication methods for the AirTransports:e:

func (at *AirTransport) Receive(sender *AirTransport, message string) {
    s := fmt.Sprintf("%s (Transport: %s): '%s'", sender.PilotsName, sender.Type, message)
    fmt.Printf("[%s's runway log] %s\n", sender.PilotsName, s)
    at.runwayLog = append(at.runwayLog, s)
}

func (at *AirTransport) Say(message string) {
    at.Runway.Communicate(at, message)
}

func (at *AirTransport) PrivateMessage(dst *AirTransport, message string) {
    if at != dst {
        at.Runway.Message(at, dst, message)
    }
}

What we are doing here is relying on the Runway as a mediator for every message sent and received in order to grant a log for communication and a separate log for every AirTransport.

That’s mostly the implementation. So, let’s jump into the instantiation:

r := &Runway{}

    h := NewHelicopter("Jack")
    p := NewPlane("Tom")
    u := NewUFO("ET")

    r.Join(h)
    r.Join(p)
    r.Join(u)

    h.Say("Trying to land on Runway")
    p.Say("Acknowledged")
    u.Say("Phone home")

    h.PrivateMessage(p, "Is there an UFO in the Runway")
    p.PrivateMessage(h, "Dude, there's an UFO in the Runway")

    u.Say("I shouldn't be able to read your PM's but I can... ")

We are creating a simple Runway and three different AirTransports and making them communicate among themselves. This brings us to the results:

package main
import "fmt"
type AirTransport struct {
PilotsName string
Type string
Runway *Runway
runwayLog []string
}
func NewHelicopter(pilotsName string) *AirTransport {
return &AirTransport{PilotsName: pilotsName, Type: "Helicopter"}
}
func NewPlane(pilotsName string) *AirTransport {
return &AirTransport{PilotsName: pilotsName, Type: "Plane"}
}
func NewUFO(pilotsName string) *AirTransport {
return &AirTransport{PilotsName: pilotsName, Type: "UFO"}
}
type ATCMediator interface {
communicate(airTransport *AirTransport, message string)
}
type Runway struct {
airTransports []*AirTransport
}
func (r *Runway) Communicate(src *AirTransport, message string) {
for _, at := range r.airTransports {
if at != src {
at.Receive(at, message)
}
}
}
func (r *Runway) Message(src, dst *AirTransport, message string) {
for _, at := range r.airTransports {
if at == dst {
at.Receive(src, message)
}
}
}
func (r *Runway) Join(at *AirTransport) {
joinMsg := at.PilotsName + " joined the Runway"
r.Communicate(at, joinMsg)
at.Runway = r
r.airTransports = append(r.airTransports, at)
}
func (at *AirTransport) Receive(sender *AirTransport, message string) {
s := fmt.Sprintf("%s (Transport: %s): '%s'", sender.PilotsName, sender.Type, message)
fmt.Printf("[%s's runway log] %s\n", sender.PilotsName, s)
at.runwayLog = append(at.runwayLog, s)
}
func (at *AirTransport) Say(message string) {
at.Runway.Communicate(at, message)
}
func (at *AirTransport) PrivateMessage(dst *AirTransport, message string) {
if at != dst {
at.Runway.Message(at, dst, message)
}
}
func main() {
r := &Runway{}
h := NewHelicopter("Jack")
p := NewPlane("Tom")
u := NewUFO("ET")
r.Join(h)
r.Join(p)
r.Join(u)
h.Say("Trying to land on Runway")
p.Say("Acknowledged")
u.Say("Phone home")
h.PrivateMessage(p, "Is there an UFO in the Runway")
p.PrivateMessage(h, "Dude, there's an UFO in the Runway")
u.Say("I shouldn't be able to read your PM's but I can... ")
}

As you can see, in the terminal, we will get all the logs from the pilots (or AirTransports) who are communicating over the mediator. The messages are repeated because they are broadcasted through all the users.

That’s mostly it– Happy Coding!

RELATED TAGS

go

CONTRIBUTOR

Tomas Sirio
Attributions:
  1. undefined by undefined
Copyright ©2024 Educative, Inc. All rights reserved
Did you find this helpful?