Bridge design pattern

The Bridge pattern is a design with a slightly cryptic definition from the original Gang of Four book. It decouples an abstraction from its implementation so that the two can vary independently. This cryptic explanation just means that you could even decouple the most basic form of functionality: decouple an object from what it does.

Description

The Bridge pattern tries to decouple things as usual with design patterns. It decouples abstraction (an object) from its implementation (the thing that the object does). This way, we can change what an object does as much as we want. It also allows us to change the abstracted object while reusing the same implementation.

Objectives

The objective of the Bridge pattern is to bring flexibility to a struct that change often. Knowing the inputs and outputs of a method, it allows us to change code without knowing too much about it and leaving the freedom for both sides to be modified more easily.

Two printers and two ways of printing for each

For our example, we will go to a console printer abstraction to keep it simple. We will have two implementations. The first will write to the console. Having learned about the io.Writer interface in the previous section, we will make the second write to an io.Writer interface to provide more flexibility to the solution. We will also have two abstracted object users of the implementations--a Normal object, which will use each implementation in a straightforward manner, and a Packt implementation, which will append the sentence Message from Packt: to the printing message.

At the end of this section, we will have two abstraction objects, which have two different implementations of their functionality. So, actually, we will have 22 possible combinations of object functionality.

Requirements and acceptance criteria

As we mentioned previously, we will have two objects (Packt and Normal printer) and two implementations (PrinterImpl1 and PrinterImpl2) that we will join by using the Bridge design pattern. More or less, we will have the following requirements and acceptance criteria:

  • A PrinterAPI that accepts a message to print
  • An implementation of the API that simply prints the message to the console
  • An implementation of the API that prints to an io.Writer interface
  • A Printer abstraction with a Print method to implement in printing types
  • A normal printer object, which will implement the Printer and the PrinterAPI interface
  • The normal printer will forward the message directly to the implementation
  • A Packt printer, which will implement the Printer abstraction and the PrinterAPI interface
  • The Packt printer will append the message Message from Packt: to all prints

Unit testing the Bridge pattern

Let's start with acceptance criteria 1, the PrinterAPI interface. Implementers of this interface must provide a PrintMessage(string) method that will print the message passed as an argument:

type PrinterAPI interface { 
  PrintMessage(string) error 
} 

We will pass to acceptance criteria 2 with an implementation of the previous API:

type PrinterImpl1 struct{} 
 
func (p *PrinterImpl1) PrintMessage(msg string) error { 
  return errors.New("Not implemented yet") 
} 

Our PrinterImpl1 is a type that implements the PrinterAPI interface by providing an implementation of the PrintMessage method. The PrintMessage method is not implemented yet, and returns an error. This is enough to write our first unit test to cover PrinterImpl1:

