Pregunta

A problem been thinking on...

I'm on an application that involves two web maps, both showing the same data but in slightly different situations basically (say a different basemap or projection, etc)...

They both need to be in sync, so if I add some data to one the other should update. If I remove data from one, the other should update. If I move my map around on one, the other should move...etc. Then it sometimes gets more complicated with some business logic of like...I need to convert projections between each, users can unlock it so both don't move around in sync, etc.

Is there a good design pattern for this? To reduce duplicate code of maintaining both map states?

My first thought, maybe have some sort of Observer pattern of a MapsState that will take in events and update both maps as see fit. Abstract all the logic for maintaining these two states in there....and the ui/view code would just talk to this MapsState.

Though other ideas?

¿Fue útil?

Solución

I've encountered a similar problem in a project. Basically, having multiple views (on different devices) that relate in part to the same data, and in part data and actions specific to that view but are still connected via shared business logic.

Now, the exact solution for your problem depends heavily on what kind of architecture you're using and the technology at your disposal. In my case I was working with multiple instances of a Node.js monolith and React+Redux clients, so the decision to use WebSockets to communicate state syncs and Redis as a client for server instance pub-sub syncing was natural because of the existing solutions in the ecosystem and the scope of the app. However, the basic concepts and steps should be at least in part transferable to any stack.

Rather than just referencing an established pattern for you to model to your problem, I’ll go into a bit more detail about the implementation/modelling steps. Some of these you’ve already done and some are pretty obvious, but I’ll list out everything so that this post is complete on its own in case someone has a similar issue. Note also that these steps should be done in parallel because at any step you might need to revisit previous steps as new insights come.

Step 0: List possible state changes

Before you do anything, you need to decide what actions you will support and how they impact each of the views (maps). This does not have to be a complete list in terms of listing all the actions, but each action should be categorised in some abstract action (e.g. placing a rectangle or a circle on the map are basically the same thing - adding a shape on a map layer - they just require different data points and draw logic). You can approach this as identifying all the abstract data entities (e.g. points, shapes, connections, transformations, layers…) you have in your logic and which CRUD operations they support.

Step 1: Identify non-static data

First and foremost, identify the minimum amount of data is required to represent each of the update (state change) actions for each of the views. What does each view require to represent some state change? In other words, model the data which isn’t static. This is crucial since you’re working with geospatial data which can be relatively large and introduce extra latency if you decide to send large parts of the state around.

Step 2: Model and store that data

Model all of that data into a state that can be used to initialise any of your views and store it in one place as a single point of truth. Basically, create a database schema for your state. What kind of storage you’ll use for this depends on the end result of your model and of course your architecture. My guess is it’s gonna be a remote database - either relational or document based depending on the model.

Step 3: Extract all view client logic

At this point you should have a clear distinction between what data is in your new “database state” and what data is possibly view client specific. Each view can now have its own state that is an extension of that “database state” combined with the data specific to that view. Isolate all logic that is related to just that view - how it represents data, how it sends state change actions, how it reacts to state changes.

Step 4: Create a “server“ between the views and the database

Now that you have both your “database” and the views with their own logic, you need create a “server” that does database updates and sends state change notifications to your views. This does not have to be a single app. You can go heavy into microservices here and make a service for each update action and also make services that are subscribed to the datable state and send sync events to the view clients if the state changed. This part can also be scaled by utilising some pub-sub solution like Apache Kafka or RabbitMQ, but my guess is that’s an overkill for your use case. If you want to do multiple services, I advise starting by making one for sending out notifications to view clients, one for each distinct set of updates and one for any extra data requests. That should be enough for you to have a structure that can get further divided or combined depending on your requirements.

As for the details on how to handle the communication between the server and the view clients, this can be approached in multiple ways, but on the web they all end up as a variant o this:

  • Use HTTP requests or WebSocket events to send state update requests from the view clients to the server.
  • Use Server-Sent Events or WebSocket events to send state sync notifications from the server to the view clients.
  • If the sync notifications have a large payload and cause latency, separate that event into the server just sending a notification that something has changed, and make the client fetch the state change itself

Step 5: Locking mechanism

Some complex or long actions will require you do lock a part of the state. Use the system described above to implement a locking mechanism for something that only one user should be able to change at any given moment.

This can easily be done by treating the operation as a transaction where you start the operation with sending a lock request to the server (lock the minimum amount of the state that needs to be locked). The server then saves the lock to the state in case any new view clients get added while the operation is in progress, and sends sync notifications to all the existing view clients that that part of the state is locked by view client X. View client X then does all it needs to do and unlocks the state at the and. Additionally, don’t forget to add some kind of timeout mechanism in case view client X disconnects before unlocking the state.

 

Hope this helps. The described solution is a bit client heavy because I suppose that an application that does operations on maps and geospatial data is already client heavy, but more things can easily be moved into the database state and the server operations if needed. Additionally, this whole abstract implementation can easily be used on a non-web app just by approaching Step 4 as some kind of controller service, or whatever your platform has that fits the role of a server.

Otros consejos

It sounds like there is some canonical format for the data ("same data"), and then each of the map front-ends has its own in-memory representation for the purposes of interaction.

If you're able to use microservices in your application, you can separate the persistent storage of the map data into a separate service, then use a publisher-subscriber pattern (pub/sub) to broadcast updates to both of the map front-ends when a data change is made.

The info you choose to broadcast can be tuned to minimize RPCs from the maps to storage. For example, if the maps show customized regions, the published events might just broadcast entity IDs and their locations. Then each front-end can just send RPCs for the updates that affect the region being displayed.

Regarding the handling of actual data updates, it's up to you whether or not it's done atomically. For example, if one map front-end updates a data point, you could block out further interaction in that front-end until the respective pub/sub event is received, or not.

The convenient thing about centralizing storage here is that it allows you to scale up to more than two front-ends without needing to modify any of the existing ones. It also keeps updates of other front-ends from being a bottleneck, e.g., if one of them is too busy to accept an update.

Licenciado bajo: CC-BY-SA con atribución
scroll top