Skip to content

Instantly share code, notes, and snippets.

@HallM
Created January 4, 2016 22:48
Show Gist options
  • Save HallM/bd18724ad19e3adb83a2 to your computer and use it in GitHub Desktop.
Save HallM/bd18724ad19e3adb83a2 to your computer and use it in GitHub Desktop.
Evolving the MVC pattern for the real world
universe: everything that isn't this software
can only interface with software through the shell
- user
- monitor
- browser
- database
- file system
- external API
- API on the same machine but different piece of software
- sensors
- time
shell: deals with the universe, "translates" between universe- and internal- schemas/languages. the I/O
each piece is isolated from each other and should send data through a controller to reach another shell piece
if you need to get to another shell (for logging), pass through a controller to get there
if an error occurs, handle it in an controller where the error could be processed and user notified.
- view
* could be a single piece like React components, a view in Java.
* could be separate with input, such as button click, handled in different code than the output, rendering
- URL router
- database code ("models", but not 100%)
- logger
- some API communicator code
- sensor interface
- cron-like task scheduler
- just about any event emitter (the emitter itself, not the handler)
controllers: handles events from the shell, processes data, and (more than likely) sends data to the shell
can get to everything in the software including other controllers
controllers are not just processing req's. they exist for data flow, limitation of access and responsibility
internal fn()s: stateless utilities to perform calculations. can mutate. mutation is an optimization.
can call other fn()s, but nothing else. can be called from controllers or other fn()s. returns data back up
no logging for internal fn()s. The primary excuse for logging (testing steps) is handled by proper tests.
if intermediaries of a calculation need to be logged, return them for the controller to handle
------------------
what about:
- libraries
- schemas
- state
Libraries
the libraries themselves may be together on disk (/lib), but where they are used depends on purpose
libraries exist all over. shell code probably use a networking library. fn()s may use a math library.
odds are, a library would not be used in multiple areas, as that library probably crosses boundaries/purposes
yet, this could happen. just be sure to make sure the library's functions are used in appropriate areas.
language standard lib. strings library. you'll probably use them everywhere.
if it makes external access, it should be called by shell code. otherwise, use best judgement.
Schemas
schemas are data types. they define how data should look. they are static.
they belong nowhere and yet everywhere. much like "int" is accessible in the software, these are as well.
are they exposed to the universe? maybe, maybe not.
there will be universe schemas the shell must deal with. there will be internal schemas all code deals with.
some schemas may live internally and externally and some may be derivatives of another.
using an ORM can complicate things. the "model" exists in the shell including the generated/inherited code
try to split the schema out (ducks, interfaces, whatever you can use)
most ORM functions (find, update, remove, etc) cannot be called by the fn()s, only controller or the models.
cross model calls should be handled through a controller.
while you could abstract away the ORM to an internal schema, why use an ORM? the ORM already is an abstraction
the fn()s should only understand the schema of the data, not the ORM model.
if you absolutely cannot spit schema from model:
- creating a new type containing the data could duplicate the schema, which leads to inconsistencies
the inconsistencies could be mitigated by defining types based on fn() requirements
- use code review, linter, anything you can to make sure ORM functions do not occur in fn()s
State
last because it's complicated. pretending state can be offloaded/ignored is just obscuring the necessary evil
security through obscurity doesn't work. don't make state more complicated. it's hard enough.
minimize global state. global state is a last-resort optimization (caching anyone?). easy to mess up.
constants are, well, constant. they are not state. global constants are acceptable.
constants loaded from external sources should be loaded on boot, accessed as read-only (lang set once or wrap)
application settings that can change aren't really constant. they can change after all.
what if the application is run twice, one instance changes settings.
game balance settings, even loaded from external for ease of changes, is constant. the app doesn't change it.
shell code handling some input should ideally generate a "local state" that the rest can work from
"localize" global state with a copy or synchronization. failure to do so can lead to race conditions.
fn()s only ever work from "local state" passed in through args. no globals allowed.
global state does not need to be centralized. it's ok to have different state controllers for different data
mutating a global? definitely must be synchronized.
- what about those localized copies?
if you need the absolute latest value, use synchronization not copy
if you expect to change a value and then use it, either:
think really hard if you *really* should be doing this, may be another way
change the localized first and global later (no need for latest value elsewhere)
use synchronization (other areas need the latest value)
if the new value relies on the old value, take extra care. use sync heavily. can lead to data corruption
with a DB, use database-side mutations (COL=COL+formula for SQLers; $inc, $push for Mongo'ers; etc)
with application state, must synchronize the entire processing from get state to set state.
in short, "global state" exists within the controller level
"local state" is passed around during processing
loading "state" from a database happens. this state could be "global" in nature while looking innocently local
ideally, contracts are placed on code to specify what state is accessed or modified. make global state obvious
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment