Method injection

Method injection is everywhere. You probably use it every day and you don't even realize it. Have you ever written code like this?:

fmt.Fprint(os.Stdout, "Hello World")

How about this?:

req, err := http.NewRequest("POST", "/login", body)

This is method injection—the passing in of the dependency as a parameter to the request.

Let's examine the previous examples in more detail. The function signature for Fprint() is as follows:

// Fprint formats using the default formats for its operands and writes 
// to w.
It returns the number of bytes written and any write error
// encountered.
func Fprint(w io.Writer, a ...interface{}) (n int, err error)

As you can see, the first parameter, io.Writer, is a dependency for this function. What makes this different from any other function call is the fact that the dependency provides an invocation context or data to the function call.

In the first example, the dependency was required, as it is being used as the output destination. However, dependencies used in method injection are not always required. Sometimes the dependency is optional, as we can see in the following example:

func NewRequest(method, url string, body io.Reader) (*http.Request, error) {
// validate method
m, err := validateMethod(method)
if err != nil {
return nil, err
}

// validate URL
u, err := validateURL(url)
if err != nil {
return nil, err
}

// process body (if exists)
var b io.ReadCloser
if body != nil {
// read body
b = ioutil.NopCloser(body)
}

// build Request and return
req := &http.Request{
URL: u,
Method: m,
Body: b,
}

return req, nil
}

This is not the actual implementation from the standard library; I have simplified it to highlight the critical parts. In the preceding example, io.Reader is optional, and as such, is protected by a guard clause.

When applying method injection, the dependencies are specific to the current invocation, and we will frequently find ourselves needing guard clauses. To help us decide whether or not to include guard clauses, let's dive a little deeper into our examples.

In the fmt.Fprint() standard library implementation, there is no guard clause on io.Writer, meaning that supplying nil will cause the function to panic. This is because, without io.Writer, there is nowhere for the output to go.

However, in the http.NewRequest() implementation, there is a guard clause because it is possible to make an HTTP request that does not contain a request body.

So, what does that mean for the functions that we write? In most cases, we should avoid writing code that can cause a crash with a panic. Let's implement a function whose purpose is similar to Fprint() and see whether we can avoid panics. Here is the first rough implementation (with panic):

// TimeStampWriterV1 will output the supplied message to 
//writer preceded with a timestamp
func TimeStampWriterV1(writer io.Writer, message string) {
timestamp := time.Now().Format(time.RFC3339)
fmt.Fprintf(writer, "%s -> %s", timestamp, message)
}

What's the first thing that comes to mind to avoid the panic caused by a nil writer?

We could add a guard clause and return an error when io.Writer is not supplied, as shown in the following code:

// TimeStampWriterV2 will output the supplied message to 
//writer preceded with a timestamp
func TimeStampWriterV2(writer io.Writer, message string) error {
if writer == nil {
return errors.New("writer cannot be nil")
}

timestamp := time.Now().Format(time.RFC3339)
fmt.Fprintf(writer,"%s -> %s", timestamp, message)

return nil
}

While this still looks and feels like regular, valid Go code, we now have an error that only happens when we, the programmer, make a mistake. A much better option would be a reasonable default, as shown in the following code:

// TimeStampWriterV3 will output the supplied message to 
//writer preceded with a timestamp
func TimeStampWriterV3(writer io.Writer, message string) {
if writer == nil {
// default to Standard Out
writer = os.Stdout
}

timestamp := time.Now().Format(time.RFC3339)
fmt.Fprintf(writer,"%s -> %s", timestamp, message)
}

This technique is called defensive coding. The central concept is that it's better to continue working, even with a degraded experience, than to crash.

Although these examples have all been functions, method injection can be used with structs in precisely the same way. There is one caveat—do not save the injected dependency as a member variable. We are using method injection because the dependency provides function invocation context or data. Saving the dependency as a member variable causes it to be shared between calls, effectively leaking this context between requests.

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

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