The next pattern we'll see in this chapter is the Facade pattern. When we discussed the Proxy pattern, you got to know that it was a way to wrap an type to hide some of its features of complexity from the user. Imagine that we group many proxies in a single point such as a file or a library. This could be a Facade pattern.
A facade, in architectural terms, is the front wall that hides the rooms and corridors of a building. It protects its inhabitants from cold and rain, and provides them privacy. It orders and divides the dwellings.
The Facade design pattern does the same, but in our code. It shields the code from unwanted access, orders some calls, and hides the complexity scope from the user.
You use Facade when you want to hide the complexity of some tasks, especially when most of them share utilities (such as authentication in an API). A library is a form of facade, where someone has to provide some methods for a developer to do certain things in a friendly way. This way, if a developer needs to use your library, he doesn't need to know all the inner tasks to retrieve the result he/she wants.
So, you use the Facade design pattern in the following scenarios:
As an example, we are going to take the first steps toward writing our own library that accesses OpenWeatherMaps
service. In case you are not familiar with OpenWeatherMap
service, it is an HTTP service that provides you with live information about weather, as well as historical data on it. The HTTP REST API is very easy to use, and will be a good example on how to create a Facade pattern for hiding the complexity of the network connections behind the REST service.
The OpenWeatherMap
API gives lots of information, so we are going to focus on getting live weather data in one city in some geo-located place by using its latitude and longitude values. The following are the requirements and acceptance criteria for this design pattern:
OpenWeatherMap
service will pass through it.To start with our API Facade, we will need an interface with the methods asked in acceptance criteria 2 and acceptance criteria 3:
type CurrentWeatherDataRetriever interface { GetByCityAndCountryCode(city, countryCode string) (Weather, error) GetByGeoCoordinates(lat, lon float32) (Weather, error) }
We will call acceptance criteria 2
GetByCityAndCountryCode
; we will also need a city name and a country code in the string format. A country code is a two-character code, which represents the International Organization for Standardization (ISO) name of world countries. It returns a Weather
value, which we will define later, and an error if something goes wrong.
Acceptance criteria 3 will be called GetByGeoCoordinates
, and will need latitude and longitude values in the float32
format. It will also return a Weather
value and an error. The Weather
value is going to be defined according to the returned JSON that the OpenWeatherMap
API works with. You can find the description of this JSON at the webpage http://openweathermap.org/current#current_JSON.
If you look at the JSON definition, it has the following type:
type Weather struct { ID int `json:"id"` Name string `json:"name"` Cod int `json:"cod"` Coord struct { Lon float32 `json:"lon"` Lat float32 `json:"lat"` } `json:"coord"` Weather []struct { Id int `json:"id"` Main string `json:"main"` Description string `json:"description"` Icon string `json:"icon"` } `json:"weather"` Base string `json:"base"` Main struct { Temp float32 `json:"temp"` Pressure float32 `json:"pressure"` Humidity float32 `json:"humidity"` TempMin float32 `json:"temp_min"` TempMax float32 `json:"temp_max"` } `json:"main"` Wind struct { Speed float32 `json:"speed"` Deg float32 `json:"deg"` } `json:"wind"` Clouds struct { All int `json:"all"` } `json:"clouds"` Rain struct { ThreeHours float32 `json:"3h"` } `json:"rain"` Dt uint32 `json:"dt"` Sys struct { Type int `json:"type"` ID int `json:"id"` Message float32 `json:"message"` Country string `json:"country"` Sunrise int `json:"sunrise"` Sunset int `json:"sunset"` }`json:"sys"` }
It's quite a long struct, but we have everything that a response could include. The struct is called Weather
, as it is composed of an ID, a name and a Code (Cod
), and a few anonymous structs, which are: Coord
, Weather
, Base
, Main
, Wind
, Clouds
, Rain
, Dt
, and Sys
. We could write these anonymous structs outside of the Weather
struct by giving them a name, but it would only be useful if we have to work with them separately.
After every member and struct within our Weather
struct, you can find a `json:"something"`
line. This comes in handy when differentiating between the JSON key name and your member name. If the JSON key is something
, we aren't forced to call our member something
. For example, our ID member will be called id
in the JSON response.
Why don't we give the name of the JSON keys to our types? Well, if your fields in your type are lowercase, the encoding/json
package won't parse them correctly. Also, that last annotation provides us a certain flexibility, not only in terms of changing the members' names, but also of omitting some key if we don't need it, with the following signature:
`json:"something,omitempty"
With omitempty
at the end, the parse won't fail if this key is not present in the bytes representation of the JSON key.
Okay, our acceptance criteria 1 ask for a single point of access to the API. This is going to be called CurrentWeatherData
:
type CurrentWeatherData struct { APIkey string }
The CurrentWeatherData
type has an API key as public member to work. This is because you have to be a registered user in OpenWeatherMap
to enjoy their services. Refer to the OpenWeatherMap
API's webpage for documentation on how to get an API key. We won't need it in our example, because we aren't going to do integration tests.
We need mock data so that we can write a mock
function to retrieve the data. When sending an HTTP request, the response is contained in a member called body in the form of an io.Reader
. We have already worked with types that implement the io.Reader
interface, so this should look familiar to you. Our mock
function appears like this:
func getMockData() io.Reader { response := `{ "coord":{"lon":-3.7,"lat":40.42},"weather : [{"id":803,"main":"Clouds","description":"broken clouds","icon":"04n"}],"base":"stations","main":{"temp":303.56,"pressure":1016.46,"humidity":26.8,"temp_min":300.95,"temp_max":305.93},"wind":{"speed":3.17,"deg":151.001},"rain":{"3h":0.0075},"clouds":{"all":68},"dt":1471295823,"sys":{"type":3,"id":1442829648,"message":0.0278,"country":"ES","sunrise":1471238808,"sunset":1471288232},"id":3117735,"name":"Madrid","cod":200}` r := bytes.NewReader([]byte(response)) return r }
This preceding mocked data was produced by making a request to OpenWeatherMap
using an API key. The response
variable is a string containing a JSON response. Take a close look at the grave accent (`
) used to open and close the string. This way, you can use as many quotes as you want without any problem.
Further on, we use a special function in the bytes package called NewReader
, which accepts an slice of bytes (which we create by converting the type from string), and returns an io.Reader
implementor with the contents of the slice. This is perfect to mimic the Body
member of an HTTP response.
We will write a test to try response parser
. Both methods return the same type, so we can use the same JSON parser
for both:
func TestOpenWeatherMap_responseParser(t *testing.T) { r := getMockData() openWeatherMap := CurrentWeatherData{APIkey: ""} weather, err := openWeatherMap.responseParser(r) if err != nil { t.Fatal(err) } if weather.ID != 3117735 { t.Errorf("Madrid id is 3117735, not %d ", weather.ID) } }
In the preceding test, we first asked for some mock data, which we store in the variable r
. Later, we created a type of CurrentWeatherData
, which we called openWeatherMap
. Finally, we asked for a weather value for the provided io.Reader
interface that we store in the variable weather
. After checking for errors, we make sure that the ID is the same as the one stored in the mock data that we got from the getMockData
method.
We have to declare the responseParser
method before running tests, or the code won't compile:
func (p *CurrentWeatherData) responseParser(body io.Reader) (*Weather, error) { return nil, fmt.Errorf("Not implemented yet") }
With all the aforementioned, we can run this test:
go test -v -run=responseParser . === RUN TestOpenWeatherMap_responseParser --- FAIL: TestOpenWeatherMap_responseParser (0.00s) facade_test.go:72: Not implemented yet FAIL exit status 1 FAIL
Okay. We won't write more tests, because the rest would be merely integration tests, which are outside of the scope of explanation of a structural pattern, and will force us to have an API key as well as an Internet connection. If you want to see what the integration tests look like for this example, refer to the code that comes bundled with the book.
First of all, we are going to implement the parser that our methods will use to parse the JSON response from the OpenWeatherMap
REST API:
func (p *CurrentWeatherData) responseParser(body io.Reader) (*Weather, error) { w := new(Weather) err := json.NewDecoder(body).Decode(w) if err != nil { return nil, err } return w, nil }
And this should be enough to pass the test by now:
go test -v -run=responseParser .
=== RUN TestOpenWeatherMap_responseParser
--- PASS: TestOpenWeatherMap_responseParser (0.00s)
PASS
ok
At least we have our parser well tested. Let's structure our code to look like a library. First, we will create the methods to retrieve the weather of a city by its name and its country code, and the method that uses its latitude and longitude:
func (c *CurrentWeatherData) GetByGeoCoordinates(lat, lon float32) (weather *Weather, err error) { return c.doRequest( fmt.Sprintf("http://api.openweathermap.org/data/2.5/weather q=%s,%s&APPID=%s", lat, lon, c.APIkey)) } func (c *CurrentWeatherData) GetByCityAndCountryCode(city, countryCode string) (weather *Weather, err error) { return c.doRequest( fmt.Sprintf("http://api.openweathermap.org/data/2.5/weather?lat=%f&lon=%f&APPID=%s", city, countryCode, c.APIkey) ) }
A piece of cake? Of course! Everything must be as easy as possible, and it is a sign of a good job. The complexity in this facade is to create connections to the OpenWeatherMap
API, and control the possible errors. This problem is shared between all the Facade methods in our example, so we don't need to write more than one API call right now.
What we do is pass the URL that the REST API needs in order to return the information we desire. This is achieved by the fmt.Sprintf
function, which formats the strings in each case. For example, to gather the data using a city name and a country code, we use the following string:
fmt.Sprintf("http://api.openweathermap.org/data/2.5/weather?lat=%f&lon=%f&APPID=%s", city, countryCode, c.APIkey)
This takes the pre-formatted string
https://openweathermap.org/api
and formats it by replacing each %s
specifier with the city, the countryCode
that we introduced in the arguments, and the API key member of the CurrentWeatherData
type.
But, we haven't set any API key! Yes, because this is a library, and the users of the library will have to use their own API keys. We are hiding the complexity of creating the URIs, and handling the errors.
Finally, the doRequest
function is a big fish, so we will see it in detail, step by step:
func (o *CurrentWeatherData) doRequest(uri string) (weather *Weather, err error) { client := &http.Client{} req, err := http.NewRequest("GET", uri, nil) if err != nil { return } req.Header.Set("Content-Type", "application/json")
First, the signature tells us that the doRequest
method accepts a URI string, and returns a pointer to the Weather
variable and an error. We start by creating an http.Client
class, which will make the requests. Then, we create a request object, which will use the GET
method, as described in the OpenWeatherMap
webpage, and the URI we passed. If we were to use a different method, or more than one, they would have to be brought about by arguments in the signature. Nevertheless, we will use just the GET
method, so we could hardcode it there.
Then, we check whether the request object has been created successfully, and set a header that says that the content type is a JSON:
resp, err := client.Do(req) if err != nil { return } if resp.StatusCode != 200 { byt, errMsg := ioutil.ReadAll(resp.Body) if errMsg == nil { errMsg = fmt.Errorf("%s", string(byt)) } err = fmt.Errorf("Status code was %d, aborting. Error message was: %s ",resp.StatusCode, errMsg) return }
Then we make the request, and check for errors. Because we have given names to our return types, if any error occurs, we just have to return the function, and Go will return the variable err
and the variable weather
in the state they were in at that precise moment.
We check the status code of the response, as we only accept 200 as a good response. If 200 isn't returned, we will create an error message with the contents of the body and the status code returned:
weather, err = o.responseParser(resp.Body) resp.Body.Close() return }
Finally, if everything goes well, we use the responseParser
function we wrote earlier to parse the contents of Body, which is an io.Reader
interface. Maybe you are wondering why we aren't controlling err
from the response parser
method. It's funny, because we are actually controlling it. responseParser
and doRequest
have the same return signature. Both return a Weather
pointer and an error (if any), so we can return directly whatever the result was.
We have the first milestone for a library for the OpenWeatherMap
API using the facade pattern. We have hidden the complexity of accessing the OpenWeatherMap
REST API in the doRequest
and responseParser
functions, and the users of our library have an easy-to-use syntax to query the API. For example, to retrieve the weather for Madrid, Spain, a user will only have to introduce arguments and an API key at the beginning:
weatherMap := CurrentWeatherData{*apiKey} weather, err := weatherMap.GetByCityAndCountryCode("Madrid", "ES") if err != nil { t.Fatal(err) } fmt.Printf("Temperature in Madrid is %f celsius ", weather.Main.Temp-273.15)
The console output for the weather in Madrid at the moment of writing this chapter is the following:
$ Temperature in Madrid is 30.600006 celsius
A typical summer day!
3.147.84.169