Adapter design pattern

One of the most commonly used structural patterns is the Adapter pattern. Like in real life, where you have plug adapters and bolt adapters, in Go, an adapter will allow us to use something that wasn't built for a specific task at the beginning.

Description

The Adapter pattern is very useful when, for example, an interface gets outdated and it's not possible to replace it easily or fast. Instead, you create a new interface to deal with the current needs of your application, which, under the hood, uses implementations of the old interface.

Adapter also helps us to maintain the open/closed principle in our apps, making them more predictable too. They also allow us to write code which uses some base that we can't modify.

Note

The open/closed principle was first stated by Bertrand Meyer in his book Object-Oriented Software Construction. He stated that code should be open to new functionality, but closed to modifications. What does it mean? Well, it implies a few things. On one hand, we should try to write code that is extensible and not only one that works. At the same time, we should try not to modify the source code (yours or other people's) as much as we can, because we aren't always aware of the implications of this modification. Just keep in mind that extensibility in code is only possible through the use of design patterns and interface-oriented programming.

Objectives

The Adapter design pattern will help you fit the needs of two parts of the code that are incompatible at first. This is the key to being kept in mind when deciding if the Adapter pattern is a good design for your problem--two interfaces that are incompatible, but which must work together, are good candidates for an Adapter pattern (but they could also use the facade pattern, for example).

Using an incompatible interface with an Adapter object

For our example, we will have an old Printer interface and a new one. Users of the new interface don't expect the signature that the old one has, and we need an Adapter so that users can still use old implementations if necessary (to work with some legacy code, for example).

Requirements and acceptance criteria

Having an old interface called LegacyPrinter and a new one called ModernPrinter, create a structure that implements the ModernPrinter interface and can use the LegacyPrinter interface as described in the following steps:

  1. Create an Adapter object that implements the ModernPrinter interface.
  2. The new Adapter object must contain an instance of the LegacyPrinter interface.
  3. When using ModernPrinter, it must call the LegacyPrinter interface under the hood, prefixing it with the text Adapter.

Unit testing our Printer adapter

We will write the legacy code first, but we won't test it as we should imagine that it isn't our code:

type LegacyPrinter interface { 
  Print(s string) string 
} 
type MyLegacyPrinter struct {} 
 
func (l *MyLegacyPrinter) Print(s string) (newMsg string) { 
  newMsg = fmt.Sprintf("Legacy Printer: %s
", s) 
  println(newMsg) 
  return 
} 

The legacy interface called LegacyPrinter has a Print method that accepts a string and returns a message. Our MyLegacyPrinter struct implements the LegacyPrinter interface and modifies the passed string by prefixing the text Legacy Printer:. After modifying the text, the MyLegacyPrinter struct prints the text on the console, and then returns it.

Now we'll declare the new interface that we'll have to adapt:

type ModernPrinter interface { 
  PrintStored() string 
} 

In this case, the new PrintStored method doesn't accept any string as an argument, because it will have to be stored in the implementers in advance. We will call our Adapter pattern's PrinterAdapter interface:

type PrinterAdapter struct{ 
  OldPrinter LegacyPrinter 
  Msg        string 
} 
func(p *PrinterAdapter) PrintStored() (newMsg string) { 
  return 
} 

As mentioned earlier, the PrinterAdapter adapter must have a field to store the string to print. It must also have a field to store an instance of the LegacyPrinter adapter. So let's write the unit tests:

func TestAdapter(t *testing.T){ 
  msg := "Hello World!" 

We will use the message Hello World! for our adapter. When using this message with an instance of the MyLegacyPrinter struct, it prints the text Legacy Printer: Hello World!:

adapter := PrinterAdapter{OldPrinter: &MyLegacyPrinter{}, Msg: msg} 

We created an instance of the PrinterAdapter interface called adapter. We passed an instance of the MyLegacyPrinter struct as the LegacyPrinter field called OldPrinter. Also, we set the message we want to print in the Msg field:

returnedMsg := adapter.PrintStored() 

if returnedMsg != "Legacy Printer: Adapter: Hello World!
" { 
  t.Errorf("Message didn't match: %s
", returnedMsg) 
} 

Then we used the PrintStored method of the ModernPrinter interface; this method doesn't accept any argument and must return the modified string. We know that the MyLegacyPrinter struct returns the passed string prefixed with the text LegacyPrinter:, and the adapter will prefix it with the text Adapter: So, in the end, we must have the text Legacy Printer: Adapter: Hello World! .

As we are storing an instance of an interface, we must also check that we handle the situation where the pointer is nil. This is done with the following test:

adapter = PrinterAdapter{OldPrinter: nil, Msg: msg} 
returnedMsg = adapter.PrintStored() 

if returnedMsg != "Hello World!" { 
  t.Errorf("Message didn't match: %s
", returnedMsg) 
} 

If we don't pass an instance of the LegacyPrinter interface, the Adapter must ignore its adapt nature, and simply print and return the original message. Time to run our tests; consider the following:

$ go test -v .
=== RUN   TestAdapter
--- FAIL: TestAdapter (0.00s)
        adapter_test.go:11: Message didn't match: 
        adapter_test.go:17: Message didn't match: 
FAIL
exit status 1
FAIL

Implementation

To make our single test pass, we must reuse the old MyLegacyPrinter that is stored in PrinterAdapter struct:

type PrinterAdapter struct{ 
  OldPrinter LegacyPrinter 
  Msg        string 
} 
 
func(p *PrinterAdapter) PrintStored() (newMsg string) { 
  if p.OldPrinter != nil { 
    newMsg = fmt.Sprintf("Adapter: %s", p.Msg) 
    newMsg = p.OldPrinter.Print(newMsg) 
  } 
  else { 
    newMsg = p.Msg 
  } 
return 
} 

In the PrintStored method, we check whether we actually have an instance of a LegacyPrinter. In this case, we compose a new string with the stored message and the Adapter prefix to store it in the returning variable (called newMsg). Then we use the pointer to the MyLegacyPrinter struct to print the composed message using the LegacyPrinter interface.

In case there is no LegacyPrinter instance stored in the OldPrinter field, we simply assign the stored message to the returning variable newMsg and return the method. This should be enough to pass our tests:

$ go test -v .
=== RUN   TestAdapter
Legacy Printer: Adapter: Hello World!
--- PASS: TestAdapter (0.00s)
PASS
ok

Perfect! Now we can still use the old LegacyPrinter interface by using this Adapter while we use the ModernPrinter interface for future implementations. Just keep in mind that the Adapter pattern must ideally just provide the way to use the old LegacyPrinter and nothing else. This way, its scope will be more encapsulated and more maintainable in the future.

Examples of the Adapter pattern in Go's source code

You can find Adapter implementations at many places in the Go language's source code. The famous http.Handler interface has a very interesting adapter implementation. A very simple, Hello World server in Go is usually done like this:

package main 
 
import ( 
    "fmt" 
    "log" 
    "net/http" 
) 
type MyServer struct{ 
  Msg string 
} 
func (m *MyServer) ServeHTTP(w http.ResponseWriter,r *http.Request){ 
  fmt.Fprintf(w, "Hello, World") 
} 
 
func main() { 
  server := &MyServer{ 
  Msg:"Hello, World", 
} 
 
http.Handle("/", server)  
log.Fatal(http.ListenAndServe(":8080", nil)) 
} 

The HTTP package has a function called Handle (like a static method in Java) that accepts two parameters--a string to represent the route and a Handler interface. The Handler interface is like the following:

type Handler interface { 
  ServeHTTP(ResponseWriter, *Request) 
} 

We need to implement a ServeHTTP method that the server side of an HTTP connection will use to execute its context. But there is also a function HandlerFunc that allows you to define some endpoint behavior:

func main() { 
  http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 
    fmt.Fprintf(w, "Hello, World") 
  }) 
 
  log.Fatal(http.ListenAndServe(":8080", nil)) 
} 

The HandleFunc function is actually part of an adapter for using functions directly as ServeHTTP implementations. Read the last sentence slowly again--can you guess how it is done?

type HandlerFunc func(ResponseWriter, *Request) 
 
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { 
  f(w, r) 
} 

We can define a type that is a function in the same way that we define a struct. We make this function-type to implement the ServeHTTP method. Finally, from the ServeHTTP function, we call the receiver itself f(w, r).

You have to think about the implicit interface implementation of Go. When we define a function like func(ResponseWriter, *Request), it is implicitly being recognized as HandlerFunc. And because the HandleFunc function implements the Handler interface, our function implements the Handler interface implicitly too. Does this sound familiar to you? If A = B and B = C, then A = C. Implicit implementation gives a lot of flexibility and power to Go, but you must also be careful, because you don't know if a method or function could be implementing some interface that could provoke undesirable behaviors.

We can find more examples in Go's source code. The io package has another powerful example with the use of pipes. A pipe in Linux is a flow mechanism that takes something on the input and outputs something else on the output. The io package has two interfaces, which are used everywhere in Go's source code--the io.Reader and the io.Writer interface:

type Reader interface { 
  Read(p []byte) (n int, err error) 
} 
 
type Writer interface { 
  Write(p []byte) (n int, err error) 
} 

We use io.Reader everywhere, for example, when you open a file using os.OpenFile, it returns a file, which, in fact, implements the io.Reader interface. Why is it useful? Imagine that you write a Counter struct that counts from the number you provide to zero:

type Counter struct {} 
func (f *Counter) Count(n uint64) uint64 { 
  if n == 0 { 
    println(strconv.Itoa(0)) 
    return 0 
  } 
 
  cur := n 
  println(strconv.FormatUint(cur, 10)) 
  return f.Count(n - 1) 
} 

If you provide the number 3 to this small snippet, it will print the following:

3
2
1

Well, not really impressive! What if I want to write to a file instead of printing? We can implement this method too. What if I want to print to a file and to the console? Well, we can implement this method too. We must modularize it a bit more by using the io.Writer interface:

type Counter struct { 
  Writer io.Writer 
} 
func (f *Counter) Count(n uint64) uint64 { 
  if n == 0 { 
    f.Writer.Write([]byte(strconv.Itoa(0) + "
")) 
    return 0 
  } 
 
  cur := n 
  f.Writer.Write([]byte(strconv.FormatUint(cur, 10) + "
")) 
  return f.Count(n - 1) 
}
 

Now we provide an io.Writer in the Writer field. This way, we could create the counter like this: c := Counter{os.Stdout}, and we will get a console Writer. But wait a second, we haven't solved the issue where we wanted to take the count to many Writer consoles. But we can write a new Adapter with an io.Writer and, using a Pipe() to connect a reader with a writer, we can read on the opposite extreme. This way, you can solve the issue where these two interfaces, Reader and Writer, which are incompatible, can be used together.

In fact, we don't need to write the Adapter--the Go's io library has one for us in io.Pipe(). The pipe will allow us to convert a Reader to a Writer interface. The io.Pipe() method will provide us a Writer (the entrance of the pipe) and a Reader (the exit) to play with. So let's create a pipe, and assign the provided writer to the Counter of the preceding example:

pipeReader, pipeWriter := io.Pipe() 
defer pw.Close() 
defer pr.Close() 
 
counter := Counter{ 
  Writer: pipeWriter, 
} 

Now we have a Reader interface where we previously had a Writer. Where can we use the Reader? The io.TeeReader function helps us to copy the stream of data from a Reader interface to the Writer interface and, it returns a new Reader that you can still use to stream data again to a second writer. So we will stream the data from the same reader to two writers--the file and the Stdout.

tee := io.TeeReader(pipeReader, file) 

So now we know that we are writing to a file that we have passed to the TeeReader function. We still need to print to the console. The io.Copy adapter can be used like TeeReader--it takes a reader and writes its contents to a writer:

go func(){ 
  io.Copy(os.Stdout, tee) 
}() 

We have to launch the Copy function in a different Go routine so that the writes are performed concurrently, and one read/write doesn't block a different read/write. Let's modify the counter variable to make it count till 5 again:

counter.Count(5) 

With this modification to the code, we get the following output:

$ go run counter.go
5
4
3
2
1
0

Okay, the count has been printed on the console. What about the file?

$ cat /tmp/pipe
5
4
3
2
1
0

Awesome! By using the io.Pipe() adapter provided in the Go native library, we have uncoupled our counter from its output, and we have adapted a Writer interface to a Reader one.

What the Go source code tells us about the Adapter pattern

With the Adapter design pattern, you have learned a quick way to achieve the open/close principle in your applications. Instead of modifying your old source code (something which could not be possible in some situations), you have created a way to use the old functionality with a new signature.

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

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