Gang of Four’s Flyweight design pattern introduces the concept of intrinsic and extrinsic states:
The key concept here is the distinction between intrinsic and extrinsic state. Intrinsic state is stored in the flyweight; it consists of information that’s independent of the flyweight’s context, thereby making it sharable. Extrinsic state depends on and varies with the flyweight’s context and therefore can’t be shared. Client objects are responsible for passing extrinsic state to the flyweight when it needs it.
In other words, the state of an object can be decomposed with respect to a group of objects as an intrinsic state and an extrinsic state, where the intrinsic state is the intersection of the states of all objects of the group and the extrinsic state is the difference of the state of the object and the intrinsic state. Since the intrinsic state is duplicated in each object of the group, space can be saved by replacing the group of objects by a single flyweight object storing a single intrinsic state. The flyweight object cannot however store the multiple extrinsic states of the objects of the group, so the extrinsic states are stored outside and passed to the flyweight object in each request from client objects. Such an optimised communication protocol is often called a stateless protocol since the flyweight object does not store extrinsic state. Examples of stateless protocols include IP and HTTP (and more generally any REST protocols, where intrinsic state is called resource state and extrinsic state is called application state).
For instance, let’s take three objects with their respective clients:
o1 ← c1
o2 ← c2
o3 ← c3
We can decompose the state of each object with respect to the three objects:
state 1 = intrinsic state ∪ extrinsic state 1
state 2 = intrinsic state ∪ extrinsic state 2
state 3 = intrinsic state ∪ extrinsic state 3where:
intrinsic state = state 1 ∩ state 2 ∩ state 3
extrinsic state 1 = state 1 \ intrinsic state
extrinsic state 2 = state 2 \ intrinsic state
extrinsic state 3 = state 3 \ intrinsic state
Here the intrinsic state is duplicated. So storing it in a single flyweight object (and moving the extrinsic states into clients) saves space:
o ← c1, c2, c3
So far so good. Now I have been wondering if it is possible to classify attributes as intrinsic state or extrinsic state from the definition of an unshared class, in order to derive a flyweight class.
Is the following syntactic characterisation of intrinsic and extrinsic state for unshared classes correct?
- intrinsic state = immutable instance variables ∪ class variables;
- extrinsic state = mutable instance variables.
For instance, let’s define the following unshared class:
class Unshared:
__z = 0
def __init__(self):
self.__x = 0
self.__y = 0
def f(self):
return self.__x + self.__y
def g(self):
self.__y += 1
@classmethod
def h(cls):
cls.__z += 1
The proposed characterisation would yield:
- intrinsic state = {
__x
,__z
}; - extrinsic state = {
__y
}.
which seems correct. That unshared class can then be transformed into a flyweight class for saving space:
class Flyweight:
__z = 0
def __init__(self):
self.__x = 0
def f(self, y):
return self.__x + y
@classmethod
def h(cls):
cls.__z += 1
Edit
My characterisation was incomplete, as noted by @DocBrown, because it prevented mutable flyweight objects, by excluding mutable instance variables that are instance or class specific from the intrinsic state. And it also incorrectly included immutable instance variables that are instance specific in the intrinsic state.
Let’s define the following terms:
- A group is a set of unshared objects from which a single flyweight object is derived.
- An instance-scope instance variable is an instance variable that is specific to an individual object.
- A lesser-group-scope instance variable is an instance variable that is common to a smaller group of unshared objects than the group under consideration.
- A group-scope instance variable is an instance variable that is common to the group of unshared objects under consideration.
- An greater-group-scope instance variable is an instance variable that is common to a larger group of unshared objects than the group under consideration.
- A class-scope instance variable is an instance variable that is common to all objects.
An instance-scope instance variable and a lesser-group-scope instance variable are not sharable by the group of unshared objects under consideration, while a group-scope instance variable, greater-group-scope instance variable, a class-scope instance variable and a class variable are sharable by the group of unshared objects under consideration.
We can conclude that the complete characterisation of intrinsic and extrinsic state for unshared classes is the following:
- intrinsic state = group-scope instance variables ∪ greater-group-scope instance variables ∪ class-scope instance variables ∪ class variables;
- extrinsic state = instance-scope instance variables ∪ lesser-group-scope instance variables.
However as @DocBrown rightly pointed, this characterisation is not purely syntactic. Class variables, class-scope instance variables and some mutable instance-scope instance variables can be identified syntactically just by looking at the unshared class definition. But greater-group-scope instance variables, group-scope instance variables, lesser-group-scope instance variables, immutable instance-scope instance variables and mutable instance-scope instance variables can only be identified semantically.
When deriving a flyweight class from an unshared class, its instance-scope instance variables and lesser-group-scope instance variables are moved outside of the flyweight class, its group-scope instance variables become instance-scope instance variables of the flyweight class, its greater-group-scope instance variables are moved outside of the flyweight class, its class-scope instance variables remain class-scope instance variables of the flyweight class, and its class variables remain class variables of the flyweight class.
For instance, let’s define the following unshared class:
import collections
class Unshared:
__L = 0 # immutable class variable
__instances = collections.defaultdict(list) # mutable class variable
def __init__(self, GROUP, I, i, j, k):
self.__instances[GROUP].append(self)
self.__GROUP = GROUP # immutable group-scope instance variable
self.__I = I # immutable instance-scope instance variable
self.__i = i # mutable instance-scope instance variable
for instance in self.__instances[GROUP]:
instance.__j = j # mutable group-scope instance variable
self.__K = 0 # immutable class-scope instance variable
for instances in self.__instances.values():
for instance in instances:
instance.__k = k # mutable class-scope instance variable
def query(self):
return {
"instance": [self.__I, self.__i],
"group": [self.__GROUP, self.__j],
"class": [self.__K, self.__k, self.__L, tuple(self.__instances)]
}
def manipulate(self):
# Update the mutable instance-scope variables.
self.__i = None
# Update the mutable group-scope variables.
for instance in self.__instances[self.__GROUP]:
instance.__j = None
# Update the mutable class-scope variables.
for instances in self.__instances.values():
for instance in instances:
instance.__k = None
self.__instances[None] = []
The new characterisation yields:
- intrinsic state = {
__GROUP
,__j
,__K
,__k
,__L
,__instances
}; - extrinsic state = {
__I
,__i
}.
That unshared class can then be transformed into a flyweight class for saving space:
class Flyweight:
__L = 0
__instances = {}
def __init__(self, GROUP, j, k):
self.__instances[GROUP] = self
self.__GROUP = GROUP
self.__j = j
self.__K = 0
for instance in self.__instances.values():
instance.__k = k
def query(self, I, i):
return {
"instance": [I, i],
"group": [self.__GROUP, self.__j],
"class": [self.__K, self.__k, self.__L, tuple(self.__instances)]
}
def manipulate(self):
# Update the mutable group-scope variables.
self.__j = None
# Update the mutable class-scope variables.
for instance in self.__instances.values():
instance.__k = None
self.__instances[None] = []
Unshared class usage:
ra1 = Unshared("a", 1, 1, 1, 0) # unshared instance of group "a"
ra2 = Unshared("a", 2, 2, 1, 0) # unshared instance of group "a"
rb1 = Unshared("b", 1, 1, 2, 0) # unshared instance of group "b"
rb2 = Unshared("b", 2, 2, 2, 0) # unshared instance of group "b"
print(ra1.query()) # {"instance": [1, 1], "group": ["a", 1], "class": [0, 0, 0, ("a", "b")]}
print(ra2.query()) # {"instance": [2, 2], "group": ["a", 1], "class": [0, 0, 0, ("a", "b")]}
print(rb1.query()) # {"instance": [1, 1], "group": ["b", 2], "class": [0, 0, 0, ("a", "b")]}
print(rb2.query()) # {"instance": [2, 2], "group": ["b", 2], "class": [0, 0, 0, ("a", "b")]}
ra1.manipulate()
print(ra1.query()) # {"instance": [1, None], "group": ["a", None], "class": [0, None, 0, ("a", "b", None)]}
print(ra2.query()) # {"instance": [2, 2], "group": ["a", None], "class": [0, None, 0, ("a", "b", None)]}
print(rb1.query()) # {"instance": [1, 1], "group": ["b", 2], "class": [0, None, 0, ("a", "b", None)]}
print(rb2.query()) # {"instance": [2, 2], "group": ["b", 2], "class": [0, None, 0, ("a", "b", None)]}
Flyweight class usage:
fa = Flyweight("a", 1, 0) # shared instance of group "a"
fb = Flyweight("b", 2, 0) # shared instance of group "b"
print(fa.query(1, 1)) # {"instance": [1, 1], "group": ["a", 1], "class": [0, 0, 0, ("a", "b")]}
print(fa.query(2, 2)) # {"instance": [2, 2], "group": ["a", 1], "class": [0, 0, 0, ("a", "b")]}
print(fb.query(1, 1)) # {"instance": [1, 1], "group": ["b", 2], "class": [0, 0, 0, ("a", "b")]}
print(fb.query(2, 2)) # {"instance": [2, 2], "group": ["b", 2], "class": [0, 0, 0, ("a", "b")]}
fa.manipulate()
print(fa.query(1, None)) # {"instance": [1, None], "group": ["a", None], "class": [0, None, 0, ("a", "b", None)]}
print(fa.query(2, 2)) # {"instance": [2, 2], "group": ["a", None], "class": [0, None, 0, ("a", "b", None)]}
print(fb.query(1, 1)) # {"instance": [1, 1], "group": ["b", 2], "class": [0, None, 0, ("a", "b", None)]}
print(fb.query(2, 2)) # {"instance": [2, 2], "group": ["b", 2], "class": [0, None, 0, ("a", "b", None)]}