City map application
Let's imagine that we are writing a program that displays a city map with different icons like cafes, bus stops and so on. We are going to use Swing GUI framework, but you are not required to know Swing to follow this topic because all the code will be self-explanatory. Let's create a class representing such an icon:
import javax.swing.*;
import java.awt.*;
class MapIcon {
private int xPos;
private int yPos;
private final ImageIcon icon;
public MapIcon(int xPos, int yPos, String src) {
this.xPos = xPos;
this.yPos = yPos;
this.icon = new ImageIcon(src);
}
public void draw(Component c, Graphics g) {
icon.paintIcon(c, g, xPos, yPos);
}
}
It stores coordinates and an ImageIcon and has a method to draw itself. After that, let's create a simple class to display a fragment of the city map with a bunch of cafe icons randomly scattered over it:
import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
class MapFrame extends JFrame {
private final String bgSrc = "src/main/resources/map.png";
private final String cafeSrc = "src/main/resources/cafe.png";
private final List<MapIcon> mapIcons = new ArrayList<>();
private ImageIcon background;
public MapFrame() {
super("City map");
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(800, 800);
setLocationRelativeTo(null);
setLayout(null);
initMap();
setVisible(true);
}
@Override
public void paint(Graphics g) {
background.paintIcon(this, g, 0, 0);
for (MapIcon mapIcon : mapIcons) {
mapIcon.draw(this, g);
}
}
private void initMap() {
background = new ImageIcon(bgSrc);
loadIcons(50, cafeSrc);
}
private void loadIcons(int number, String src) {
Random random = new Random();
for (int i = 0; i < number; i++) {
int xPos = random.nextInt(getWidth());
int yPos = random.nextInt(getHeight());
MapIcon mapIcon = new MapIcon(xPos, yPos, src);
mapIcons.add(mapIcon);
}
}
}
Now let's run this app.
public class Main {
public static void main(String[] args) {
new MapFrame();
}
}
This is what we will see:
Our app is working but there are still some issues we might want to fix.
Memory footprint
It's not the best-looking navigation app, but it illustrates our task pretty well. You might have already noticed an issue in this code. We allocate memory for images each time we create an icon despite the fact that the image file is the same in all of them. If the map was bigger and we had a lot of icons to load, the application would consume a lot of memory. It would be ideal if we could find a way to avoid it.
Before we start any memory usage optimization, let's take a look at the class and figure out which parts of the internal state of the MapIcon object are immutable and can be shared among all such objects and which parts of the state are specific to each individual object.
Screen coordinates are specific for each MapIcon and may change if we, for example, add the zoom feature to our application. The icon image and its size are the same and can be shared among all similar objects. Such immutable parts of an object state are called the object's intrinsic state. The other parts of the object's state that can be changed from the outside are called its extrinsic state.
This means that we can reduce memory consumption by moving the intrinsic state into a separate object. It will be cached and shared when needed among other objects. This can be achieved by using the Flyweight design pattern. Let's see how it works in detail.
Pattern diagram
As you can see in the diagram above, Flyweight pattern includes the following components:
- The Flyweight interface which can have methods to pass extrinsic state to Flyweight objects.
- The ConcreteFlyweight object that implements the Flyweight interface and can have an intrinsic state which is immutable and doesn't depend on external context.
- FlyweightFactory which creates and manages Flyweight objects. When the Client requests a Flyweight object, the FlyweightFactory either supplies an existing object or creates it on demand.
- Client stores a reference to a Flyweight object and its extrinsic state.
It perfectly follows SOLID principles.
Pattern implementation
We've already defined the extrinsic and intrinsic states of MapIcon. Now we will refactor our application using the Flyweight pattern. First, we define the MapIconImage interface with a method intended to pass extrinsic state to Flyweight objects:
interface MapIconImage {
void draw(Component c, Graphics g, int xPos, int yPos);
}
Next, we create a couple of classes that will implement the MapIconImage interface:
// a simple icon
class MapIconImageSimple implements MapIconImage {
private final ImageIcon icon;
public MapIconImageSimple(String src) {
this.icon = new ImageIcon(src);
}
@Override
public void draw(Component c, Graphics g, int xPos, int yPos) {
icon.paintIcon(c, g, xPos, yPos);
}
}
// a bigger icon with a border around its edge
class MapIconImageHighlighted implements MapIconImage {
private final int size = 64;
private final ImageIcon icon;
public MapIconImageHighlighted(String src) {
ImageIcon original = new ImageIcon(src);
Image scaled = original.getImage()
.getScaledInstance(size, size, Image.SCALE_SMOOTH);
this.icon = new ImageIcon(scaled);
}
@Override
public void draw(Component c, Graphics g, int xPos, int yPos) {
g.setColor(Color.CYAN);
g.fillOval(xPos, yPos, size, size);
icon.paintIcon(c, g, xPos, yPos);
}
}
Next, we modify the MapIcon class to reflect the changes we've made so far:
class MapIcon {
private int xPos;
private int yPos;
private final MapIconImage mapIconImage;
public MapIcon(int xPos, int yPos, MapIconImage mapIconImage) {
this.xPos = xPos;
this.yPos = yPos;
this.mapIconImage = mapIconImage;
}
public void draw(Component c, Graphics g) {
mapIconImage.draw(c, g, xPos, yPos);
}
}
The last component will be MapIconImageFactory:
import java.util.HashMap;
import java.util.Map;
enum MapIconType { SIMPLE, HIGHLIGHTED }
class MapIconImageFactory {
private final Map<MapIconType, MapIconImage> mapIconImages = new HashMap<>();
public MapIconImage getMapIconImage(MapIconType type, String src) {
MapIconImage mapIconImage = mapIconImages.get(type);
if (mapIconImage == null) {
mapIconImage = switch (type) {
case SIMPLE -> new MapIconImageSimple(src);
case HIGHLIGHTED -> new MapIconImageHighlighted(src);
};
mapIconImages.put(type, mapIconImage);
}
return mapIconImage;
}
}
And finally, we update our MapFrame class by adding the MapIconImageFactory field and changing the initMap() and loadIcons() methods accordingly:
class MapFrame extends JFrame {
// other fields
private final MapIconImageFactory factory = new MapIconImageFactory();
// constructor and paint method
private void initMap() {
background = new ImageIcon(bgSrc);
loadIcons(50, MapIconType.SIMPLE);
loadIcons(25, MapIconType.HIGHLIGHTED);
}
private void loadIcons(int number, MapIconType type) {
Random random = new Random();
for (int i = 0; i < number; i++) {
int xPos = random.nextInt(getWidth());
int yPos = random.nextInt(getHeight());
MapIconImage mapIconImage = factory.getMapIconImage(type, cafeSrc);
MapIcon mapIcon = new MapIcon(xPos, yPos, mapIconImage);
mapIcons.add(mapIcon);
}
}
}
This is what we get if we run the application again:
The application works fine and, although we added more icons, it has a lesser memory footprint. How much memory can we save using this pattern? It depends on the size of the icon image and the number of icons. The bigger the icon image is and the more icons there are on the map, the higher the savings. Keep in mind that only profiling can give a reliable answer. Given that this pattern introduces new classes and makes the code more complex, it may be ineffective if potential memory saving is low.
Conclusion
The Flyweight is a structural pattern whose main goal is to optimize memory usage by caching and sharing some data. This pattern is useful when the application operates a large number of objects with a big memory footprint and the major part of such objects' state can be moved to a separate class and shared. However, the pattern makes the code more complex, and therefore not every application would benefit from it.