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 patterns deal with the composition of classes or objects while keeping them extensible, flexible 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.
Video
Objectives
In this lab you will
understand a real-world scenario and choose when to apply the appropriate design pattern.
design and implement the adapter design pattern.
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.
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.).
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.
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:
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.
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.
publicinterfaceWeatherGeo{// 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.
publicclassOpenMeteoWeatherimplements WeatherGeo {privatefinal String API_URL ="https://api.open-meteo.com/v1/forecast";// Returns 10 days weather forecasts for the given geo coordinates
@Overridepublic 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();}returnnull;}publicstatic 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 =newint[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 =newint[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();returnnull;}}}
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.
If your instructor is using GitHub classroom, then you should click on your class submission link,
link your GitHub username to your name if you have not already done so, accept the assignment, clone the
repository into your local
development environment, and push the code to the remote repository on GitHub. Please make sure that your
written
answers are included in either a README (Markdown) file or a PDF file.
Lab dues dates are listed on GitHub classroom unless otherwise
noted.
If your instructor is using GitHub classroom, your submission will be
auto-graded
by running the included unit tests as well as manually graded for correctness, style, and quality.
How to submit your lab to GitHub Classroom
The video below demonstrates how to submit your work to GitHub classroom
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.,
and ). 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.