2.1.1 Compositing Model
Motivation
A Compositing Model aggregates data from multiple Model objects so that the View has a single and uniform point of access for its data source. Two different sub-types of a Compositing Model exist, each addressing a different use cases:
- A Union Compositing Model performs union of homogeneous data originating from different sources. For example, a Union Model can present data from multiple files, each handled by a SubModel.
- A Join Compositing Model extracts and combines relevant
information from heterogeneous Models and provides a convenient
interface to the result. For example, a
CustomerHistory
Model could combine ModelsCustomers
andOrders
, returning the combined information to the View.
Design
The Compositing Model acts both as listener and notifier. It holds references to its SubModels, and registers onto them as a listener. Its life cycle can be temporary and disjoint from the life cycle of its SubModels.
Notifications from individual SubModels are received and re-issued by the Compositing Model to notify the View. Vice-versa, data requests issued by the View on the Compositing Model are routed to the appropriate SubModel.
By its very nature, the Compositing Model is expected to be the only endpoint for a specific View, at least for data retrieval. The Controller, on the other hand, can either:
- issue change requests on the Compositing Model, which in turns forwards the change request to the appropriate SubModel according to some criteria. The Compositing Model is therefore acting as a surrogate Controller
- issue change requests directly on any of the Submodels. The SubModel will then issue the notification, which is forwarded to the View by the Compositing Model.
It is worth pointing out that nothing prevents other Views (or Controllers) to interact directly with any of the SubModels.
The above design considerations are common between the Union and the Join Compositing Models. The most substantial difference between them is in their interface. Union Models generally have the same interface as their SubModels, and Views can display either of them transparently. Join Models, on the other hand, likely provide a different interface to derived data, and the View is designed to target only this interface.
Practical example
A practical example of the Union Compositing Model for an AddressBook application
is here presented. The AddressBook
class aggregates a list of SubModels,
each extracting data from a different file source:
csv1_model = AddressBookCSV("file1.csv")
xml_model = AddressBookXML("file.xml")
csv2_model = AddressBookCSV("file2.csv")
address_book = AddressBook([csv1_model, xml_model, csv2_model])
A naive implementation for AddressBookCSV
is here shown to illustrate its
interface. The common base class Model
provides notification services
by implementing register
, unregister
, notify_listeners
, and the
listeners set
class AddressBookCSV(Model):
def __init__(self, filename):
super(AddressBookCSV, self).__init__()
self._filename = filename
def num_entries(self):
try:
return len(open(self._filename, "r").readlines())
except:
return 0
def get_entry(self, entry_number):
try:
with open(self._filename, "r") as f:
line = f.readlines()[entry_number]
name, phone = line.split(',')
return { 'name' : name.strip(), 'phone' : phone.strip()}
except:
raise IndexError("Invalid entry %d" % entry_number)
def append_entry(self, name, phone):
with open(self._filename, "a") as f:
f.write('{},{}\n'.format(name, phone))
self.notify_listeners()
The AddressBook
class is a Union Compositing Model implementing the same interface
of the SubModels
class AddressBook(Model):
def __init__(self, sub_models):
super(AddressBook, self).__init__()
self._sub_models = sub_models
for m in self._sub_models:
m.register(self)
def num_entries(self):
return sum([m.num_entries() for m in self._sub_models])
def get_entry(self, entry_number):
accumulated = itertools.accumulate(
[m.num_entries() for m in self._sub_models])
source_idx = [x <= entry_number for x in accumulated].index(False)
return self._sub_models[source_idx].get_entry(
entry_number - accumulated[source_idx])
def append_entry(self, name, phone):
self._sub_models[-1].append_entry(name, phone)
def notify(self):
self.notify_listeners()
The class accepts an arbitrary number of SubModels at initialization, and
registers as a listener on each of them. It implements the same interface,
retrieving data from the SubModels: the number of entries is the sum of the
SubModel entries, and the get_entry
method returns the entry from the
appropriate SubModel. append_entry
is used to add a new entry to the models.
In this case, the new entry is added to the last SubModel. Note how the
issuing of the notification is left to the SubModel. The notification from the
SubModel is then forwarded by the Compositing Model it to its listeners.