Applying the 'Hide Delegate' Refactoring to Weather API Responses

Applying Martin Fowler's 'Refactoring' lessons to a real project
DelightRoom's avatar
Nov 01, 2021
Applying the 'Hide Delegate' Refactoring to Weather API Responses
The original Today's Panel screen and the newly revamped Today's Panel screen in progress

Currently, the Alarmy app provides weather information in the 'Today's Panel' tab as shown below, powered by the weather API provided by Accuweather. During the recent revamp of Today's Panel, we needed to switch our weather API from Accuweather to the Openweather API, and while implementing this migration, I decided to try out 'Hide Delegate'—one of the encapsulation refactoring techniques I learned in our engineering study group.

{
    "value": {
        "geo": { ... },
        "headline": { ... },
        "current": { ... },
        "airquality": { ... },
        "hourly_forecast": [ ... ],
        "daily_forecast": [{
                "date_time_ms": 1628175600000,
                "temperature": {
                    "min": {
                        "value": 10,
                        "unit": "C"
                    },
                    "max": {
                        "value": 35,
                        "unit": "C"
                    }
                },
                ...
            },
            ...
        ]
    }
}

I've prepared an example showing how we fetch the information needed to display the weekly forecast from the API response. When we request weather data, we get a JSON response formatted like the example below. To handle this kind of hierarchical Key-Value JSON data in Kotlin, we need a correspondingly nested set of Data Classes for each JsonObject. To easily compare the before and after of our refactoring, I structured the Data Classes to show how they looked before applying encapsulation.

data class Weather(
	val geo: WeatherGeo,
	val headline: WeatherHeadline,
	...
	@SerailizedName("daily_forecast") val dailyForecasts: List<DailyForecast>,
)

data class DailyForecast(
	...
	val temperature: MinMaxTemperature,
	...
)

data class MinMaxTemperature(
	val max: Temperature,
	val min: Temperature,
)

data class Temperature(
	val value: Int,
	val unit: String,
)
Weekly forecast list item

To display the 'high/low temperature' from the weekly forecast on the example screen, we need a chained object access like 'weather.dailyForecasts.get(n).temperature.max.value'. This kind of chain dependency results in highly coupled code that is bad for maintenance, because if any property or function of a single object in the chain changes, every object relying on it will require cascading updates. It also hurts readability since the continuous object access makes the code long and forces you to understand every intermediate object just to figure out what the code does. This is a classic problem caused by low modularity due to a lack of encapsulation. Let's look at what happens when we add delegate methods to the DailyForecast class in our example, apply 'Hide Delegate' encapsulation to obscure the delegated object, and see the effects of this refactoring.

data class Weather(
	val geo: WeatherGeo,
	val headline: WeatherHeadline,
	...
	val dailyForecasts: List<DailyForecast>,
)

data class DailyForecast(
	...
	private val temperature: MinMaxTemperature,
	...
) {
	val minTemperature = temperature.min.value
	val maxTemperature = temperature.max.value
}

data class MinMaxTemperature(
	val max: Temperature,
	val min: Temperature,
)

data class Temperature(
	val value: Int,
	val unit: String,
)

By declaring the temperature property of the DailyForecast object with a private access modifier, I hid it from the outside and created minTemperature and maxTemperature delegate methods to provide access to the high and low temperature data. Applying 'Hide Delegate' in this way means the 'weather.dailyForecasts.get(n).temperature.max.value' code used to display the 'high/low temperature' on our example screen can be improved to 'weather.dailyForecasts.get(n).maxTemperature'. In practice, since we access each DailyForecast object through a loop, it will end up looking like this:

// 캡슐화 '위임 숨기기' 적용 전
weather.dailyForecasts.forEach { forecast ->
	forecast.temperature.max.value
}

// 캡슐화 '위임 숨기기' 적용 후
weather.dailyForecasts.forEach { forecast ->
	forecast.maxTemperature
}

The 'forecast.temperature.max.value' chain to access the high temperature data has now changed to 'forecast.maxTemperature'. By encapsulating the temperature property, I prevented unnecessary object access, and by declaring the maxTemperature delegate method, I completely hid the internal implementation of DailyForecast. Through this encapsulation, any code using maxTemperature is shielded from internal changes to the Temperature object, which prevents unnecessary modifications. Plus, explicitly naming the method 'maxTemperature' significantly boosts readability.

This weather API migration proved to be a perfect opportunity to apply a refactoring technique from our study group. But as the book points out, refactoring shouldn't be treated as some massive undertaking you have to carve out special time for; it should be woven naturally into your everyday development process, just like writing an If statement. I’ll keep studying and hope to occasionally share more good examples of applying these lessons to real projects in the future.

We are looking for members to share valuable experiences with at DelightRoom!

Share article