In playtomic, especially in the mobile team, we are fan of the decorator pattern, and in this article I will try to explain why it is a better idea to use the decorator pattern than inheritance, showing an example of how we use it.
This article will be technical, so I recommend to you to read before a bit of theory about the difference between inheritance and the decorator pattern. https://newbedev.com/decorator-pattern-versus-sub-classing
The inheritance
The inheritance in Kotlin is the same as in some other languages, we use the inheritance when we know there is a relationship between a child and its parent class, for example, an iPhone is a phone, a phone is an electronic device. Each child is an specialised version of the parent in which inherits all the properties of the parent.
The decorator pattern
The decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class.
The coolest of the decorator pattern is that you can create a decorator class and use it more than once, and at any moment you can add or remove any other one (with the inheritance you can not).
An example in playtomic
Let’s see an example about how Playtomic app manages the HTTP requests. As a spoiler I will tell you that with the decorator pattern we can change which http client we are using in runtime, yes, in runtime. Let 's see.
To start, we have a IHttpClient
interface, which is a basic thing that we will need to create the decorator classes. In this case we only have one simple method to make a request with an object that contains the needed information to make that request.
interface IHttpClient {
fun request(httpRequest: HttpRequest): Promise<HttpResponse>
}
In our case, we also have some other implemented methods (inside the interface) to make our lives easier while we are making requests. All of them are very similar to this one:
fun get(endpoint: String, params: Map<String, Any>?): Promise<ByteArray> =
request(HttpRequest(method = HttpMethod.get, url = endpoint, queryParams = params))
.then(map = { it.body })
Well, I am not going to show very deeply the implementation of each of our http clients implementation, I will just enumerate some of them:
- OkHttpClient => This one is the main client that makes the remote http requests to our backend
class OkHttpClient(
private val baseUrl: String,
timeOut: Long? = null,
client: OkHttpClient? = null,
private val urlEncoder: IHttpParameterEncoder = HttpUrlParameterEncoder(),
private val bodyEncoders: List<IHttpParameterEncoder> = listOf(HttpJsonParameterEncoder(), ...))
: IHttpClient {
This is the base HTTP client, so here there is nothing too much to see.
- AuthHttpClient => With this one, we include the authentication and if the session has expired we can re-login to the user and throw the request again (and for the user is totally transparent)
class AuthHttpClient(private val httpClient: IHttpClient,
private val keychain: IKeyValueStorage) : IHttpClient by httpClient {
In this case we are decorating the http client to add the authentication into the request, so, as you can see into the class definition (... by httpClient
) means that the interface that this class is implementing IHttpClient
interface is being decorated by the parameter httpclient
. So, in that case we can only implement into this class the methods that we need for the decoration (In this case, is the only one that we have). So, it looks like this:
override fun request(httpRequest: HttpRequest): Promise<HttpResponse> =
request(httpRequest, refreshTokenAllowed = true)
fun request(httpRequest: HttpRequest, refreshTokenAllowed: Boolean): Promise<HttpResponse> {
val headers = httpRequest.headers?.toMutableMap() ?: mutableMapOf()
val accessToken = keychain.accessToken
if (accessToken != null && isAnemoneRequest(httpRequest)) {
headers.put("Authorization", "Bearer $accessToken")
}
val authHttpRequest = httpRequest.copy(headers = headers)
val pendingPromise = PendingPromise<HttpResponse>()
httpClient.request(authHttpRequest).then(pendingPromise::fulfill).catchError { error ->
if (this.isAnemoneRequest(httpRequest) && this.isSessionError(error)) {
if (refreshTokenAllowed) {
if (accessToken != this.keychain.accessToken) {
this.request(httpRequest = httpRequest, refreshTokenAllowed = false).then(pendingPromise::fulfill).catchError(pendingPromise::reject)
} else {
this.refreshToken(PendingRequest(request = httpRequest, promise = pendingPromise, error = error))
}
} else {
this.logout?.invoke()
this.httpClient.request(httpRequest).then(pendingPromise::fulfill).catchError(pendingPromise::reject)
}
} else {
pendingPromise.reject(error)
}
}
return pendingPromise
}
As I said before, we are including into the request a header with the authentication needed information and checking if the request fails because of a session expired error to throw the request again.
- AnalyticsHttpClient => This one measures and track the response, so with this we can analyse the traffic, the error rate ...
class AnalyticsHttpClient(private val httpClient: IHttpClient,
private val analyticsManager: IAnalyticsManager) : IHttpClient by httpClient {
This decorator is doing something very similar to the previous one to be able to track timings and errors
- LocalHttpClient => This one use as response some jsons stored into the app
class LocalHttpClient(private val context: Context) : IHttpClient {
This is another base client to read local documents in JSON to be able to serve it as a response.
And if you are still asking how we are changing the used http client in runtime, I will confess that it has nothing to do with the decorator pattern but this is how we are doing it. We have another implementation of the IHttpClient
interface that receives 2 implementations of the HTTP client and depending on the endpoint url and method verb we decide which one to use.
class AppHttpClient(val appVersion: String,
var localHttpClient: IHttpClient,
var remoteHttpClient: IHttpClient)
: IHttpClient {
private val localEndpoints = mapOf<String, String>(
"POST /v2/auth/login" to "/v2/auth/login",
"GET /v2/users/me" to "/v2/users/me",
"GET /v2/tournaments/7c0b8e6f-3693-4f48-b370-b9643a8d04fe" to "/v2/tournaments/1",
"GET /v2/status/version_control" to "/v2/status/version_control",
"" to ""
)
override fun request(httpRequest: HttpRequest): Promise<HttpResponse> {
val headers = httpRequest.headers?.toMutableMap() ?: mutableMapOf()
headers.put("X-Requested-With", "${BuildConfig.APPLICATION_ID}.app $appVersion")
headers.put("User-Agent", "Android ${android.os.Build.VERSION.RELEASE}")
val appHttpRequest = httpRequest.copy(headers = headers)
val key = "${appHttpRequest.method.description} ${appHttpRequest.url}"
val file = localEndpoints[key]
return if (file != null) {
localHttpClient.request(appHttpRequest.copy(url = file))
} else {
remoteHttpClient.request(appHttpRequest)
}
}
}
To complete this post and understand the power of the composition thanks to the decorator pattern, let’s take a quick look at how easy it is to add a new decorator component, to include a new behavior or remove one of it just by adding or removing the proper line into the instantiation. Ours one looks like this:
val baseHttpClient = OkHttpClient(baseUrl = baseUrl, timeOut = 30)
val authHttpClient = AuthHttpClient(
httpClient = baseHttpClient,
keychain = keychain
)
val analyticsHttpClient = AnalyticsHttpClient(
httpClient = authHttpClient,
analyticsManager = managerProvider.analyticsManager
)
val appHttpClient = AppHttpClient(
appVersion = appVersion,
localHttpClient = LocalHttpClient(applicationContext),
remoteHttpClient = analyticsHttpClient
)
Top comments (0)