What does this mean for Go?

Typically, when discussing OCP, the examples are littered with abstract classes, inheritance, virtual functions, and all kinds of things that Go doesn't have. Or does it?

What is an abstract class really? What is it actually trying to achieve?

It's trying to provide a place for code that is shared between several implementations. We can do that in Go—it's called composition. You can see it at work in the following code:

type rowConverter struct {
}

// populate the supplied Person from *sql.Row or *sql.Rows object
func (d *rowConverter) populate(in *Person, scan func(dest ...interface{}) error) error {
return scan(in.Name, in.Email)
}

type LoadPerson struct {
// compose the row converter into this loader
rowConverter
}

func (loader *LoadPerson) ByID(id int) (Person, error) {
row := loader.loadFromDB(id)

person := Person{}
// call the composed "abstract class"
err := loader.populate(&person, row.Scan)

return person, err
}

type LoadAll struct {
// compose the row converter into this loader
rowConverter
}

func (loader *LoadPerson) All() ([]Person, error) {
rows := loader.loadAllFromDB()
defer rows.Close()

output := []Person{}
for rows.Next() {
person := Person{}

// call the composed "abstract class"
err := loader.populate(&person, rows.Scan)
if err != nil {
return nil, err
}
}

return output, nil
}

In the preceding example, we have extracted some of the shared logic into a rowConverter struct. Then, by embedding that struct in the other structs, we can use it without any changes. We have achieved the goals of the abstract class and OCP. Our code is open; we can embed wherever we like but closed. The embedded class has no knowledge of the fact that it was embedded, nor did it require any changes to be used.

Earlier, we defined closed as remaining unchanged, but restricted the scope to only the parts of the API that were exported or used by others. It is not reasonable to expect that internal implementation details, including private member variables, should never change. The best way to achieve this is to hide those implementation details. This is called encapsulation.

At the package level, encapsulation is simple: we make it private. A good rule of thumb here is to make everything private and only make things public when you really have to. Again, my justification is risk and work avoidance. The moment you export something is the moment that someone could rely on it. Once they rely on it, it should become closed; you have to maintain it, and any changes have a higher risk of breaking something. With proper encapsulation, changes within a package should be invisible to existing users.

At the object level, private doesn't mean what it does in other languages, so we have to learn to behave ourselves. Accessing private member variables leaves the objects tightly coupled, a decision that will come back to bite us. 

One of my favorite features of Go's type system is the ability to attach methods to just about anything. Let's say you are writing an HTTP handler for a health check. It does nothing more than return the status 204 (No Content). The interface we need to satisfy is as follows:

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

A simple implementation might look as shown in the following code:

// a HTTP health check handler in long form
type healthCheck struct {
}

func (h *healthCheck) ServeHTTP(resp http.ResponseWriter, _ *http.Request) {
resp.WriteHeader(http.StatusNoContent)
}

func healthCheckUsage() {
http.Handle("/health", &healthCheckLong{})
}

We could create a new struct to implement an interface, but that's going to be at least five lines. We can reduce it to three, as shown in the following code:

// a HTTP health check handler in short form
func healthCheck(resp http.ResponseWriter, _ *http.Request) {
resp.WriteHeader(http.StatusNoContent)
}

func healthCheckUsage() {
http.Handle("/health", http.HandlerFunc(healthCheck))
}

The secret sauce, in this case, is hidden in the standard library. We are casting our function into the http.HandlerFunc type, which has a ServeHTTP method attached to it. This nifty little trick makes it easy for us to satisfy the http.Handler interface. As we have already seen in this chapter, moving towards interfaces leads us to less coupled code that is easier to maintain and extend.

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

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