func TestPrintAPI1(t *testing.T){ 
  api1 := PrinterImpl1{} 
 
  err := api1.PrintMessage("Hello") 
  if err != nil { 
    t.Errorf("Error trying to use the API1 implementation: Message: %s
", err.Error()) 
  } 
} 

In our test to cover PrintAPI1, we created an instance of PrinterImpl1 type. Then we used its PrintMessage method to print the message Hello to the console. As we have no implementation yet, it must return the error srring Not implemented yet:

$ go test -v -run=TestPrintAPI1 . 
=== RUN   TestPrintAPI1 
--- FAIL: TestPrintAPI1 (0.00s) 
        bridge_test.go:14: Error trying to use the API1 implementation: Message: Not implemented yet 
FAIL 
exit status 1 
FAIL    _/C_/Users/mario/Desktop/go-design-patterns/structural/bridge/traditional

Okay. Now we have to write the second API test that will work with an io.Writer interface:

type PrinterImpl2 struct{ 
  Writer io.Writer 
} 
 
func (d *PrinterImpl2) PrintMessage(msg string) error { 
  return errors.New("Not implemented yet") 
} 

As you can see, our PrinterImpl2 struct stores an io.Writer implementer. Also, our PrintMessage method follows the PrinterAPI interface.

Now that we are familiar with the io.Writer interface, we are going to make a test object that implements this interface, and stores whatever is written to it in a local field. This will help us check the contents that are being sent through the writer:

type TestWriter struct { 
  Msg string 
} 
 
func (t *TestWriter) Write(p []byte) (n int, err error) { 
  n = len(p) 
  if n > 0 { 
    t.Msg = string(p) 
    return n, nil 
  } 
  err = errors.New("Content received on Writer was empty") 
  return 
} 

In our test object, we checked that the content isn't empty before writing it to the local field. If it's empty, we return the error, and if not, we write the contents of p in the Msg field. We will use this small struct in the following tests for the second API:

func TestPrintAPI2(t *testing.T){ 
  api2 := PrinterImpl2{} 
 
  err := api2.PrintMessage("Hello") 
  if err != nil { 
    expectedErrorMessage := "You need to pass an io.Writer to PrinterImpl2" 
    if !strings.Contains(err.Error(), expectedErrorMessage) { 
      t.Errorf("Error message was not correct.
 
      Actual: %s
Expected: %s
", err.Error(), expectedErrorMessage) 
    } 
  } 

Let's stop for a second here. We create an instance of PrinterImpl2 called api2 in the first line of the preceding code. We haven't passed any instance of io.Writer on purpose, so we also checked that we actually receive an error first. Then we try to use its PrintMessage method, but we must get an error because it doesn't have any io.Writer instance stored in the Writer field. The error must be You need to pass an io.Writer to PrinterImpl2, and we implicitly check the contents of the error. Let's continue with the test:

  testWriter := TestWriter{} 
  api2 = PrinterImpl2{ 
    Writer: &testWriter, 
  } 
 
  expectedMessage := "Hello" 
  err = api2.PrintMessage(expectedMessage) 
  if err != nil { 
    t.Errorf("Error trying to use the API2 implementation: %s
", err.Error()) 
  } 
 
  if testWriter.Msg !=  expectedMessage { 
    t.Fatalf("API2 did not write correctly on the io.Writer. 
  Actual: %s
Expected: %s
", testWriter.Msg, expectedMessage) 
  } 
} 

For the second part of this unit test, we use an instance of the TestWriter object as an io.Writer interface, testWriter. We passed the message Hello to api2, and checked whether we receive any error. Then, we check the contents of the testWriter.Msg field--remember that we have written an io.Writer interface that stored any bytes passed to its Write method in the Msg field. If everything is correct, the message should contain the word Hello.

Those were our tests for PrinterImpl2. As we don't have any implementations yet, we should get a few errors when running this test:

$ go test -v -run=TestPrintAPI2 .
=== RUN   TestPrintAPI2
--- FAIL: TestPrintAPI2 (0.00s)
bridge_test.go:39: Error message was not correct.
Actual: Not implemented yet
Expected: You need to pass an io.Writer to PrinterImpl2
bridge_test.go:52: Error trying to use the API2 implementation: Not 
implemented yet
bridge_test.go:57: API2 did not write correctly on the io.Writer.
Actual:
Expected: Hello
FAIL
exit status 1
FAIL

At least one test passes--the one that checks that an error message (any) is being returned when using the PrintMessage without io.Writer being stored. Everything else fails, as expected at this stage.

Now we need a printer abstraction for objects that can use PrinterAPI implementers. We will define this as the PrinterAbstraction interface with a Print method. This covers the acceptance criteria 4:

type PrinterAbstraction interface { 
  Print() error 
} 

For acceptance criteria 5, we need a normal printer. A Printer abstraction will need a field to store a PrinterAPI. So our the NormalPrinter could look like the following:

type NormalPrinter struct { 
  Msg     string 
  Printer PrinterAPI 
} 
 
func (c *NormalPrinter) Print() error { 
  return errors.New("Not implemented yet") 
} 

This is enough to write a unit test for the Print() method:

func TestNormalPrinter_Print(t *testing.T) { 
  expectedMessage := "Hello io.Writer" 
 
  normal := NormalPrinter{ 
    Msg:expectedMessage, 
    Printer: &PrinterImpl1{}, 
  } 
 
  err := normal.Print() 
  if err != nil { 
    t.Errorf(err.Error()) 
  } 
} 

The first part of the test checks that the Print() method isn't implemented yet when using PrinterImpl1 PrinterAPI interface. The message we'll use along this test is Hello io.Writer. With the PrinterImpl1, we don't have an easy way to check the contents of the message, as we print directly to the console. Checking, in this case, is visual, so we can check acceptance criteria 6:

  testWriter := TestWriter{} 
  normal = NormalPrinter{ 
    Msg: expectedMessage, 
    Printer: &PrinterImpl2{ 
      Writer:&testWriter, 
    }, 
  } 
 
  err = normal.Print() 
  if err != nil { 
    t.Error(err.Error()) 
  } 
 
  if testWriter.Msg != expectedMessage { 
    t.Errorf("The expected message on the io.Writer doesn't match actual.
  Actual: %s
Expected: %s
", testWriter.Msg, expectedMessage) 
  } 
} 

The second part of NormalPrinter tests uses PrinterImpl2, the one that needs an io.Writer interface implementer. We reuse our TestWriter struct here to check the contents of the message. So, in short, we want a NormalPrinter struct that accepts a Msg of type string and a Printer of type PrinterAPI. At this point, if I use the Print method, I shouldn't get any error, and the Msg field on TestWriter must contain the message we passed to NormalPrinter on its initialization.

Let's run the tests:

$ go test -v -run=TestNormalPrinter_Print .
=== RUN   TestNormalPrinter_Print
--- FAIL: TestNormalPrinter_Print (0.00s)
    bridge_test.go:72: Not implemented yet
    bridge_test.go:85: Not implemented yet
    bridge_test.go:89: The expected message on the io.Writer doesn't match actual.
             Actual:
             Expected: Hello io.Writer
FAIL
exit status 1
FAIL

There is a trick to quickly check the validity of a unit test--the number of times we called t.Error or t.Errorf must match the number of messages of error on the console and the lines where they were produced. In the preceding test results, there are three errors at lines 72, 85, and 89, which exactly match the checks we wrote.

Our PacktPrinter struct will have a very similar definition to NormalPrinter at this point:

type PacktPrinter struct { 
  Msg     string 
  Printer PrinterAPI 
} 
 
func (c *PacktPrinter) Print() error { 
  return errors.New("Not implemented yet") 
} 

This covers acceptance criteria 7. And we can almost copy and paste the contents of the previous test with a few changes:

func TestPacktPrinter_Print(t *testing.T) { 
  passedMessage := "Hello io.Writer" 
  expectedMessage := "Message from Packt: Hello io.Writer" 
 
  packt := PacktPrinter{ 
    Msg:passedMessage, 
    Printer: &PrinterImpl1{}, 
  } 
 
  err := packt.Print() 
  if err != nil { 
    t.Errorf(err.Error()) 
  } 
 
  testWriter := TestWriter{} 
  packt = PacktPrinter{ 
    Msg: passedMessage, 
    Printer:&PrinterImpl2{ 
      Writer:&testWriter, 
    }, 
  } 
 
  err = packt.Print() 
  if err != nil { 
    t.Error(err.Error()) 
  } 
 
  if testWriter.Msg != expectedMessage { 
    t.Errorf("The expected message on the io.Writer doesn't match actual.
  Actual: %s
Expected: %s
", testWriter.Msg,expectedMessage) 
  } 
} 

What have we changed here? Now we have passedMessage, which represents the message we are passing to PackPrinter. We also have an expected message that contains the prefixed message from Packt. If you remember acceptance criteria 8, this abstraction must prefix the text Message from Packt: to any message that is passed to it, and, at the same time, it must be able to use any implementation of a PrinterAPI interface.

The second change is that we actually create PacktPrinter structs instead of the NormalPrinter structs; everything else is the same:

$ go test -v -run=TestPacktPrinter_Print .
=== RUN   TestPacktPrinter_Print
--- FAIL: TestPacktPrinter_Print (0.00s)
    bridge_test.go:104: Not implemented yet
    bridge_test.go:117: Not implemented yet
    bridge_test.go:121: The expected message on the io.Writer d
oesn't match actual.
        Actual:
        Expected: Message from Packt: Hello io.Writer
FAIL
exit status 1
FAIL

Three checks, three errors. All tests have been covered, and we can finally move on to the implementation.

Implementation

We will start implementing in the same order that we created our tests, first with the PrinterImpl1 definition:

type PrinterImpl1 struct{} 
func (d *PrinterImpl1) PrintMessage(msg string) error { 
  fmt.Printf("%s
", msg) 
  return nil 
} 

Our first API takes the message msg and prints it to the console. In the case of an empty string, nothing will be printed. This is enough to pass the first test:

$ go test -v -run=TestPrintAPI1 .
=== RUN   TestPrintAPI1
Hello
--- PASS: TestPrintAPI1 (0.00s)
PASS
ok

You can see the Hello message in the second line of the output of the test, just after the RUN message.

The PrinterImpl2 struct isn't very complex either. The difference is that instead of printing to the console, we are going to write on an io.Writer interface, which must be stored in the struct:

type PrinterImpl2 struct { 
  Writer io.Writer 
} 
 
func (d *PrinterImpl2) PrintMessage(msg string) error { 
  if d.Writer == nil { 
    return errors.New("You need to pass an io.Writer to PrinterImpl2") 
  } 
 
  fmt.Fprintf(d.Writer, "%s", msg) 
  return nil 
} 

As defined in our tests, we checked the contents of the Writer field first and returned the expected error message You need to pass an io.Writer to PrinterImpl2 , if nothing is stored. This is the message we'll check later in the test. Then, the fmt.Fprintf method takes an io.Writer interface as the first field and a message formatted as the rest, so we simply forward the contents of the msg argument to the io.Writer provided:

$ go test -v -run=TestPrintAPI2 .
=== RUN   TestPrintAPI2
--- PASS: TestPrintAPI2 (0.00s)
PASS
ok

Now we'll continue with the normal printer. This printer must simply forward the message to the PrinterAPI interface stored without any modification. In our test, we are using two implementations of PrinterAPI--one that prints to the console and one that writes to an io.Writer interface:

type NormalPrinter struct { 
  Msg     string 
  Printer PrinterAPI 
} 
 
func (c *NormalPrinter) Print() error { 
  c.Printer.PrintMessage(c.Msg) 
  return nil 
}

We returned nil as no error has occurred. This should be enough to pass the unit tests:

$ go test -v -run=TestNormalPrinter_Print .

=== RUN   TestNormalPrinter_Print

Hello io.Writer

--- PASS: TestNormalPrinter_Print (0.00s)

PASS

ok

In the preceding output, you can see the Hello io.Writer message that the PrinterImpl1 struct writes to stdout. We can consider this check as having passed:

Finally, the PackPrinter method is similar to NormalPrinter, but just prefixes every message with the text Message from Packt: :

type PacktPrinter struct { 
  Msg     string 
  Printer PrinterAPI 
} 
 
func (c *PacktPrinter) Print() error { 
  c.Printer.PrintMessage(fmt.Sprintf("Message from Packt: %s", c.Msg)) 
  return nil 
} 

Like in the NormalPrinter method, we accepted a Msg string and a PrinterAPI implementation in the Printer field. Then we used the fmt.Sprintf method to compose a new string with the text Message from Packt: and the provided message. We took the composed text and passed it to the PrintMessage method of PrinterAPI stored in the Printer field of the PacktPrinter struct:

$ go test -v -run=TestPacktPrinter_Print .
=== RUN   TestPacktPrinter_Print
Message from Packt: Hello io.Writer
--- PASS: TestPacktPrinter_Print (0.00s)
PASS
ok

Again, you can see the results of using PrinterImpl1 for writing to stdout with the text Message from Packt: Hello io.Writer. This last test should cover all of our code in the Bridge pattern. As you have seen previously, you can check the coverage by using the -cover flag:

$ go test -cover .
ok      
2.622s  coverage: 100.0% of statements

Wow! 100% coverage-this looks good. However, this doesn't mean that the code is perfect. We haven't checked that the contents of the messages weren't empty, maybe something that should be avoided, but it isn't a part of our requirements, which is also an important point. Just because some feature isn't in the requirements or the acceptance criteria doesn't mean that it shouldn't be covered.

Reuse everything with the Bridge pattern

With the Bridge pattern, we have learned how to uncouple an object and its implementation for the PrintMessage method. This way, we can reuse its abstractions as well as its implementations. We can swap the printer abstractions as well as the printer APIs as much as we want without affecting the user code.

We have also tried to keep things as simple as possible, but I'm sure that you have realized that all implementations of the PrinterAPI interface could have been created using a factory. This would be very natural, and you could find many implementations that have followed this approach. However, we shouldn't get into over-engineering, but should analyze each problem to make a precise design of its needs and finds the best way to create a reusable, maintainable, and readable source code. Readable code is commonly forgotten, but a robust and uncoupled source code is useless if nobody can understand it to maintain it. It's like a book of the tenth century--it could be a precious story but pretty frustrating if we have difficulty understanding its grammar.

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

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