2

Now I'm studying about Flyweight design pattern. I tried to find some examples through google, and this is what I found.

import java.awt.Color;
import java.awt.Graphics;

public interface Shape {

    public void draw(Graphics g, int x, int y, int width, int height, Color color);
}

This is interface Shape.

import java.awt.Color;
import java.awt.Graphics;

public class Line implements Shape {...}
public class Oval implements Shape {...}

These two class implements Shape interface.

import java.util.HashMap;

public class ShapeFactory {

    private static final HashMap<ShapeType, Shape> shapes = new HashMap<ShapeType, Shape>();

    public static Shape getShape(ShapeType type) {
        Shape shapeImpl = shapes.get(type);

        if (shapeImpl == null) {
            if (type.equals(ShapeType.OVAL_FILL)) {
                shapeImpl = new Oval(true);
            } else if (type.equals(ShapeType.OVAL_NOFILL)) {
                shapeImpl = new Oval(false);
            } else if (type.equals(ShapeType.LINE)) {
                shapeImpl = new Line();
            }
            shapes.put(type, shapeImpl);
        }
        return shapeImpl;
    }
    
    public static enum ShapeType {
        OVAL_FILL, OVAL_NOFILL, LINE;
    }
}

Through this Factory class, Oval or Line class is made by the type. I understand it but I have one question about it. I learn that Static field or function can only use in Static class. However, in this example, static Hashmap and function is declared in non-static class ShapeFactory. I found other examples in google, but every examples are similar to this. I tried to do my own Flyweight model, but it does not work. Can anyone explain about this?

Eric Kim
  • 21
  • 1

2 Answers2

3

I assume you've already read the wiki article, so I'll try to give a brief and, hopefully, simple intuition into the flyweight pattern.

The key concept that makes flyweight a "pattern" is the separation of the object state into "intrinsic" and "extrinsic" parts.

Both of these are part of the conceptual "state" of the object, the difference is in how they are represented in the code.

