Edit Page

Lab 5: The Adapter Design Pattern

The goal of this lab is to use the adapter design pattern, a structural design pattern, to solve a real-world problem.

Design patterns are proven solutions to solve recurring design problems to design flexible and reusable object-oriented software; that is, objects that are easier to implement, change, test and reuse.

Structural pat­terns deal with the composition of classes or objects while keep­ing them extensible, flex­i­ble and efficient.

The adapter design pattern is one of the twenty-three well-known Gang of Four (GoF) design patterns. It is classified under the category of structural patterns as it provides an elegant and efficient way to work with two incompatible interfaces. The adapter pattern is used to convert the interface of a class into another interface the client expects.

Adapter

Video

Objectives

In this lab you will

  1. understand a real-world scenario and choose when to apply the appropriate design pattern.
  2. design and implement the adapter design pattern.
  3. write unit tests and apply Test-Driven Development (TDD).

Requirement and Tools

  • Java JDK 1.11 or above. This is a hard requirement because we’re going to use HttpClient, which was introduced in Java 11.
  • An IDE (e.g., Apache NetBeans, Eclipse or IntelliJ IDEA).
  • If you do not like to use an IDE, you may use any text editor (e.g., VS Code, jEdit, etc.) and the Javac compiler.
  • Apache HttpCore. We are going to use Apache HttpCore URIBuilder to construct a valid URL with parameters.
  • Jackson is a 3rd party Java library that handles the serialization and deserialization of Java objects and their JSON representations. We are going to use it for JSON deserialization (converting a JSON string into an object.).
  • The unit testing framework, Junit
  • Apache Maven is a build automation tool to build projects and manage their dependencies.

Getting Started

If your instructor is using GitHub classroom, you will need to accept the assignment using the link below, clone the template repository, and import it as a project into your IDE.

If your instructor is not using GitHub classroom, clone and import the template project at https://github.com/cpit252/lab-05 ↗.

Problem Statement

A developer is working on a fully customized TV display for a business. The display lists weather information, local transportation information, and business related offers and messages. She started working on the subsystem that shows weather related information. She wanted to allow the user to get the daily weather forecasts by entering a city name. She explored using existing APIs and publicly available weather data sources to find a trusted and reliable service to use for pulling weather information for a given city. As she explores weather APIs and available options, she realized that APIs vary in complexity and API usage cost can increase drastically. After considering many options, she reluctantly decided to experiment with a Free Weather API called Open-Meteo. This API takes the geographic coordinates of a city (latitude and longitude) as an input and returns the daily weather forecasts. However, she does not want the user to deal with the complexity of supplying the geo coordinates to the system and wanted the user to enter a city name instead. This leads to an input incompatibility between her system and the API. In addition to the issue of incompatible interfaces, weather APIs cost and usage may change at any time, so she wanted to design her system with the following goals in mind:

  1. Some APIs provide an incompatible interface with her system. For instance, some APIs request a geographic coordinates (latitude and longitude) as an input but she expects the input to be a city name.
  2. She should be able to replace the weather API with any other service with minimal changes to the codebase.

Given these two issues in mind, she thought that this is a good case for the Adapter pattern.

“Program to an interface, not an implementation.” (Gang of Four 1995:18) is an OOP principle that refers to the use of interfaces or abstract classes to define the expected behavior of an object and implementing it in concrete classes. If you design your code around the concrete implementation, your code would become tightly coupled and much harder to change without breaking existing objects. If you design your code around the interface (Java interfaces or abstract classes), then the implementation is loosely coupled and much easier to change. The reason is because your design decouples the implementation class and cares about what the code is doing rather than how it does it.

To fetch weather forecasts, she needs to send an HTTP GEt request to the API endpoint /forecast and pass the GPS coordinates (lat and long) as query parameters. Example:

GET https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&past_days=10&hourly=temperature_2m,relativehumidity_2m,windspeed_10m

She will write an adapter to fix this incompatibility of interfaces issue by using a service that converts a city name into GPS coordinates, which is what the API end point takes. To get started, she wrote two interfaces for the weather: WeatherCity and WeatherGeo. The weather API is defined in a class OpenMeteoWeather, which implements the WeatherGeo. This class is considered the adaptee because it takes geo coordinates (lat and long) as parameters to return the weather forecasts. Next, she wrote an adapter class called WeatherAdapter that implements the WeatherCity interface. Below is the UML diagram and the code for her implementation.

Adapter UML diagram

WeatherCity

