Mediator design pattern

Let's continue with the Mediator pattern. As its name implies, it's a pattern that will be in between two types to exchange information. But, why will we want this behavior at all? Let's look at this in detail.

Description

One of the key objectives of any design pattern is to avoid tight coupling between objects. This can be done in many ways, as we have seen already.

But one particularly effective method when the application grows a lot is the Mediator pattern. The Mediator pattern is the perfect example of a pattern that is commonly used by every programmer without thinking very much about it.

Mediator pattern will act as the type in charge of exchanging communication between two objects. This way, the communicating objects don't need to know each other and can change more freely. The pattern that maintains which objects give what information is the Mediator.

Objectives

As previously described, the main objectives of the Mediator pattern are about loose coupling and encapsulation. The objectives are:

  • To provide loose coupling between two objects that must communicate between them
  • To reduce the amount of dependencies of a particular type to the minimum by passing these needs to the Mediator pattern

A calculator

For the Mediator pattern, we are going to develop an extremely simple arithmetic calculator. You're probably thinking that a calculator is so simple that it does not need any pattern. But we will see that this is not exactly true.

Our calculator will only do two very simple operations: sum and subtract.

Acceptance criteria

It sounds quite funny to talk about acceptance criteria to define a calculator, but let's do it anyway:

  1. Define an operation called Sum that takes a number and adds it to another number.
  2. Define an operation called Subtract that takes a number and substracts it to another number.

Well, I don't know about you, but I really need a rest after this complex criteria. So why are we defining this so much? Patience, you will have the answer soon.

Implementation

We have to jump directly to the implementation because we cannot test that the sum will be correct (well, we can, but we will be testing if Go is correctly written!). We could test that we pass the acceptance criteria, but it's a bit of an overkill for our example.

So let's start by implementing the necessary types:

package main 
 
type One struct{} 
type Two struct{} 
type Three struct{} 
type Four struct{} 
type Five struct{} 
type Six struct{} 
type Seven struct{} 
type Eight struct{} 
type Nine struct{} 
type Zero struct{} 

Well... this look quite awkward. We already have numeric types in Go to perform these operations, we don't need a type for each number!

But let's continue for a second with this insane approach. Let's implement the One struct:

type One struct{} 
 
func (o *One) OnePlus(n interface{}) interface{} { 
  switch n.(type) { 
  case One: 
    return &Two{} 
  case Two: 
    return &Three{} 
  case Three: 
    return &Four{} 
  case Four: 
    return &Five{} 
  case Five: 
    return &Six{} 
  case Six: 
    return &Seven{} 
  case Seven: 
    return &Eight{} 
  case Eight: 
    return &Nine{} 
  case Nine: 
    return [2]interface{}{&One{}, &Zero{}} 
  default: 
    return fmt.Errorf("Number not found") 
  } 
} 

OK , I'll stop here. What is wrong with this implementation? This is completely crazy! It's overkill to make every operation possible between numbers to make sums! Especially when we have more than one digit.

Well, believe it or not, this is how a lot of software is commonly designed today. A small app where an object uses two or three objects grows, and it ends up using dozens of them. It becomes an absolute hell to simply add or remove a type from the application because it is hidden in some of this craziness.

So what can we do in this calculator? Use a Mediator type that frees all the cases:

func Sum(a, b interface{}) interface{}{ 
  switch a := a.(type) { 
    case One: 
    switch b.(type) { 
      case One: 
        return &Two{} 
      case Two: 
        return &Three{} 
      default: 
        return fmt.Errorf("Number not found") 
    } 
    case Two: 
    switch b.(type) { 
      case One: 
        return &Three{} 
      case Two: 
        return &Four{} 
      default: 
      return fmt.Errorf("Number not found") 
 
    } 
    case int: 
    switch b := b.(type) { 
      case One: 
        return &Three{} 
      case Two: 
        return &Four{} 
      case int: 
        return a + b 
      default: 
      return fmt.Errorf("Number not found") 
 
    } 
    default: 
    return fmt.Errorf("Number not found") 
  } 
} 

We have just developed a couple of numbers to keep things short. The Sum function acts as a mediator between two numbers. First it checks the type of the first number named a. Then, for each type of the first number, it checks the type of the second number named b and returns the resulting type.

While the solution still looks very crazy now, the only one that knows about all possible numbers in the calculator is the Sum function. But take a closer look and you'll see that we have added a type case for the int type. We have cases One, Two , and int. Inside the int case, we also have another int case for the b number. What do we do here? If both types are of the int case, we can return the sum of them.

Do you think that this will work? Let's write a simple main function:

func main(){ 
  fmt.Printf("%#v
", Sum(One{}, Two{})) 
  fmt.Printf("%d
", Sum(1,2)) 
} 

We print the sum of type One and type Two. By using the "%#v" format, we ask to print information about the type. The second line in the function uses int types, and we also print the result. This in the console produces the following output:

$go run mediator.go
&main.Three{}
7

Not very impressive, right? But let's think for a second. By using the Mediator pattern, we have been able to refactor the initial calculator, where we have to define every operation on every type to a Mediator pattern, the Sum function.

The nice thing is that, thanks to the Mediator pattern, we have been able to start using integers as values for our calculator. We have just defined the simplest example by adding two integers, but we could have done the same with an integer and the type:

  case One: 
    switch b := b.(type) { 
    case One: 
      return &Two{} 
    case Two: 
      return &Three{} 
    case int: 
      return b+1 
    default: 
      return fmt.Errorf("Number not found") 
    } 

With this small modification, we can now use type One with an int as number b. If we keep working on this Mediator pattern, we could achieve a lot of flexibility between types, without having to implement every possible operation between them, generating a tight coupling.

We'll add a new Sum method in the main function to see this in action:

func main(){ 
  fmt.Printf("%#v
", Sum(One{}, Two{})) 
  fmt.Printf("%d
", Sum(1,2)) 
  fmt.Printf("%d
", Sum(One{},2)) 
} 
$go run mediator.go&main.Three{}33

Nice. The Mediator pattern is in charge of knowing about the possible types and returns the most convenient type for our case, which is an integer. Now we could keep growing this Sum function until we completely get rid of using the numeric types we have defined.

Uncoupling two types with the Mediator

We have carried out a disruptive example to try to think outside the box and reason deeply about the Mediator pattern. Tight coupling between entities in an app can become really complex to deal with in the future and allow more difficult refactoring if needed.

Just remember that the Mediator pattern is there to act as a managing type between two types that don't know about each other so that you can take one of the types without affecting the other and replace a type in a more easy and convenient way.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.47.230