"Intrinsic" state is just your regular fields and properties, while "extrinsic" part of the object is stored externally to the object (doesn't matter where) and passed to the object with each method call.


Consider an artificial example of PlanetSurfaceGravityForceFormula. It calculates the force of gravity on the surface of the planet for an object of the given mass.

The formula for the gravity is following: newtons law of gravity

Here, G, m₁ and are properties of the planet, while m₂ is the property of the object on the surface of the planet.

One way to design the PlanetSurfaceGravityForceFormula is to have it contain all the state necessary for the calculation:

class PlanetSurfaceGravityForceFormula {
  double G, r, m1, m2;
  public double calculateForce() {...}
}

In this case, when you need to calculate force, you'll need to create new instance of PlanetSurfaceGravityForceFormula with all the relevant constants and call the calculateForce method.

Which is fine, unless you know beforehand that you need to perform the calculation on the limited fixed number of the planets, and only m₂ is expected to change with each call.

So your new design for the PlanetSurfaceGravityForceFormula is this:

class PlanetSurfaceGravityForceFormula {
  double G, r, m1;
  public double calculateForce(double m2) {...}
}

See what happened here? m₂ was a part of the intrinsic state before, but now it becomes the extrinsic state, as it's passed from outside for each method call.

This design has several advantages over the previous one:

  1. It's possible to reuse the same instance of PlanetSurfaceGravityForceFormula for different values of m2
  2. If the planets are fixed and known in advance, it's possible to create static constants of the PlanetSurfaceGravityForceFormula for each such planet and use them for calculation
  3. If the planets are known to be limited, but not known in advance, it's possible to create some kind of PlanetSurfaceGravityForceFormulaFactory that returns cached instances of the PlanetSurfaceGravityForceFormula.

Hopefully this helps to understand the difference between "intrinsic" and "extrinsic" state of the object.

One thing that I'd like to emphasize is that in real life you seldom need to "force" parts of the intrinsic state to become "extrinsic". Usually just following the common sense will yield the best design.


UPD Regarding:

I learn that Static field or function can only use in Static class. However, in this example, static Hashmap and function is declared in non-static class ShapeFactory.

static keyword in java, frankly, confusing (though less confusing than in C++), as it has different semantics in different context.

Instances of a nested class in java by default retain the reference to the parent class instance, making nested class static removes this reference, making the nested class behave as the regular top-level class.

Methods, marked as static can be called from both static and non-static methods, but they have access only to the static fields and methods.

Finally static fields of the class are "singletons", they are like "global variables", shared throughout your code, even when accessed from different instances of a class. They can be accessed from both regular and static methods.

So, to answer your question:

in this example, static Hashmap and function is declared in non-static class ShapeFactory

Is not strictly true, as ShapeFactory is the top-level class and it behaves the same way as the static nested classes. From what I can tell, your "abstract factory" implementation is correct, although it's not a flyweight and it technically is a flyweight, where ShapeType is an intrinsic state and Graphics g, int x, int y, int width, int height, Color color is extrinsic state of the Shape.

Aivean
  • 10,692
  • 25
  • 39
  • `ShapeFactory` in the OP is a [static factory](https://stackoverflow.com/tags/static-factory/info). – jaco0646 Oct 07 '21 at 14:24
  • @jaco0646 abstract factory is used to create flyweights. So, yes `ShapeFactory` is an abstract factory, and `Shape` is a flyweight. – Aivean Oct 07 '21 at 20:58
  • A Flyweight can be created from any kind of factory. The example here happens to be a static factory. – jaco0646 Oct 07 '21 at 22:05
1

As they have mentioned above, the example you provide is not a Flyweight pattern. This pattern is helpful to reduce the memory amount used by an application. In the following example, we have a music app that will contain song and playlists.

Each song has an Id, the song name and the size.

public class Song {
    Long id;
    String name;
    byte[] audioSize;

    public Song(Long id, String name) {
        this.id = id;
        this.name = name;
        audioSize = new byte[1000000];
    }
}

A playlist contains a list of songs and the playlist name.

public class PlayList {

    private List<Song> songs;
    private String name;

    public PlayList(String name) {
        this.songs = new ArrayList<>();
        this.name = name;
    }

    public void addSong(Song song){
        this.songs.add(song);
    }
}

To see the behaviour of the application, I have created a Main class with 1000 songs and 300000 playlists with 80 songs per playlist. When the application starts, it will prompt the available memory and just before end will prompt the memory used. If you run this code, it will crash due to memory limitation.

import java.util.*;

public class Main {

    private static final String[] songNames = new String[1000];
    private static final String[] playListNames = new String[300000];
    private static final List<PlayList> playLists = new ArrayList<>();

    public static void main(String[] args) {

        Runtime runtime = Runtime.getRuntime();
        System.out.println("Available memory: " + (runtime.maxMemory() / 1024 / 1024));

        initArrays();
        createRandomPlayLists();
        System.out.println("Created playlists: " + playLists.size());

        long usedMemory = runtime.totalMemory() - runtime.freeMemory();
        System.out.println("Used memory : " + usedMemory / 1024 / 1024);
    }

    private static void createRandomPlayLists() {
        Random random = new Random();
        long id = 0L;
        for (int i = 0; i < playListNames.length; i++) {
            PlayList playList = new PlayList(playListNames[i]);
            for (int j = 0; j < 80; j++) {
                Song song = new Song(++id, songNames[random.nextInt(songNames.length)]);
                playList.addSong(song);
            }
            playLists.add(playList);
        }
    }

    private static void initArrays() {
        for (int i = 0; i < songNames.length; i++) {
            songNames[i] = "Song " + i;
        }
        for (int i = 0; i < playListNames.length; i++) {
            playListNames[i] = "PlayList " + i;
        }
    }
}

To avoid this memory limitation, we can use the flightweight pattern. To implement this pattern we can add a Map to store the songs added to a playlist. If a song was never added to a playlist it will be added to the map. At the end of the execution, the total memory used for the application should be arround 2.6 GB

import java.util.*;

public class Main {

    private static final String[] songNames = new String[1000];
    private static final String[] playListNames = new String[300000];
    private static final List<PlayList> playLists = new ArrayList<>();

    public static void main(String[] args) {

        Runtime runtime = Runtime.getRuntime();
        System.out.println("Available memory: " + (runtime.maxMemory() / 1024 / 1024));

        initArrays();
        createRandomPlayLists();
        System.out.println("Created playlists: " + playLists.size());

        long usedMemory = runtime.totalMemory() - runtime.freeMemory();
        System.out.println("Used memory : " + usedMemory / 1024 / 1024);
    }

    private static void createRandomPlayLists() {
        Random random = new Random();
        long id = 0L;
        Map<String, Song> loadedSongs = new HashMap<>();
        for (int i = 0; i < playListNames.length; i++) {
            PlayList playList = new PlayList(playListNames[i]);
            for (int j = 0; j < 80; j++) {

                String songName = songNames[random.nextInt(songNames.length)];
                if (loadedSongs.containsKey(songName)) {
                    playList.addSong(loadedSongs.get(songName));
                } else {
                    //Create the song in memory
                    Song song = new Song(++id, songName);
                    //Add the song to the map. This will make the song available the next time it is needed
                    loadedSongs.put(songName, song);
                    playList.addSong(song);
                }
            }
            playLists.add(playList);
        }
    }

    private static void initArrays() {
        for (int i = 0; i < songNames.length; i++) {
            songNames[i] = "Song " + i;
        }
        for (int i = 0; i < playListNames.length; i++) {
            playListNames[i] = "PlayList " + i;
        }
    }
}

I will add an implementation using SongFactory class that will allow to enable or disable the Flyweight functionallity with a boolean


import java.util.HashMap;
import java.util.Map;

public class SongFactory {

    private static final Map<String, Song> loadedSongs = new HashMap<>();
    public static boolean flyweightEnabled = true;
    private static Long id = 0L;

    public static Song newSong(String songName) {
        if (flyweightEnabled) {
            //If the song is in memory, we return it
            if (loadedSongs.containsKey(songName)) {
                return loadedSongs.get(songName);
            } else {
                //Create the song in memory
                Song song = new Song(++id, songName);
                //Add the song to the map. This will make the song available the next time it is needed
                loadedSongs.put(songName, song);
                return song;
            }
        } else {
            Song song = new Song(++id, songName);
            return song;
        }
    }
}

import java.util.*;

public class Main {

    private static final String[] songNames = new String[1000];
    private static final String[] playListNames = new String[300000];
    private static final List<PlayList> playLists = new ArrayList<>();

    public static void main(String[] args) {

        Runtime runtime = Runtime.getRuntime();
        System.out.println("Available memory: " + (runtime.maxMemory() / 1024 / 1024));

        initArrays();
        createRandomPlayLists();
        System.out.println("Created playlists: " + playLists.size());

        long usedMemory = runtime.totalMemory() - runtime.freeMemory();
        System.out.println("Used memory : " + usedMemory / 1024 / 1024);
    }

    private static void createRandomPlayLists() {
        Random random = new Random();
        for (int i = 0; i < playListNames.length; i++) {
            PlayList playList = new PlayList(playListNames[i]);
            for (int j = 0; j < 80; j++) {
                Song song = SongFactory.newSong(songNames[random.nextInt(songNames.length)]);
                playList.addSong(song);
            }
            playLists.add(playList);
        }
    }

    private static void initArrays() {
        for (int i = 0; i < songNames.length; i++) {
            songNames[i] = "Song " + i;
        }
        for (int i = 0; i < playListNames.length; i++) {
            playListNames[i] = "PlayList " + i;
        }
    }
}

I hope this will help you to understand better the Flyweight pattern

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
koldou
  • 11
  • 1
  • 2
  • 2
    What you described is not a flyweight, but a simple caching. Flyweight is about "intrinsic" and "extrinsic" state of an object, not just about reusing same data objects. – Aivean Oct 06 '21 at 23:34