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.
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.
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.
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.
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:
PrinterAPI
that accepts a message to printio.Writer
interfacePrinter
abstraction with a Print
method to implement in printing typesnormal
printer object, which will implement the Printer
and the PrinterAPI
interfacenormal
printer will forward the message directly to the implementationPackt
printer, which will implement the Printer
abstraction and the PrinterAPI
interfacePackt
printer will append the message Message from Packt:
to all printsLet'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.
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.
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.
3.135.247.68