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.
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.
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.
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).
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).
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:
ModernPrinter
interface.LegacyPrinter
interface.ModernPrinter
, it must call the LegacyPrinter
interface under the hood, prefixing it with the text 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
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.
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.
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.
3.12.166.255