1
2
3
public interface WeatherCity {
    // Get weather data by the city name.
}

WeatherGeo

1
2
3
4
public interface WeatherGeo {
    // Get weather data by the geographical coordinates (latitude, longitude).

}
OpenMeteoWeather (The Adaptee) This is the adaptee class, which takes a geographic coordinates of a city and returns the daily weather forecasts. The adaptee is a class that has an incompatible interface the client can’t work with.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class OpenMeteoWeather implements WeatherGeo {

    private final String API_URL = "https://api.open-meteo.com/v1/forecast";
    // Returns 10 days weather forecasts for the given geo coordinates
    @Override
    public WeatherInfo getWeatherInfo(double latitude, double longitude) {
        // build the URL
        WeatherInfo wInfo = null;
        try {
            // Build the URL for the API endpoint
            URI uri = new URIBuilder(API_URL)
                    .addParameter("latitude", String.format("%,.2f", latitude))
                    .addParameter("longitude", String.format("%,.2f", longitude))
                    .addParameter("timezone", "Asia/Kuwait")
                    .addParameter("daily", "temperature_2m_max,temperature_2m_min")
                    .build();
            HttpResponse<String> response = HTTPHelper.sendGet(uri);
            if (response != null) {
                wInfo = parseWeatherResponse(response.body(), WeatherInfo.class);
                return wInfo;
            }

        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
        return null;
    }
    public static WeatherInfo parseWeatherResponse(String responseString, Class<?> elementClass){
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            JsonNode weatherInfoNode = objectMapper.readTree(responseString);
            WeatherInfo wInfo = new WeatherInfo();
            // Get latitude and longitude
            wInfo.setLatitude(weatherInfoNode.get("latitude").doubleValue());
            wInfo.setLongitude(weatherInfoNode.get("longitude").doubleValue());
            // Get the dates for the seven days forecasts
            Iterator<JsonNode>  datesIterator = weatherInfoNode.get("daily").get("time").elements();
            ArrayList<LocalDate> dates = new ArrayList<>();
            while(datesIterator.hasNext()){
                dates.add(LocalDate.parse(datesIterator.next().asText()));
            }
            wInfo.setDates(dates);
            // Get max temps
            JsonNode maxTempsNodes= weatherInfoNode.get("daily").get("temperature_2m_max");
            int []maxTemps = new int[maxTempsNodes.size()];
            for (int i=0; i< maxTemps.length; i++) {
                maxTemps[i] = (int) Math.round(Double.valueOf(maxTempsNodes.get(i).toString()));
            }
            wInfo.setMaxTemps(maxTemps);
            // Get min temps
            JsonNode minTempsNodes= weatherInfoNode.get("daily").get("temperature_2m_min");
            int []minTemps = new int[minTempsNodes.size()];
            for (int i=0; i< maxTemps.length; i++) {
                minTemps[i] = (int) Math.round(Double.valueOf(minTempsNodes.get(i).toString()));
            }
            wInfo.setMinTemps(minTemps);
            return wInfo;
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
}

WeatherAdapter (The Adapter) This is the adapter class, which takes a geographic coordinates of a city and returns the daily weather forecasts. The adapter is a class used as an intermediary between the client and the adaptee.

1
2
public class WeatherAdapter {
}

App.java (The client)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class App {
    public static void main(String[] args) {
        // Get the weather for Jeddah (21.543333, 39.172778)
        OpenMeteoWeather openMeteoWeather = new OpenMeteoWeather();
        GeoLocationService geoLocationService = new GeoLocationService();
        Location location = geoLocationService.search("Jeddah");
        WeatherInfo weatherInfo = openMeteoWeather.getWeatherInfo(location.getLatitude(),
                location.getLongitude());
        System.out.println(weatherInfo);
    }
}

Questions:

  1. Complete the code using the adapter design pattern?
  2. Explain how the adapter design pattern binds the client to an interface not a concrete implementation?

Deliverables and Submission

Extra Task [Optional]

If you are done with this activity, you may enable a continuos integration tool such as CircleCI ↗ to automatically run your JUnit test upon code changes. You may also add more unit tests to increase code coverage. Please embed the badge that shows the status of your build and test (passing/failing) as well as the coverage percentage into your README file (e.g., passing status image and failing status image). Please be sure to fork the repository or push to a remote repository under your own account, so you can enable the integration of CI tools in your own account.

You may refer to the lecture notes from the prerequisite course CPIT-251 on Continuous Integration (CI) and adding a code coverage badge.