Let’s pretend I work on a project tracking application. The application is a database backed, server hosted, web application. In this application there are Projects which have many Activities which have many Tasks. A Task has two date fields an originalDueDate and a projectedDueDate.
In addition, there are dynamic fields on the Activities and the Projects which indicate whether the Activity or Project is behind schedule based on the projected due dates of the child tasks and various other variables such as remaining buffer time, etc.
There are a number of things that can cause the projectedDueDate to change. For example, an employee working on the project may (via a server request) enter in a shipping delay. Alternatively, a site may (via a server request) enter in an unexpected closure.
When any of these things occur I need to not only update the projectedDueDate of the Task but also trigger the corresponding Project and Activity to update as well.
What is the best way to do this?
I’ve thought of the observer pattern but I don’t keep a single copy of all these objects in memory. When a request comes in, I query the Task in from the database, at that point there is no associated Activity in memory that would be a listener. I could remove the ability to query for Tasks and force the application to query first by Project, then by Activity (in context of Project), then by task (in context of Activity) adding the observer relationships at each step but I’m not sure if that is the best way.
I could setup a database event listening system so when a Task modified event is dispatched I have a handler which queries for the Activity at that point.
I could simply setup a two-way relationship between Task and Activity so that the Task knows about the parent Activity and when the Task updates his state the Task grabs his parent and updates state.
Right now I’m stuck considering all the options and am wondering if any single approach (doesn’t have to be a listed approach) is jumping out at others as the best approach.
1
First: never feel bad about storing data in two places. This is done in “Reporting” / “Warehousing” databases all the time – it’s a de-normalization and it’s perfectly acceptable. As long as you have disk space to do it, great! One less join or query is a lot less load.
Now… architecturally this type of problem happens when you have either:
-
An Aggregate (which is the combination of objects that aren’t directly related but are required for business logic to apply.
-
An Event (which is the type that happens in Publish and Subscribe models) that needs to be broadcast to one or more subscribers.
Likely you have #2 – and rather than hacking it up in the database, consider a few alternatives. You’re on the right track with Observer pattern.
Solving:
-
The best thing you can do is create a new object. Load data from the disparate objects through whatever mechanism(s) via one or more factories then apply your business logic to compute the new resulting date and persist to the database via your normal route which may involve Converters to get back to your standard entities or simply SQL Updates to change the dates.
-
Sounds like you require an event – when data needs to be shared between two pieces of the system that don’t normally talk to each other during the current execution path, events can give you a clean way to communicate. This is especially true in your scenario when multiple commands (requests) can cause this event.
In #2, you can go the route of “eventual consistency” and process the event asynchronously or you can opt to handle it within the same transaction. Without knowing more about your architecture, it’s hard to say how to make this event mechanism available.
Remember, you don’t need one subscriber for every possible Task. Just one (per type of event) that receives the information required, loads the Activity or Project (if required) and makes the changes needed to keep the system synchronized.
Check out DDD – even if you don’t use it, it helps you think about problems in different ways. (also CQRS and Event Sourcing are great topics)
Let’s take a look at your observer pattern: As you stated, parent objects may not be in the memory at the time of change. The observer pattern is only applied to situations where the entire observer hierarchy is present in the memory so that changes may be reflected directly. To consider this option would mean considering the following:
- What does it mean for the server to contain the entire hierarchy in memory? Can the server handle it under all circumstances?
- What does it mean for storage? Does every change need to be applied to the database immediatly? Is event-queuing something that needs to be considered at this point?
However, there is one answer in the form of the observer pattern possible, where you could delay all these caveats (until a design restructure would require it). This would be a design that I haven’t seen used in practice yet: create a Observer class where it’s instances would be pointing to the parents, instead of having the parents be the observers. So they can for example hold the database record id of the parent that they’re observing for.
This way, when a Task for example is loaded, you could also load all the observers from the database that observer that task (from a seperate table with records each pointing to 1 task and 1 observer, then get all observers by task ID). Now you don’t have to get the entire hierarchy, only the observer objects that observe said task.
In these observer objects you would then have the parent’s ID as a variable, so that when the observers need to update, you could go for one of the following options:
- Load observing parents into memory, then change them and have the changes reflected in the database. Most likely used option for: when the parent objects in question are needed rather soon again, to not have to reload them into memory again so soon.
- Update the parent objects in database directly. Most likely used option for: not needing to get data from the database in order to perform the update operation.
But seeing as the hierarchy of observers may be deeper than just 1 level, chances are a task update results in a activity update, which results in a project update. So to deal with this, you’d have to load the observers that observe the parent of the Task in question. Because of this, the observer objects would then have to check if their target parent has observers, after the direct update is complete, to be able to notify the parent’s parents. I’m not sure if a generalized recursive structure for this is advisable, you’d have to find out for yourself if the project/activity/task structure could become more complex in the future, in order to decide on that.
Anywho, I hope this helps. Good luck.
As already said, I don’t think the Observer pattern applies, because your Projects, Activities and Tasks are not objects. You cannot register an individual Project record to receive notifications from Task record updates. The Observer patterns could be used only with objects in memory.
You could put a trigger on the Tasks table to runs a stored procedure that updates the relevant Activity or Project. This can work but you have to implement part of your business logic in SQL. It would probably duplicate code in your server. Also, when you update multiple Tasks, you would trigger that procedure as many times as you have tasks. It is not necessarily performant.
You have to think the dependency is not necessarily from Task to Activity, You might want to move an Activity with all its Tasks in bulk.
What I would suggest is to wrap all your schedule updates in module I’ll call the “Scheduler”. The Sheduler would provide a number of services for adding or updating your Tasks, or Activities or Projects. All updates would go thru that Scheduler. Whenever a request is made, the Scheduler would open a transaction on your database, execute the requested updates and adjust any other dependent data acordingly before committing.
It might look a bit inelegant, the updates are not automatic, and need to be written out, but I am pretty sure the code would be easier to understand and to debug.