Reckoner Update June 2023
The progress was pretty incremental this month as well. I’ve made some pretty big infrastructure backend changes.
☁️ Sync Updates
I had some difficulties at first setting up PeerDart. Originally I was trying to reuse some of the existing synchronization code I wrote for PocketBase, but I ultimately needed to write wholly new code to handle the case of peer-to-peer communication. I now have the ability for two peers to initiate communication with each other, and exchange device information.
The synchronization infrastructure will be end-to-end encrypted. I am currently using a symmetric key exchange instead of a public-private key exchange since it is simpler to use symmetric keys. I might consider making it more complicated in the future and looking at per-device keys instead of one key for each device. This would increase security.
I also need to think about how to handle adding the third or more devices and letting other devices know about that device. This will be an area of focus for next month.
🧑💻 Refactoring
Or how I learned to stop worrying and love MVC.
Background
To provide some background on the latest work, I need to let you know about one of the members of my Brain Trust. He’s the OG member when I wanted to have an excuse to talk more often, but needed a structured way to do so. Let’s call him Frood. He has a background in human factors for man-machine interfaces (think layout of screens and controls for a process) and some background in managing development projects. One of the first things he hammered on when I was outlining the infrastructure is what is the Model, View, and Controller for Reckoner.
Original Organization
I resisted the MVC pattern at first and looked up other organizational structures which people were using for organizing Flutter projects. I can’t find the reference now, but I settled on a folder structure based on what the code did. Before the refactor I ended up with a folder structure like this which roughly corresponded to the following in each folder.
- config - Constants or static helper functions and classes
- config/layout - Device specific LayoutConfig class implementation
- data - Anything todo with the database, root folder held classes for Drift SQL ORM
- data/models - held *Info immutable classes for loaded data and *Save classes for editing data
- data/database - Only file here is database.dart for the ReckonerDb object
- data/database/dao - Database Access Object, used to house special logic for loading and saving data
- data/database/tables - Table definitions
- routes - Widgets for the routes of the application. Each file is named according to the route; /account/edit/{id} is account_edit.dart.
- widgets - Any widget which isn’t a route is in here
- widgets/form - Widgets specific to input forms
- widgets/options - Widgets specific to the /options route
- util - Anything which doesn’t find into the other folders
The issue was that concerns were poorly separated. Widgets were often directly accessing the ReckonerDb object to get a data stream from a DAO query. The worst case was the category_group.dart which had both presentation of the data, mutation of data to alternative classes and models, and logic around setting balances and doing computations. Another pain point was that some views had to deal with 3 different classes for the same data type (Transactions - SQL ORM model, TransactionInfo - immutable data with relationships to other class entities, and TransactionSave to handle editing and saving of the transactions) while other just simply used one class like the currency edit.
One common thread is that I tended to code my view and model with a backend mindset. The *Info classes would often have helper functional useful for presenting the data. Similarly, the *Save classes all had a .save() function which would do validation of the *Save model state and save it to the database. However, a widget could be working with a *Info object, *Save object, the ORM classes, or occasionally all three! Grouping data and the logic that acts on the data is generally a good practice in the backend, but has made the code unwieldy in the frontend.
New Organization
I first tackled this by seeing how I could eliminate the *Info and *Save classes. After several iterations and some tests later, I ended up with the following folder structure.
- config - constants and a few configuration specific files
- model - houses all of the Drift ORM classes
- model/database - houses the Drift ORM base definitions
- model/database/dao - handles loading of complex entities and setting object relationships based on table foreign keys. This could be moved to the controller, but is the sole piece of logic I want to host next to the rest of the data models.
- model/database/tables - ORM table definitions
- model/util - currently houses files to help with JSON serialization of models
- controller - houses all of the business logic for the application. Also houses classes globally available
- controller/database - database entity specific logic for saving models
- controller/database/instance - platform specific files for acquiring SQLite DB (web vs the rest)
- controller/util - one-off internal logic helpers (CSV and encryption handlers right now)
- view - Everything that used to be in routes
- view/widgets - Any widget which isn’t a route is in here
- view/widgets/form - Widgets specific to input forms
- view/widgets/util - Utility functions for widgets
- view/widgets/util/layout - Layout platform specific handling of layout (desktop minimum window size)
- view/widgets/options - Widgets specific to the /options route
- view/model - View specific models. Currently only a model for loading data for report display. This might eventually be moved to the root model folder, but it isn’t an ORM model as per the other files in the model folder…
This may seem more complicated just looking at the folder structure, but has greatly simplified separation of concerns. I strive so that files from different root folders shouldn’t need to import anything more than one directory deep. Thus the view should only need to import the necessary classes from the model directory for the models it is touching and the controller class for any data loaders and logic. Also, the controller shouldn’t ever load anything from the view folder and the same for the model with the controller and view. Additionally, I have been actively moving logic to the controller folder so the view folder is strictly presentation.
I also stopped creating unit tests about a year ago originally since I was spending too much time maintaining them due to changes. I’ve come to realize now that it is much easier to test and maintain the unit tests when I’ve stopped intermixing the logic and presentation. I’ll start to consider writing tests again and looking at my testing coverage for the controller and view folders. The goal is to have nothing, or next to nothing to test in the model as all of the logic is moved to the controller folder.
🧑🏫 Lessons Learned
Conventions are conventions for a reason. However, I tend to learn a few things the hard way 🤦. Frood said it best when I first announced the conventions change.
While this may seem painful, the value of architecture only begins to shine when you reach a certain level of complexity. Congrats, Reckoner has breached the threshold!
This is very true. When I was first programming the simple bits, currency and accounts at the time, it seemed like separating these concerns out just made it more work and more complicated. Now that the application is sufficiently complicated, I see how conventions like MVC really help.
Don’t confuse this for me saying MVC is the be-all, end-all of application architecture either. There are lots of good alternatives to MVC for frontend application development and MVC makes no sense for backend development. In fact, a lot of my background was in backend development where the common patterns I encountered were object oriented coupling of data with logic specific to that data and functional where functions would transform data as needed. Even the time I spent doing frontend development was atypical as we were using Visual Basic for the frontend and the code was action-procedural.
It also helped for me to have the concept re-contextualized and mentally relabeled. I think of the view as functional and a function of the input state, the model as pure data (as much as possible), and the controller as all of the logic necessary to glue the other two. I’m not sure why this specific reframing helps me, but it’s what finally got me to understanding the concept and its importance.
What’s Next
More tasks have been added to version 0.2. Still, good progress for this month.
General Tasks
- Make Reckoner documentation website
- Thoroughly test the application over several days using realistic workflows
Version 0.2: Multi-Device Synchronization
- Investigate peer-to-peer synchronization
- Add PeerJS dependency
- Device communication and key exchange
- Determine infrastructure for synchronization chain
- Determine how to remove compromised device and notify other devices
- Update database with synchronization model
- Synchronize data based on last sync time
Version 1.0 (Future)
- Release on FDroid
- Release on FlatHub
- Release on Google Play