Separation of Concerns
Back when I was testing out the WAV file and graphics libraries and beginning to dabble in wiring them together, I saw that mingling the code making calls to each of them would soon become intractable, even in a prototype. So I set about segregating them into distinct areas within the codebase, dividing their concerns, secluding the types they used and setting up interfaces to isolate things -- this also helped quarantine IO
used in the graphics code.
I had decided early on that I wasn't going to try to approach this application with a specific architecture already in mind, since I didn't know if there was some uniquely functional architecture that would emerge or if an already-established architecture would present itself along the way.
In separating the code for calling these libraries, though, I was struck by the impression that the new areas conformed loosely to the Model and View of some MVC-esque architectural pattern. In an effort to aid my thinking, I decided to employ that terminology as a working vocabulary, at least until something better came along.
Pipes
As part of isolating and modularizing the different areas in my code-base, I decided to employ the pipes library for conducting communication between them.
For those unfamiliar with it, pipes offers a monadic layer for piping communication between discrete stages in an application. Each stage can be one or a set of methods working in a loop which sends information up- and/or down-stream to neighboring stages in the pipeline. These stages can almost look and feel as though they're each running in their own threads/actors, since they're passing messages back and forth, but everything's actually running sequentially under the hood.
Some of the higher-level concepts used by the pipes library mapped pretty well to the emerging areas of the application: the model matched a pipes Server
, the view a pipes Client
, and the glue code connecting the two could run on top of a third bi-directional pipe stub -- a Proxy
in pipes types.
The Client would send (user) input to the glue code via a pipe request and receive what to draw for output as the response; the Server would receive requests to update the model from the glue code and provide the current model state in response.
The glue stage would translate between the two other stages, selecting the appropriate model transformations to do based on the current state and user input and translating the model state into something that could be displayed by the view.
Model-View-Presenter
As things were progressing, the distinct areas of the codebase started to jell, and eventually the responsibilities of the glue code became apparent to me. Once they had, I recognized that the glue seemed to constitute an architectural layer in its own right, so I did some research.
After a bit of reading, I found that the Model-View-Presenter pattern seemed to be a very good match for how my code was growing, so I decided to push things in that direction:
- The view is pretty dumb, just pushing input from the user to the presenter and drawing what the presenter tells it to as output
- The presenter (formerly the glue code) holds all the user-interface logic, taking the user input and current application state to decide what updates to do to the model and then telling the view what to render
- The model holds the application state and domain logic, performing the updates it's told to do and passing the current state (representation) back to the presenter
It was quite interesting to see how naturally this architecture emerged.
Audio Playback
The only application responsibility not represented in the above diagram is audio playback. Playback functionality is provided by the SDL2 library, which, given the separation outlined earlier, is under the view's purview; but the control of playback is done as part of the model. This seems to be in violation of the segregation of library code that was done, but it is mitigated by the fact that playback (control) has been abstracted away from, meaning the implementation lives underneath the abstraction boundary and the model doesn't directly interact with the graphics library.