napari: proposal: reducing complexity of models
š Feature
Reduce complexity of current model code by factoring out user-facing API as a view, business logic as a controller, and data structures as a model.
Motivation
Model code is getting excessively complex. Files are typically over 1000 lines long and difficult to parse/keep track of whatās going on. Currently, our models are dealing with data management, presentation logic (i.e. Python magic & data conversion to make it better for users), and business logic.
Right now I feel like we have to make tradeoffs between nice UX and maintainability for the API and separating these components makes it easier to maintain without sacrificing our nice API. Take for example how we handle private variables: either they go without any documentation and can be confusing to the developer or they wind up awkwardly in the public-facing documentation where it doesnāt really belong and confuses the user.
Furthermore, I think that the current user-facing API functions more like a view since we do a lot of input/output conversions and use custom Python syntax for a nicer user experience. Perhaps the best example of this is the LayerList.
Pitch
I want to split out the current model code into model, view, and controller classes.
Model
The model would be a simple data class that notifies when its fields are changed.
from dataclasses import dataclass
@dataclass
class PointsLayerModel(BaseLayerModel):
data: Array = notifier()
mode: Mode = notifier()
...
This makes it easy to keep track of all relevant information pertaining to the state. You can even easily convert this state into a dictionary for saving and sharing with dataclasses.asdict
. Keeping this class simple may also make it easier to handle settings and create autogenerated code from the state.
View
The view would be a simple shell that mimics what you can do with the GUI. It emits events when a user interacts with it and may clean the input before handing it off to the controller.
class Points(Layer):
@property
def mode(self):
return str(self._model.mode)
@mode.setter
def mode(self, mode):
self.events.mode(value=Mode(mode))
Controller
The controller handles the business logic, connects the model to the view, and modifies the model. In a lot of cases it will be a simple event handler that prevents setters from being called multiple times (this can be autogenerated).
class PointsLayerController(BaseLayerController):
def on_mode_change(self, event):
with event.source.events.mode.blocker():
self.model.mode = event.value
def on_data_change(self, event):
data = event.value
data_view = ...
# calculation of data view goes here
self.model.data_view = data_view
Alternatives
We could factor out the view without separating the model and the controller and it wouldnāt be too big of an issue, but I believe that it would make it easiest to maintain in the future.
Additional context
Flow of current structure (diagram by @shanaxel42):
Flow of structure in #1040 (diagram by @shanaxel42):
Flow of proposed structure:
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Comments: 16 (11 by maintainers)
Thanks to everyone for weighing in! It seems that there is pretty strong consensus towards keeping the public API a model, so Iāll ditch API View idea, but Iām glad that people are considering the use of a controller (no surprise from @shanaxel42 tho since she was the one who first proposed it š). Maybe we can use this issue to work out what belongs in the controllers vs everything else.
@0x00b1 Iād love to work on this with you! I donāt have any experience in architecture/other stuff so I would really appreciate your knowledge on what to do š
@sofroniewn youāll need to create custom classes that mimic python data structures like what we did with
LayerList
. However, Iām not sure how much @jni would be a fan of this šHello! First off I know Iāve been super out of the loop on things so my opinions here may not be up to data with the current discussions. But wanted to give some input. Thanks @kne42 for writing this up, and especially for making the diagrams.
So what I like about this proposal is the addition of the ācontrollerā as the place where you cleanup data before updating the model or add other logic. It seems like the main issue around the model files getting bloated could be solved by just adding this layer.
I see the appeal of wanting to make the separate view file for interacting with the model but Iām with @jni that āThe model should be the APIā. At lest for the developer use case.
I would also strongly discourage moving in any direction that continues to use the blocker pattern, it just creates a high likelihood of infinite loops or repeat calls and was one of the main things I was trying to eliminate in my original proposal.
Napari is unique in that it supports interfacing with the data from both the GUI and the data directly. And now a third use case - plugins. If you wanted to incorporate the addition of controllers my suggestion would be to have any use case besides direct data manipulation go through the controller. So what this looks like for each use case:
Developer Scenario a user wants to interact with the data directly. They should be able to directly update the models which emit an event that the event handler picks up to update the views.
GUI user scenario a user interacts with the data though the GIU, an event is emitted, the event handler picks it up and calls the controllerās update method which then updates the data model
Plugin Scenario A plugin interacts with the models by calling update methods defined in the controller. This also has the advantage of more control over what a plugin can/cannot do with models.
Anyway my 2 cents, thanks @kne42 for all your hard work on this!
In my opinion, the plug-in interface shouldnāt make any assumptions about the user interface (or even the existence of a user interface). As a teenager in the late-90s, I have endearing, nostalgic memories of insane VST plug-in interfaces, but thatās not something I want to revisit!
Probably? I havenāt been active on this repo in a while, can you catch me up to speed on what weāre doing with those?
@kne42 I love the diagram. My suggestion:
I think this has the benefit of making it orthogonal to plug-in discussions.
šÆ
Napari hasnāt reached a 1.0 release, thereās no Napari publication, and (as far as I know) thereāre few Napari users. From a maintenance perspective, publicizing classes like
napari.layers.Image
(and encouraging their use) will help ensure Napariās fundamental data structures remain (or, depending on your perspective, become) ergonomic, tested, and documented.