I have 4 entities:
Mall
contains multiple Shop
subobjectsShop
contains multiple Basket
subobjectsBasket
contains multiple Fruit
subobjects.Fruit
which is a leaf subobject.updateMall(...)
calls updateShop(...)
, which calls updateBasket(...)
, which calls updateFruit(...)
.
Each of these updateXxx(...)
function sends a XxxUpdatedEvent
once it is done.
Therefore, if updateMall(...)
is called, there will be many event sent: FruitUpdatedEvent
, BasketUpdatedEvent
, ShopUpdatedEvent
, MallUpdatedEvent
.
This granularity of event is desirable because some event listeners may only care about e.g. BasketUpdatedEvent
and not necessarily about parent-data event.
Here is a sequence diagram of 2 producers and 2 listeners:
As you can see, Listener2
needs to keep all data in sync with its own internal business logic. Therefore it needs to listen to everything. It cannot only listen to MallUpdatedEvent
, because sometimes only FruitUpdatedEvent
will be emitted.
(In my real life scenario, this Listener2
is a ElasticSearch index, trying to keep up with the changed data).
The issue here is, as you can see, the redundancy of the nested synchronization. The sub-object synchronisation methods are called more than needed to do the same job over and over.
I am pretty sure this design issue is common, and people had to address it over and over, but unfortunately my googling efforts didn't pay off. What is a good design pattern to address this issue in a efficient, granular and elegant way?
I've thought of some debouncing mechanism, but it is not bulletproof.
From your question, it appears that you can design your system to maintain the following invariants and that you probably already did:
updateFruit
is the only place in your logic where fruits may be altered.updateFruit
always triggers the FruitUpdatedEvent
.Likewise for the other entity types. updateBasket
might add, remove or perhaps reorder the fruits associated with a basket, but changes to the fruits themselves would be deferred to updateFruit
.
Under these invariants, costlyBasketSynchronize
does not need to call costlyFruitSynchronize
. After all, if any of the fruits in the basket was modified along with the basket itself, then the FruitUpdatedEvent
has already been triggered and handled. This principle extends to the higher levels of nesting as well.
Backbone, a client side framework for JavaScript, has been successfully following this principle since 2010. It has a similar hierarchy, but with only two levels. The smaller entity is called Model
and the larger, containing one is called Collection
.
Model
has a set
method to update its attributes. If any of the attributes change value, the model triggers a change
event.
Collection
also has a set
method, to update its models. This might add, remove or reorder models associated with the collection. In addition, it recursively calls the set
method of each model to update its attributes, which in turn might trigger change
events. The collection itself triggers a single update
event at the end, regardless of whether any of the models in it have changed attributes or not.
A listener interested in both levels of the datastructure simply subscribes both to the change
and the update
events. The change
listener handles model attribute changes while the update
listener handles collection composition changes. It is a very clean separation of concerns and there is no redundancy in the system.