Structural Design Patterns
Kotlin Design Patterns: Flyweight
Purpose of the pattern
The pattern is mostly used to balance memory usage in your app. It’s all about object reusability (note that I’ve used the word Object, not Class). Instead of creating new objects each time, you have a pool of similar objects that can be reused.
This way, instead of allocating memory each time the object is created, you allocate it once and reuse the previously created object.
This means you’ll save some CPU and memory in object creation and make garbage collecting faster. However, there is a tradeoff. Depending on implementation, you will have to either:
- Control removing objects from the pool, which can be tricky as if you remove the object that is currently used, it’ll break things.
- Allocate memory for unused objects. In this approach there is no deleting objects from the pool, which means allocating memory for something not currently used.
Both approaches have downsides, choose one depending on your needs. If something is unclear Example should make it all more understandable:
Example
Your app uses the same image throughout multiple places on the same screen. You launch your app, and … it crashes due to OutOfMemoryError
. The images are already compressed, and they cannot be smaller. You’ve to figure out a way how to save more memory.
This is a perfect use case for a Flyweight
say there are 3 images, each being 5 MB big, meaning they take 15 MB total, but with Flyweight
they’ll only take 5 MB.
Moreover, you’re getting the images from the Web by the URL
, using Flyweight
here will also save time and limit internet consumption.
Let’s implement it:
In Flyweight
we usually call the creating class Factory
. Application
will depend on both the Image
and ImageFactory
as it’ll use Image
models, but create them through ImageFactory
.
data class Image(val bytes: ByteArray)
class ImageFactory {
private val cache = mutableMapOf<String, Image>()
private suspend fun getImage(url: String): Image =
cache[url] ?: fetchImage(url).also { image -> cache[url] = image }
}
As you can see, the Flyweight
pattern implementation is pretty simple. Here’s how to use it:
fun main() {
val factory = ImageFactory()
val scope = CoroutineScope(Dispatchers.IO)
scope.launch {
val image = factory.get("image")
}
scope.launch {
val image = factory.get("image")
}
}
In this exact example, there is a problem of synchronization
However, there is 1 catch: you make 2 requests for an image with the same URL, which is not what we want. It happens because, before the first fetchImage(url)
ends, another Coroutines tries to get the URL from cache
, but since it’s not yet present, it invokes fetchImage(url)
as well.
We can fix it in multiple ways. I’ll show the way using Mutex
that works similarly to the Flyweight
itself:
class ImageFactory {
private val cache = mutableMapOf<String, Image>()
private val locks = mutableMapOf<String, Mutex>()
private val lock = Mutex()
suspend fun get(url: String): Image {
val imageMutex = lock.withLock {
locks.getOrPut(url) { Mutex() }
}
val image = imageMutex.withLock {
getImage(url)
}
locks.remove(url)
return image
}
private suspend fun getImage(url: String): Image =
cache[url] ?: fetchImage(url).also { image -> cache[url] = image }
}
Thanks for reading! Please clap and follow me for more!
More design patterns
Based on the book:
“Wzorce projektowe : elementy oprogramowania obiektowego wielokrotnego użytku” — Erich Gamma Autor; Janusz Jabłonowski (Translator); Grady Booch (Introduction author); Richard Helm (Author); Ralph Johnson (Author); John M Vlissides (Author)